first commit

This commit is contained in:
2025-07-16 22:08:48 +03:00
commit d692b55e9f
52 changed files with 9386 additions and 0 deletions

7
.editorconfig Normal file
View File

@@ -0,0 +1,7 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 2
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
.DS_Store
.thumbs.db
node_modules
# Quasar core related directories
.quasar
/dist
/quasar.config.*.temporary.compiled*
# Cordova related directories and files
/src-cordova/node_modules
/src-cordova/platforms
/src-cordova/plugins
/src-cordova/www
# Capacitor related directories and files
/src-capacitor/www
/src-capacitor/node_modules
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
# local .env files
.env.local*

5
.npmrc Normal file
View File

@@ -0,0 +1,5 @@
# pnpm-related options
shamefully-hoist=true
strict-peer-dependencies=false
# to get the latest compatible packages when creating the project https://github.com/pnpm/pnpm/issues/6463
resolution-mode=highest

6
.prettierrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

15
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"editorconfig.editorconfig",
"vue.volar",
"wayou.vscode-todo-highlight"
],
"unwantedRecommendations": [
"octref.vetur",
"hookyqr.beautify",
"dbaeumer.jshint",
"ms-vscode.vscode-typescript-tslint-plugin"
]
}

15
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": [
"source.fixAll.eslint"
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"vue"
]
}

40
README.md Normal file
View File

@@ -0,0 +1,40 @@
# LandingPage (projectsnodelandingpage)
A Quasar Project
## Install the dependencies
```bash
yarn
# or
npm install
```
### Start the app in development mode (hot-code reloading, error reporting, etc.)
```bash
quasar dev
```
### Lint the files
```bash
yarn lint
# or
npm run lint
```
### Format the files
```bash
yarn format
# or
npm run format
```
### Build the app for production
```bash
quasar build
```
### Customize the configuration
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).

73
eslint.config.js Normal file
View File

@@ -0,0 +1,73 @@
import js from '@eslint/js'
import globals from 'globals'
import pluginVue from 'eslint-plugin-vue'
import pluginQuasar from '@quasar/app-vite/eslint'
import prettierSkipFormatting from '@vue/eslint-config-prettier/skip-formatting'
export default [
{
/**
* Ignore the following files.
* Please note that pluginQuasar.configs.recommended() already ignores
* the "node_modules" folder for you (and all other Quasar project
* relevant folders and files).
*
* ESLint requires "ignores" key to be the only one in this object
*/
// ignores: []
},
...pluginQuasar.configs.recommended(),
js.configs.recommended,
/**
* https://eslint.vuejs.org
*
* pluginVue.configs.base
* -> Settings and rules to enable correct ESLint parsing.
* pluginVue.configs[ 'flat/essential']
* -> base, plus rules to prevent errors or unintended behavior.
* pluginVue.configs["flat/strongly-recommended"]
* -> Above, plus rules to considerably improve code readability and/or dev experience.
* pluginVue.configs["flat/recommended"]
* -> Above, plus rules to enforce subjective community defaults to ensure consistency.
*/
...pluginVue.configs[ 'flat/essential' ],
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
...globals.node, // SSR, Electron, config files
process: 'readonly', // process.env.*
ga: 'readonly', // Google Analytics
cordova: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly', // BEX related
browser: 'readonly' // BEX related
}
},
// add your custom rules here
rules: {
'prefer-promise-reject-errors': 'off',
// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
},
{
files: [ 'src-pwa/custom-service-worker.js' ],
languageOptions: {
globals: {
...globals.serviceworker
}
}
},
prettierSkipFormatting
]

BIN
i18n-2.xlsm Normal file

Binary file not shown.

21
index.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title><%= productName %></title>
<meta charset="utf-8">
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="icon" type="image/ico" href="favicon.ico">
</head>
<body>
<!-- quasar:entry-point -->
</body>
</html>

3
jsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "./.quasar/tsconfig.json"
}

7653
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "projectsnodelandingpage",
"version": "0.0.1",
"description": "A Quasar Project",
"productName": "LandingPage",
"author": "AlexMart",
"type": "module",
"private": true,
"scripts": {
"lint": "eslint -c ./eslint.config.js \"./src*/**/*.{js,cjs,mjs,vue}\"",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test": "echo \"No test specified\" && exit 0",
"dev": "quasar dev",
"build": "quasar build",
"postinstall": "quasar prepare"
},
"dependencies": {
"vue-i18n": "^11.0.0",
"@quasar/extras": "^1.16.4",
"quasar": "^2.16.0",
"vue": "^3.4.18",
"vue-router": "^4.0.0"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@eslint/js": "^9.14.0",
"eslint": "^9.14.0",
"eslint-plugin-vue": "^9.30.0",
"globals": "^15.12.0",
"vite-plugin-checker": "^0.9.0",
"@vue/eslint-config-prettier": "^10.1.0",
"prettier": "^3.3.3",
"@quasar/app-vite": "^2.1.0",
"autoprefixer": "^10.4.2",
"postcss": "^8.4.14"
},
"engines": {
"node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
}
}

29
postcss.config.js Normal file
View File

@@ -0,0 +1,29 @@
// https://github.com/michael-ciniawsky/postcss-load-config
import autoprefixer from 'autoprefixer'
// import rtlcss from 'postcss-rtlcss'
export default {
plugins: [
// https://github.com/postcss/autoprefixer
autoprefixer({
overrideBrowserslist: [
'last 4 Chrome versions',
'last 4 Firefox versions',
'last 4 Edge versions',
'last 4 Safari versions',
'last 4 Android versions',
'last 4 ChromeAndroid versions',
'last 4 FirefoxAndroid versions',
'last 4 iOS versions'
]
}),
// https://github.com/elchininet/postcss-rtlcss
// If you want to support RTL css, then
// 1. yarn/pnpm/bun/npm install postcss-rtlcss
// 2. optionally set quasar.config.js > framework > lang to an RTL language
// 3. uncomment the following line (and its import statement above):
// rtlcss()
]
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
public/img/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/img/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
public/img/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
public/img/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

228
quasar.config.js Normal file
View File

@@ -0,0 +1,228 @@
// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file
import { defineConfig } from '#q-app/wrappers'
import { fileURLToPath } from 'node:url'
export default defineConfig((ctx) => {
return {
// https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
// preFetch: true,
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [
'i18n'
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
css: [
'app.scss'
],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
'mdi-v7',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it
'material-icons', // optional, you are not bound to it
],
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
build: {
target: {
browser: [ 'es2022', 'firefox115', 'chrome115', 'safari14' ],
node: 'node20'
},
vueRouterMode: 'history', // available values: 'hash', 'history'
// vueRouterBase,
// vueDevtools,
// vueOptionsAPI: false,
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
// publicPath: '/',
// analyze: true,
// env: {},
// rawDefine: {}
// ignorePublicFolder: true,
// minify: false,
// polyfillModulePreload: true,
// distDir
// extendViteConf (viteConf) {},
// viteVuePluginOptions: {},
vitePlugins: [
['@intlify/unplugin-vue-i18n/vite', {
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false,
// if you want to use named tokens in your Vue I18n messages, such as 'Hello {name}',
// you need to set `runtimeOnly: false`
// runtimeOnly: false,
ssr: ctx.modeName === 'ssr',
// you need to set i18n resource including paths !
include: [ fileURLToPath(new URL('./src/i18n', import.meta.url)) ]
}],
['vite-plugin-checker', {
eslint: {
lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{js,mjs,cjs,vue}"',
useFlatConfig: true
}
}, { server: false }]
]
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
devServer: {
port: 9010,
// https: true,
open: true // opens browser window automatically
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
framework: {
config: {},
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],
// Quasar plugins
plugins: []
},
// animations: 'all', // --- includes all animations
// https://v2.quasar.dev/options/animations
animations: [],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#sourcefiles
// sourceFiles: {
// rootComponent: 'src/App.vue',
// router: 'src/router/index',
// store: 'src/store/index',
// pwaRegisterServiceWorker: 'src-pwa/register-service-worker',
// pwaServiceWorker: 'src-pwa/custom-service-worker',
// pwaManifestFile: 'src-pwa/manifest.json',
// electronMain: 'src-electron/electron-main',
// electronPreload: 'src-electron/electron-preload'
// bexManifestFile: 'src-bex/manifest.json
// },
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
ssr: {
prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
middlewares: [
'render' // keep this as last one
],
// extendPackageJson (json) {},
// extendSSRWebserverConf (esbuildConf) {},
// manualStoreSerialization: true,
// manualStoreSsrContextInjection: true,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
pwa: false
// pwaOfflineHtmlFilename: 'offline.html', // do NOT use index.html as name!
// pwaExtendGenerateSWOptions (cfg) {},
// pwaExtendInjectManifestOptions (cfg) {}
},
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
pwa: {
workboxMode: 'GenerateSW' // 'GenerateSW' or 'InjectManifest'
// swFilename: 'sw.js',
// manifestFilename: 'manifest.json',
// extendManifestJson (json) {},
// useCredentialsForManifestTag: true,
// injectPwaMetaTags: false,
// extendPWACustomSWConf (esbuildConf) {},
// extendGenerateSWOptions (cfg) {},
// extendInjectManifestOptions (cfg) {}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
electron: {
// extendElectronMainConf (esbuildConf) {},
// extendElectronPreloadConf (esbuildConf) {},
// extendPackageJson (json) {},
// Electron preload scripts (if any) from /src-electron, WITHOUT file extension
preloadScripts: [ 'electron-preload' ],
// specify the debugging port to use for the Electron app when running in development mode
inspectPort: 5858,
bundler: 'packager', // 'packager' or 'builder'
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
builder: {
// https://www.electron.build/configuration/configuration
appId: 'projectsnodelandingpage'
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
bex: {
// extendBexScriptsConf (esbuildConf) {},
// extendBexManifestJson (json) {},
/**
* The list of extra scripts (js/ts) not in your bex manifest that you want to
* compile and use in your browser extension. Maybe dynamic use them?
*
* Each entry in the list should be a relative filename to /src-bex/
*
* @example [ 'my-script.ts', 'sub-folder/my-other-script.js' ]
*/
extraScripts: []
}
}
})

7
src/App.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<router-view />
</template>
<script setup>
//
</script>

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 360">
<path
d="M43.4 303.4c0 3.8-2.3 6.3-7.1 6.3h-15v-22h14.4c4.3 0 6.2 2.2 6.2 5.2 0 2.6-1.5 4.4-3.4 5 2.8.4 4.9 2.5 4.9 5.5zm-8-13H24.1v6.9H35c2.1 0 4-1.3 4-3.8 0-2.2-1.3-3.1-3.7-3.1zm5.1 12.6c0-2.3-1.8-3.7-4-3.7H24.2v7.7h11.7c3.4 0 4.6-1.8 4.6-4zm36.3 4v2.7H56v-22h20.6v2.7H58.9v6.8h14.6v2.3H58.9v7.5h17.9zm23-5.8v8.5H97v-8.5l-11-13.4h3.4l8.9 11 8.8-11h3.4l-10.8 13.4zm19.1-1.8V298c0-7.9 5.2-10.7 12.7-10.7 7.5 0 13 2.8 13 10.7v1.4c0 7.9-5.5 10.8-13 10.8s-12.7-3-12.7-10.8zm22.7 0V298c0-5.7-3.9-8-10-8-6 0-9.8 2.3-9.8 8v1.4c0 5.8 3.8 8.1 9.8 8.1 6 0 10-2.3 10-8.1zm37.2-11.6v21.9h-2.9l-15.8-17.9v17.9h-2.8v-22h3l15.6 18v-18h2.9zm37.9 10.2v1.3c0 7.8-5.2 10.4-12.4 10.4H193v-22h11.2c7.2 0 12.4 2.8 12.4 10.3zm-3 0c0-5.3-3.3-7.6-9.4-7.6h-8.4V307h8.4c6 0 9.5-2 9.5-7.7V298zm50.8-7.6h-9.7v19.3h-3v-19.3h-9.7v-2.6h22.4v2.6zm34.4-2.6v21.9h-3v-10.1h-16.8v10h-2.8v-21.8h2.8v9.2H296v-9.2h2.9zm34.9 19.2v2.7h-20.7v-22h20.6v2.7H316v6.8h14.5v2.3H316v7.5h17.8zM24 340.2v7.3h13.9v2.4h-14v9.6H21v-22h20v2.7H24zm41.5 11.4h-9.8v7.9H53v-22h13.3c5.1 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6H66c3.1 0 5.3-1.5 5.3-4.7 0-3.3-2.2-4.1-5.3-4.1H55.7v8.8zm47.9 6.2H89l-2 4.3h-3.2l10.7-22.2H98l10.7 22.2h-3.2l-2-4.3zm-1-2.3l-6.3-13-6 13h12.2zm46.3-15.3v21.9H146v-17.2L135.7 358h-2.1l-10.2-15.6v17h-2.8v-21.8h3l11 16.9 11.3-17h3zm35 19.3v2.6h-20.7v-22h20.6v2.7H166v6.8h14.5v2.3H166v7.6h17.8zm47-19.3l-8.3 22h-3l-7.1-18.6-7 18.6h-3l-8.2-22h3.3L204 356l6.8-18.5h3.4L221 356l6.6-18.5h3.3zm10 11.6v-1.4c0-7.8 5.2-10.7 12.7-10.7 7.6 0 13 2.9 13 10.7v1.4c0 7.9-5.4 10.8-13 10.8-7.5 0-12.7-3-12.7-10.8zm22.8 0v-1.4c0-5.7-4-8-10-8s-9.9 2.3-9.9 8v1.4c0 5.8 3.8 8.2 9.8 8.2 6.1 0 10-2.4 10-8.2zm28.3 2.4h-9.8v7.9h-2.8v-22h13.2c5.2 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6h10.2c3 0 5.2-1.5 5.2-4.7 0-3.3-2.1-4.1-5.2-4.1h-10.2v8.8zm40.3-1.5l-6.8 5.6v6.4h-2.9v-22h2.9v12.3l15.2-12.2h3.7l-9.9 8.1 10.3 13.8h-3.6l-8.9-12z" />
<path fill="#050A14"
d="M188.4 71.7a10.4 10.4 0 01-20.8 0 10.4 10.4 0 1120.8 0zM224.2 45c-2.2-3.9-5-7.5-8.2-10.7l-12 7c-3.7-3.2-8-5.7-12.6-7.3a49.4 49.4 0 00-9.7 13.9 59 59 0 0140.1 14l7.6-4.4a57 57 0 00-5.2-12.5zM178 125.1c4.5 0 9-.6 13.4-1.7v-14a40 40 0 0012.5-7.2 47.7 47.7 0 00-7.1-15.3 59 59 0 01-32.2 27.7v8.7c4.4 1.2 8.9 1.8 13.4 1.8zM131.8 45c-2.3 4-4 8.1-5.2 12.5l12 7a40 40 0 000 14.4c5.7 1.5 11.3 2 16.9 1.5a59 59 0 01-8-41.7l-7.5-4.3c-3.2 3.2-6 6.7-8.2 10.6z" />
<path fill="#00B4FF"
d="M224.2 98.4c2.3-3.9 4-8 5.2-12.4l-12-7a40 40 0 000-14.5c-5.7-1.5-11.3-2-16.9-1.5a59 59 0 018 41.7l7.5 4.4c3.2-3.2 6-6.8 8.2-10.7zm-92.4 0c2.2 4 5 7.5 8.2 10.7l12-7a40 40 0 0012.6 7.3c4-4.1 7.3-8.8 9.7-13.8a59 59 0 01-40-14l-7.7 4.4c1.2 4.3 3 8.5 5.2 12.4zm46.2-80c-4.5 0-9 .5-13.4 1.7V34a40 40 0 00-12.5 7.2c1.5 5.7 4 10.8 7.1 15.4a59 59 0 0132.2-27.7V20a53.3 53.3 0 00-13.4-1.8z" />
<path fill="#00B4FF"
d="M178 9.2a62.6 62.6 0 11-.1 125.2A62.6 62.6 0 01178 9.2m0-9.2a71.7 71.7 0 100 143.5A71.7 71.7 0 00178 0z" />
<path fill="#050A14"
d="M96.6 212v4.3c-9.2-.8-15.4-5.8-15.4-17.8V180h4.6v18.4c0 8.6 4 12.6 10.8 13.5zm16-31.9v18.4c0 8.9-4.3 12.8-10.9 13.5v4.4c9.2-.7 15.5-5.6 15.5-18v-18.3h-4.7zM62.2 199v-2.2c0-12.7-8.8-17.4-21-17.4-12.1 0-20.7 4.7-20.7 17.4v2.2c0 12.8 8.6 17.6 20.7 17.6 1.5 0 3-.1 4.4-.3l11.8 6.2 2-3.3-8.2-4-6.4-3.1a32 32 0 01-3.6.2c-9.8 0-16-3.9-16-13.3v-2.2c0-9.3 6.2-13.1 16-13.1 9.9 0 16.3 3.8 16.3 13.1v2.2c0 5.3-2.1 8.7-5.6 10.8l4.8 2.4c3.4-2.8 5.5-7 5.5-13.2zM168 215.6h5.1L156 179.7h-4.8l17 36zM143 205l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.8-3.7H143zm133.7 10.7h5.2l-17.3-35.9h-4.8l17 36zm-25-10.7l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.7-3.7h-14.8zm73.8-2.5c6-1.2 9-5.4 9-11.4 0-8-4.5-10.9-12.9-10.9h-21.4v35.5h4.6v-31.3h16.5c5 0 8.5 1.4 8.5 6.7 0 5.2-3.5 7.7-8.5 7.7h-11.4v4.1h10.7l9.3 12.8h5.5l-9.9-13.2zm-117.4 9.9c-9.7 0-14.7-2.5-18.6-6.3l-2.2 3.8c5.1 5 11 6.7 21 6.7 1.6 0 3.1-.1 4.6-.3l-1.9-4h-3zm18.4-7c0-6.4-4.7-8.6-13.8-9.4l-10.1-1c-6.7-.7-9.3-2.2-9.3-5.6 0-2.5 1.4-4 4.6-5l-1.8-3.8c-4.7 1.4-7.5 4.2-7.5 8.9 0 5.2 3.4 8.7 13 9.6l11.3 1.2c6.4.6 8.9 2 8.9 5.4 0 2.7-2.1 4.7-6 5.8l1.8 3.9c5.3-1.6 8.9-4.7 8.9-10zm-20.3-21.9c7.9 0 13.3 1.8 18.1 5.7l1.8-3.9a30 30 0 00-19.6-5.9c-2 0-4 .1-5.7.3l1.9 4 3.5-.2z" />
<path fill="#00B4FF"
d="M.5 251.9c29.6-.5 59.2-.8 88.8-1l88.7-.3 88.7.3 44.4.4 44.4.6-44.4.6-44.4.4-88.7.3-88.7-.3a7981 7981 0 01-88.8-1z" />
<path fill="none" d="M-565.2 324H-252v15.8h-313.2z" />
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

0
src/boot/.gitkeep Normal file
View File

39
src/boot/i18n.js Normal file
View File

@@ -0,0 +1,39 @@
import { defineBoot } from '#q-app/wrappers'
import { createI18n } from 'vue-i18n'
import messages from 'src/i18n'
let i18n = null
export default defineBoot(({ app }) => {
const getLocale = () => {
const saved = localStorage.getItem('lang')
if (saved && messages[saved]) return saved
const browserLang = navigator.language
if (messages[browserLang]) return browserLang
const shortLang = browserLang.split('-')[0]
return Object.keys(messages).find(lang => lang.startsWith(shortLang)) || 'en-US'
}
const locale = getLocale()
i18n = createI18n({
locale,
fallbackLocale: 'en-US',
legacy: false,
messages
})
localStorage.setItem('lang', locale)
document.documentElement.lang = locale
app.use(i18n)
})
export const setGlobalLocale = (newLocale) => {
if (i18n && i18n.global) {
i18n.global.locale.value = newLocale
localStorage.setItem('lang', newLocale)
document.documentElement.lang = newLocale
}
}

118
src/components/BaseLogo.vue Normal file
View File

@@ -0,0 +1,118 @@
<template>
<div class="flex row items-center no-wrap">
<svg
class="iconcolor q-mr-sm"
viewBox="0 0 8.4666662 8.4666662"
width="32"
height="32"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
>
<defs id="defs1" />
<g id="layer1">
<rect
style="fill:var(--icon-color);stroke-width:0.233149"
id="rect5"
width="6.9885192"
height="0.35581663"
x="3.114475"
y="0.86827624"
transform="matrix(0.77578367,0.63099897,-0.77578367,0.63099897,0,0)"
/>
<rect
style="fill:var(--icon-color);stroke-width:0.24961"
id="rect5-7"
width="7.4819207"
height="0.3809379"
x="-3.9267058"
y="5.7988153"
transform="matrix(-0.70756824,0.70664502,0.70756824,0.70664502,0,0)"
/>
<circle
style="fill:var(--icon-color);stroke-width:0.134869"
id="path5-8"
cx="1.5875"
cy="6.8791666"
r="1.0583333"
/>
<circle
style="fill:var(--icon-color);stroke-width:0.168586"
id="path5-8-5"
cx="7.1437502"
cy="7.1437502"
r="1.3229166"
/>
<circle
style="fill:var(--icon-color);stroke-width:0.118011"
id="path5-8-5-1"
cx="1.4552083"
cy="2.5135417"
r="0.92604166"
/>
<circle
style="fill:var(--icon-color);stroke-width:0.101152"
id="path5-8-5-1-7"
cx="7.1437502"
cy="1.3229166"
r="0.79374999"
/>
<circle
style="stroke-width:0.23602"
id="path5"
cx="3.96875"
cy="4.4979167"
r="1.8520833"
/>
</g>
</svg>
<span class="text-h4 text-bold q-pr-xs" style="color: var(--logo-color-bg-white);">
tg
</span>
<span class="text-h4 text-brand text-bold q-pa-0">
Projects
</span>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss">
body {
background: white;
margin: 0rem;
min-height: 100vh;
font-family: Futura, sans-serif;
}
#canvas {
position: absolute;
display: block;
top: 0;
left: 0;
z-index: -1;
}
.iconcolor {
--icon-color: var(--logo-color-bg-white);
}
@keyframes blink {
100%,
0% {
fill: $light-green-14;
}
60% {
fill: $green-14;
}
}
#path5 {
animation: blink 3s infinite;
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<slide-template title="FAQ__title">
<div
class="q-pa-md w100 flex justify-center"
style="max-width: 800px"
>
<q-list separator>
<q-expansion-item
group="FAQgroup"
v-for="item in 5"
:key="item"
switch-toggle-side
header-class="text-h5"
expand-icon="mdi-plus"
expanded-icon="mdi-close"
>
<template #header="{ expanded }">
<div :class="expanded ? 'text-brand' : ''" class="w100">
{{ $t('faq__question_' + item) }}
</div>
</template>
<q-card>
<q-card-section class="text-h6">
{{ $t('faq__answer_' + item) }}
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</div>
</slide-template>
</template>
<script setup lang="ts">
import SlideTemplate from 'components/SlideTemplate.vue';
</script>
<style scoped>
.custom-expansion.q-expansion-item--collapsed :deep(.q-item__section--side .q-icon) {
transition: transform 0.3s;
}
.custom-expansion.q-expansion-item--expanded :deep(.q-item__section--side .q-icon) {
transform: rotate(180deg);
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<div class="q-pa-md w100 flex justify-between">
<div class="flex column q-mx-lg justify-center">
<div
v-for="item in Docs"
:key="item.id"
>
<a
:href="item.href"
target="_blank"
style="text-decoration: none; color: inherit"
class="text-h6"
>
{{ $t(item.name) }}
</a>
</div>
</div>
<div class="flex column q-mx-lg">
<div class="text-h6 bold">
{{ $t('footer__contacts_ip') }}
</div>
<div class="text-caption">
{{ $t('footer__contacts_ip_detail') }}
</div>
<div class="flex items-center">
<q-icon name="mdi-map-marker-outline" color="brand" class="q-pr-sm"/>
<span>{{ $t('footer__contacts_location') }}</span>
</div>
<div class="flex items-center">
<q-icon name="mdi-phone-outline" color="brand" class="q-pr-sm" />
<span>+7 (926) 339-04-25</span>
</div>
<div class="flex items-center">
<q-icon name="mdi-email-outline" color="brand" class="q-pr-sm"/>
<a
href="mailto:a-mart@ya.ru"
style="text-decoration: none; color: inherit"
>
a-mart@ya.ru
</a>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const Docs = [
{ id: 1, name: 'footer__doc_terms_of_use', href: '' },
{ id: 2, name: 'footer__doc_privacy_policy', href: '' }
]
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div
class="flex w100 q-pa-lg text-white relative-position"
:class="!isAlignTop ? 'justify-center' : 'justify-around vert-height'"
>
<svg v-if ="isAlignTop" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none">
<polygon fill="white" points="0,0 55,0 45,101 0,101"/>
</svg>
<mesh-background v-if ="isAlignTop" style="z-index: -2"/>
<div
ref="slogan"
class="flex items-center"
>
<div
class="flex column justify-center q-pa-lg q-ma-lg q-gutter-y-md no-wrap bg-transperant"
style="max-width: 400px; "
:style="!isAlignTop ? 'text-align : center' : ''"
>
<div class="text-h5 text-grey">
{{ $t('banner__slogan_prepend') }} &mdash;
</div>
<div class="text-h4 text-brand">
{{ $t('banner__slogan_body') }}
<span class="text-no-wrap">
<q-icon dense name="telegram" style="color: #27a7e7"/>
<span style="color: #27a7e7">Telegram</span>
</span>
</div>
<div>
<q-btn
size="lg"
color="brand"
class="q-mt-xl"
>
<div class="flex items-center no-wrap">
<div>{{ $t('banner__main_btn')}}</div>
<q-icon name="keyboard_arrow_right"/>
</div>
</q-btn>
</div>
</div>
</div>
<div
ref="image"
class="text-red flex"
>
<img
src="/img/1.png"
class="q-ma-lg"
style="object-fit: scale-down;"
/>
</div>
<q-resize-observer @resize="checkAlign"/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import meshBackground from 'components/meshBackground.vue'
const slogan = ref(null)
const image = ref(null)
const isAlignTop = ref(false)
const checkAlign = () => {
if (slogan.value && image.value)
isAlignTop.value = (slogan.value.offsetTop === image.value.offsetTop)
}
</script>
<style scoped>
.vert-height {
min-height: calc(100vh * 0.75);
}
.fix-align:first-child {
text-align: center;
}
svg {
position: absolute;
bottom: 0;
width: 100%;
height: 100%;
z-index:-1;
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<slide-template title="how_it_works__title">
<q-resize-observer @resize="updateBlock"/>
<div
v-if="typeComponent !== 'tablet'"
class="flex row q-col-gutter-lg q-pa-md justify-center"
>
<div
:class="typeComponent === 'wide' ? 'col' : 'col-12'"
v-for="item in 4"
:key="item"
style="align-items: stretch;"
>
<q-card
class="q-pa-md fit"
>
<div class="flex column justify-between fit no-wrap">
<div class="flex column w100">
<span class="text-uppercase text-grey text-h6">
{{ $t('how_works__step' + item) }}
</span>
<span class="text-h6">
{{ $t('how_works__step' + item + '_description') }}
</span>
<div class="text-grey">
{{ $t(item !== 4 ? 'how_works__step_admin': 'how_works__step_user') }}
</div>
</div>
<div
class="flex column items-center justify-end q-mt-md text-grey col-grow"
>
<img
:src="'/img/' + item +'.png'"
style="object-fit: contain; max-width: 100%;"
/>
</div>
</div>
</q-card>
</div>
</div>
<div
v-if="typeComponent === 'tablet'"
class="flex row w100 q-pa-none no-wrap items-center no-scroll"
>
<div class="col-8">
<q-list>
<q-item
v-for="item in 4"
:key="item"
clickable
v-ripple
@click="activeItem=item"
:active="activeItem===item"
active-class="primary"
>
<q-item-section avatar>
<q-avatar color="brand" text-color="white">
<span v-if="item !== 4">{{item}}</span>
<q-icon v-else name="mdi-check"/>
</q-avatar>
</q-item-section>
<q-item-section>
<span class="text-h6">
{{ $t('how_works__step' + item + '_description') }}
</span>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col-4">
<q-tab-panels v-model="activeItem" animated>
<q-tab-panel
v-for="item in 4"
:key="item"
:name="item"
>
<div class="flex column items-center">
<div class="text-grey">
{{ $t(item !== 4 ? 'how_works__step_admin': 'how_works__step_user') }}
</div>
<img
:src="'/img/' + item +'.png'"
style="object-fit: contain; max-width: 100%;"
/>
</div>
</q-tab-panel>
</q-tab-panels>
</div>
</div>
</slide-template>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import SlideTemplate from 'components/SlideTemplate.vue'
const typeComponent = ref('wide')
const updateBlock = ({ width }) => {
typeComponent.value = width >= 1000
? 'wide'
: width < 600
? 'mobile'
: 'tablet'
}
const activeItem = ref(1)
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,78 @@
<template>
<slide-template title="price__title">
<q-card
class="flex column justify-center q-my-md items-center q-gutter-y-md"
style="max-width: 400px"
>
<div class="flex column items-center">
<div class="flex text-h6">
<telegram-star color="gold" size="24px"/>
<span class="q-ml-sm" style="text-decoration: line-through;">2</span>&nbsp;
<span class="text-bold text-red">0</span>&nbsp;
<span>- {{ $t('price__chat_per_day') }}</span>
</div>
<div class="flex items-center">
<q-badge color="red" class="q-mr-sm">100% OFF</q-badge>
<span>{{ $t('price__sale_date') }}</span>
</div>
</div>
<q-card
flat
class="bg-grey-3"
style="border-radius: 12px;"
>
<q-item>
<q-item-section avatar>
<telegram-star color="gold" size="48px"/>
</q-item-section>
<q-item-section>
<q-item-label class="text-grey">
{{ $t('price__stars_pay') }}
</q-item-label>
<q-item-label class="text-h6">
Telegram Stars
</q-item-label>
<q-item-label class="text-grey">
{{ $t('price__stars_description') }}
</q-item-label>
</q-item-section>
</q-item>
</q-card>
<q-list class="q-my-none q-pa-md">
<q-item
v-for="item in priceItems"
:key="item.id"
dense
>
<q-item-section avatar>
<q-avatar text-color="brand">
<q-icon v-if="item.icon" :name="item.icon" size="md"/>
<span v-else class="text-bold">{{ item.text }}</span>
</q-avatar>
</q-item-section>
<q-item-section>
{{ $t(item.label)}}
</q-item-section>
</q-item>
</q-list>
</q-card>
</slide-template>
</template>
<script setup lang="ts">
import telegramStar from 'components/TelegramStar.vue'
import SlideTemplate from 'components/SlideTemplate.vue'
const priceItems = [
{ id: 1, icon: 'mdi-all-inclusive', label: 'price_unlimited_users' },
{ id: 2, icon: 'mdi-all-inclusive', label: 'price_unlimited_projects' },
{ id: 3, text: '5', label: 'price_free_chats' },
{ id: 4, icon: 'mdi-lifebuoy', label: 'price_support' }
]
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,89 @@
<template>
<slide-template title="problem__title">
<div ref="container" class="w100">
<q-resize-observer @resize="updateWidth" />
<div
v-for="(row, rowIndex) in rows"
:key="rowIndex"
class="row q-py-none"
:class="rowClass(row.length)"
>
<div
v-for="(item, itemIndex) in row"
:key="itemIndex"
class="flex-item"
:style="itemStyle(row.length)"
>
<problem-section-item
:icon="item.icon"
:title="item.title"
:description="item.description"
style="overflow: hidden; min-width: 200px; max-width: 100%"
/>
</div>
</div>
</div>
</slide-template>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import ProblemSectionItem from 'components/ProblemSectionItem.vue'
import SlideTemplate from 'components/SlideTemplate.vue'
const problems = [
{ id: 1, icon: 'mdi-account-group-outline', title: 'problem__address_book', description: 'problem__address_book_description' },
{ id: 2, icon: 'mdi-clipboard-outline', title: 'problem__task_manager', description: 'problem__task_manager_description' },
{ id: 3, icon: 'mdi-calendar-month', title: 'problem__meeting', description: 'problem__meeting_description' },
{ id: 4, icon: 'mdi-folder-open-outline', title: 'problem__files', description: 'problem__files_description' },
{ id: 5, icon: 'mdi-lock-outline', title: 'problem__privacy', description: 'problem__privacy_description' }
]
const baseWidth = 250
const containerWidth = ref(0)
const updateWidth = ({ width }) => {
containerWidth.value = width
}
const maxPerRow = computed(() => {
return Math.max(1, Math.floor(containerWidth.value / baseWidth))
})
const rows = computed(() => {
const total = problems.length
const maxRow = maxPerRow.value
if (maxRow >= total) return [problems]
const rowCount = Math.ceil(total / maxRow)
const baseItems = Math.floor(total / rowCount)
const extra = total % rowCount
const result = []
let start = 0
for (let i = 0; i < rowCount; i++) {
const take = baseItems + (i < extra ? 1 : 0)
result.push(problems.slice(start, start + take))
start += take
}
return result
})
const rowClass = (count) => {
return count === 1 ? 'justify-center' : 'justify-between'
}
const itemStyle = (count) => {
return {
flex: `0 0 ${100 / count}%`,
maxWidth: `${100 / count}%`
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,31 @@
<template>
<div class="flex column items-center q-pa-md q-ma-md">
<div>
<q-avatar
color="brand"
text-color="white"
:icon
font-size="65px"
size="100px"
class="q-my-md"
/>
</div>
<div class="text-bold text-h4">
{{ $t(title) }}
</div>
<div class="text-h6 text-grey-8" style="max-width: 250px; text-align: center;">
{{ $t(description) }}
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
icon: String,
title: String,
description: String
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div class="flex column no-wrap">
<div class="flex w100 justify-center text-h4 q-mt-md text-bold q-pa-md text-grey">
{{ $t(title) }}
</div>
<div class="flex w100 justify-center q-pb-md column items-center">
<slot/>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
title: String
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,25 @@
<template>
<svg
class="q-ma-none q-pa-none"
:style="{
fill: color ?? 'red',
height: size ?? '42px',
width: 'auto'
}"
width="14" height="15" viewBox="0 0 14 15" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z"></path></svg>
</template>
<script setup lang="ts">
defineProps({
color: String,
size: String,
})
</script>
<style scoped>
.telegram-star-wrapper :deep(.telegram-star svg) {
fill: red;
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<div
id="background-canvas-wrapper"
class="flex fit column"
style="background-color: #00c853; opacity:0.65"
>
<canvas id="canvas" class="fit"/>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
const canvasBody = document.getElementById("canvas")
const drawArea = canvasBody.getContext("2d")
const opts = {
particleColor: "rgb(255,255,255)",
lineColor: "rgb(200,200,200)",
particleAmount: 50,
defaultSpeed: 0.1,
variantSpeed: 1,
defaultRadius: 3,
variantRadius: 2,
linkRadius: 200
}
const delay = 200
let tid
const rgb = opts.lineColor.match(/\d+/g)
let w
let h
const particles = []
function resizeReset () {
w = canvasBody.width = window.innerWidth
h = canvasBody.height = window.innerHeight
}
function deBouncer () {
clearTimeout(tid)
tid = setTimeout(function() {
resizeReset()
}, delay)
}
function checkDistance (x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
function setup () {
resizeReset()
for (let i = 0; i < opts.particleAmount; i++){
particles.push(new Particle())
}
window.requestAnimationFrame(loop)
}
function loop() {
window.requestAnimationFrame(loop)
drawArea.clearRect(0, 0, w, h)
for (let i = 0; i < particles.length; i++){
particles[i].update()
particles[i].draw()
}
for (let i = 0; i < particles.length; i++){
linkPoints(particles[i], particles)
}
}
function linkPoints (point1, hubs){
for (let i = 0; i < hubs.length; i++) {
const distance = checkDistance(point1.x, point1.y, hubs[i].x, hubs[i].y)
const opacity = 1 - distance / opts.linkRadius
if (opacity > 0) {
drawArea.lineWidth = 0.5
drawArea.strokeStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${opacity})`
drawArea.beginPath()
drawArea.moveTo(point1.x, point1.y)
drawArea.lineTo(hubs[i].x, hubs[i].y)
drawArea.closePath()
drawArea.stroke()
}
}
}
function Particle () {
this.x = Math.random() * w
this.y = Math.random() * h
this.speed = opts.defaultSpeed + Math.random() * opts.variantSpeed
this.directionAngle = Math.floor(Math.random() * 360)
this.color = opts.particleColor
this.radius = opts.defaultRadius + Math.random() * opts. variantRadius
this.vector = {
x: Math.cos(this.directionAngle) * this.speed,
y: Math.sin(this.directionAngle) * this.speed
};
this.update = function(){
this.border();
this.x += this.vector.x
this.y += this.vector.y
};
this.border = function(){
if (this.x >= w || this.x <= 0) {
this.vector.x *= -1;
}
if (this.y >= h || this.y <= 0) {
this.vector.y *= -1;
}
if (this.x > w) this.x = w
if (this.y > h) this.y = h
if (this.x < 0) this.x = 0
if (this.y < 0) this.y = 0
}
this.draw = function(){
drawArea.beginPath()
drawArea.arc(this.x, this.y, this.radius, 0, Math.PI*2)
drawArea.closePath()
drawArea.fillStyle = this.color
drawArea.fill()
}
}
window.addEventListener("resize", function(){ deBouncer() })
resizeReset()
setup()
})
</script>
<style>
#background-canvas-wrapper {
position: absolute !important;
display: block;
top: 0;
left: 0;
z-index: -1;
margin: 0;
min-height: 100%;
}
</style>

23
src/css/app.scss Normal file
View File

@@ -0,0 +1,23 @@
.text-brand {
color: $green-14 !important;
}
.bg-brand {
background: $green-14 !important;
}
$base-width: 100;
@while $base-width > 0 {
.w#{$base-width} { width: #{$base-width}+'%'; }
$base-width: $base-width - 10;
}
:root {
--body-width: 1200px;
--logo-color-bg-white: grey;
}
.main-content {
max-width: var(--body-width) !important;
margin: 0 auto;
}

View File

@@ -0,0 +1,25 @@
// Quasar SCSS (& Sass) Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
// Check documentation for full list of Quasar variables
// Your own variables (that are declared here) and Quasar's own
// ones will be available out of the box in your .vue/.scss/.sass files
// It's highly recommended to change the default colors
// to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary : #1976D2;
$secondary : #26A69A;
$accent : #9C27B0;
$dark : #1D1D1D;
$dark-page : #121212;
$positive : #21BA45;
$negative : #C10015;
$info : #31CCEC;
$warning : #F2C037;

1
src/i18n/en-US/index.js Normal file
View File

@@ -0,0 +1 @@
export default { EN: 'EN', RU: 'RU', '': '', main__how_it_works: 'How it works?', main__price: 'Price', main__faq: 'FAQ', main__contacts: 'Contacts', banner__slogan_prepend: 'banner__slogan_prepend', banner__slogan_body: 'banner__slogan_body', banner__main_btn: 'Join!', problem__title: 'problem__title', problem__address_book: 'problem__address_book', problem__address_book_description: 'problem__address_book_description', problem__task_manager: 'problem__task_manager', problem__task_manager_description: 'problem__task_manager_description', problem__meeting: 'problem__meeting', problem__meeting_description: 'problem__meeting_description', problem__files: 'problem__files', problem__files_description: 'problem__files_description', problem__privacy: 'problem__privacy', problem__privacy_description: 'problem__privacy_description', how_it_works__title: 'how_it_works__title', how_works__step1: 'Step 1', how_works__step1_description: 'how_works__step1_description', how_works__step2: 'Step 2', how_works__step2_description: 'how_works__step2_description', how_works__step3: 'Step 3 (optional)', how_works__step3_description: 'how_works__step3_description', how_works__step4: 'Done!', how_works__step4_description: 'how_works__step3_description', how_works__step_user: 'User', how_works__step_admin: 'Administrator', price__title: 'Price', price__chat_per_day: 'price__chat_per_day', price__sale_date: 'price__sale_date', price__stars_pay: 'price__stars_pay', price__stars_description: 'price__stars_description', price_unlimited_users: 'Unlimited users', price_unlimited_projects: 'Unlimited projects', price_free_chats: 'price_free_chats', price_support: 'Support', FAQ__title: 'FAQ', faq__question_1: 'faq__question_1', faq__answer_1: 'faq__answer_1', faq__question_2: 'faq__question_2', faq__answer_2: 'faq__answer_2', faq__question_3: 'faq__question_3', faq__answer_3: 'faq__answer_3', faq__question_4: 'faq__question_4', faq__answer_4: 'faq__answer_4', faq__question_5: 'faq__question_5', faq__answer_5: 'faq__answer_5', footer__doc_terms_of_use: 'Term of use', footer__doc_privacy_policy: 'Privacy Policy', footer__contacts_ip: 'Individual Entrepreneur/Sole Proprietor Martyshkin Alexey Alexandrovich', footer__contacts_ip_detail: 'PSRNSP 318774600262084, ITN 366316608346', footer__contacts_location: 'Russia, Moscow/Voronezh' }

1
src/i18n/en-US/index.ts Normal file
View File

@@ -0,0 +1 @@
export default { EN: 'EN', RU: 'RU', '': '', main__how_it_works: 'How it works?', main__price: 'Price', main__faq: 'FAQ', main__contacts: 'Contacts', banner__slogan_prepend: 'banner__slogan_prepend', banner__slogan_body: 'banner__slogan_body', banner__main_btn: 'Join!', problem__title: 'problem__title', problem__address_book: 'problem__address_book', problem__address_book_description: 'problem__address_book_description', problem__task_manager: 'problem__task_manager', problem__task_manager_description: 'problem__task_manager_description', problem__meeting: 'problem__meeting', problem__meeting_description: 'problem__meeting_description', problem__files: 'problem__files', problem__files_description: 'problem__files_description', problem__privacy: 'problem__privacy', problem__privacy_description: 'problem__privacy_description', how_it_works__title: 'how_it_works__title', how_works__step1: 'how_works__step1', how_works__step1_description: 'how_works__step1_description', how_works__step2: 'how_works__step2', how_works__step2_description: 'how_works__step2_description', how_works__step3: 'how_works__step3', how_works__step3_description: 'how_works__step3_description', price__title: 'price__title', price__chat_per_day: 'price__chat_per_day', price__sale_date: 'price__sale_date', price__stars_pay: 'price__stars_pay', price__stars_description: 'price__stars_description', price__include: 'price__include', price_unlimited_users: 'price_unlimited_users', price_unlimited_projects: 'price_unlimited_projects', price_free_chats: 'price_free_chats', price_support: 'price_support', FAQ__title: 'FAQ__title', faq__question_1: 'faq__question_1', faq__answer_1: 'faq__answer_1', faq__question_2: 'faq__question_2', faq__answer_2: 'faq__answer_2', faq__question_3: 'faq__question_3', faq__answer_3: 'faq__answer_3', faq__question_4: 'faq__question_4', faq__answer_4: 'faq__answer_4', faq__question_5: 'faq__question_5', faq__answer_5: 'faq__answer_5' }

7
src/i18n/index.js Normal file
View File

@@ -0,0 +1,7 @@
import enUS from './en-US'
import ruRU from './ru-RU'
export default {
'en-US': enUS,
'ru-RU': ruRU
}

1
src/i18n/ru-RU/index.js Normal file

File diff suppressed because one or more lines are too long

1
src/i18n/ru-RU/index.ts Normal file
View File

@@ -0,0 +1 @@
export default { EN: 'EN', RU: 'RU', '': '', main__how_it_works: 'Как это работает?', main__price: 'Цены', main__faq: 'Вопросы', main__contacts: 'Контакты', banner__slogan_prepend: 'Больше чем просто чаты', banner__slogan_body: 'Управление проектами внутри Telegram', banner__main_btn: 'Присоединиться', problem__title: 'Преимущества', problem__address_book: 'Адресная книга', problem__address_book_description: 'Не нужно угадывать, кто есть кто.', problem__task_manager: 'Задачи', problem__task_manager_description: 'Задачи из разных чатов в одном месте - все будет сделано!', problem__meeting: 'Совещания', problem__meeting_description: 'problem__meeting_description', problem__files: 'Файлы', problem__files_description: 'Не нужно помнить в каком именно чате был нужный файл. Теперь они все в одном месте.', problem__privacy: 'Доступ к информации', problem__privacy_description: 'Пользователям доступна только та информация, что есть в их чатах.', how_it_works__title: 'Как это работает?', how_works__step1: 'how_works__step1', how_works__step1_description: 'how_works__step1_description', how_works__step2: 'how_works__step2', how_works__step2_description: 'how_works__step2_description', how_works__step3: 'how_works__step3', how_works__step3_description: 'how_works__step3_description', price__title: 'price__title', price__chat_per_day: 'price__chat_per_day', price__sale_date: 'price__sale_date', price__stars_pay: 'price__stars_pay', price__stars_description: 'price__stars_description', price__include: 'price__include', price_unlimited_users: 'price_unlimited_users', price_unlimited_projects: 'price_unlimited_projects', price_free_chats: 'price_free_chats', price_support: 'price_support', FAQ__title: 'FAQ__title', faq__question_1: 'faq__question_1', faq__answer_1: 'faq__answer_1', faq__question_2: 'faq__question_2', faq__answer_2: 'faq__answer_2', faq__question_3: 'faq__question_3', faq__answer_3: 'faq__answer_3', faq__question_4: 'faq__question_4', faq__answer_4: 'faq__answer_4', faq__question_5: 'faq__question_5', faq__answer_5: 'faq__answer_5' }

185
src/layouts/MainLayout.vue Normal file
View File

@@ -0,0 +1,185 @@
<template>
<q-layout view="lHr lpr lFr" class="bg-transparent">
<q-header class="main-content text-grey glass">
<div ref="headerContainer" class="flex q-ma-md justify-between no-wrap items-center">
<base-logo ref="logo"/>
<div
ref="menuContainer"
class="row items-center q-ml-md no-wrap"
style="min-width: 42px"
>
<div
ref="buttonsContainer"
:class="{ 'invisible absolute': !showFullMenu }"
class="flex row no-wrap"
>
<q-btn
v-for="item in menuItems"
:key="item.id"
flat
no-caps
@click="scrollToElement(item.ref)"
ref="menuButtons"
>
<span class="text-no-wrap">{{ $t(item.title) }}</span>
</q-btn>
</div>
<q-btn
v-if="!showFullMenu"
flat
round
icon="menu"
@click="showDrawer = !showDrawer"
/>
<q-btn outline color="primary" class="q-ml-sm">
<div class="flex items-center no-wrap">
<span class="text-bold">{{ locale.split('-')[0] }}</span>
</div>
<q-menu>
<q-list style="min-width: 100px">
<q-item
v-for="lang in langNames"
:key="lang"
clickable
v-close-popup
@click="setLocale(lang)"
>
<q-item-section>
<q-item-label :class="isCurrentLang(lang.locale) ? 'text-primary' : ''">{{ lang.label }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>
<q-resize-observer @resize="checkSpace"/>
</div>
</q-header>
<q-drawer v-model="showDrawer" side="right" overlay>
<div class="flex column items-end q-gutter-y-md q-pa-md">
<q-btn
flat
round
icon="mdi-close"
@click="showDrawer = !showDrawer"
/>
<q-btn
v-for="item in menuItems"
:key="item.id"
flat
no-caps
@click="scrollToElement(item.ref)"
>
<span class="text-no-wrap text-h6">{{ $t(item.title) }}</span>
</q-btn>
</div>
</q-drawer>
<q-page-container
class="main-content q-pa-none q-ma-none bg-transparent"
>
<q-page class="column">
<hero-banner/>
<problem-section/>
<how-works-section id='how_works'/>
<price-section id='price'/>
<faq-section id='FAQ'/>
<footer-section id='contacts' class="bg-grey-14 text-white"/>
</q-page>
</q-page-container>
</q-layout>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import baseLogo from 'components/BaseLogo.vue'
import heroBanner from 'components/HeroBanner.vue'
import problemSection from 'components/ProblemSection.vue'
import HowWorksSection from 'components/HowWorksSection.vue'
import PriceSection from 'components/PriceSection.vue'
import FaqSection from 'components/FAQSection.vue'
import FooterSection from 'components/FooterSection.vue'
const showDrawer = ref(false)
const menuItems = [
{ id: 0, title: 'main__how_it_works', ref: 'how_works' },
{ id: 1, title: 'main__price', ref: 'price' },
{ id: 2, title: 'main__faq', ref: 'FAQ' },
{ id: 3, title: 'main__contacts', ref: 'contacts' }
]
const showFullMenu = ref(true)
const menuButtons = ref([])
const headerContainer = ref(null)
const logo = ref(null)
const menuContainer = ref(null)
const calculateButtonsWidth = () => {
return menuButtons.value.reduce(
(total, btn) => total + (btn?.$el.offsetWidth || 0), 0
)
}
const checkSpace = () => {
if (!headerContainer.value || !logo.value || !menuContainer.value) return
const headerWidth = headerContainer.value.offsetWidth
const logoWidth = logo.value.$el.offsetWidth
const menuMargin = parseFloat(getComputedStyle(menuContainer.value).marginLeft) || 0
const availableWidth = headerWidth - logoWidth - menuMargin - 40 // 40px - запас
const buttonsWidth = calculateButtonsWidth()
showFullMenu.value = buttonsWidth <= availableWidth
}
onMounted(async () => {
await nextTick()
checkSpace()
})
import { scroll } from 'quasar'
const { getScrollTarget, setVerticalScrollPosition } = scroll
const scrollToElement = (id) => {
const el = document.querySelector('#' + id)
const target = getScrollTarget(el)
const offset = el.offsetTop - 12
const duration = 300
setVerticalScrollPosition(target, offset, duration)
showDrawer.value = false
}
import { useI18n } from 'vue-i18n'
import { setGlobalLocale } from 'src/boot/i18n'
const { locale } = useI18n({ useScope: 'global' })
const langNames = [
{ locale: 'en-US', label: 'English'},
{ locale: 'ru-RU', label: 'Русский'}
]
const isCurrentLang = (lang) => locale.value === lang
const setLocale = (newLocale) => {
setGlobalLocale(newLocale.locale)
}
</script>
<style scoped>
.invisible {
opacity: 0;
pointer-events: none;
}
.glass {
background-color: rgba(255, 255, 255, 0.8) !important;
backdrop-filter: blur(8px);
}
</style>

View File

@@ -0,0 +1,27 @@
<template>
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">
404
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</template>
<script setup>
//
</script>

13
src/pages/IndexPage.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<q-page class="flex flex-center">
<img
alt="Quasar logo"
src="~assets/quasar-logo-vertical.svg"
style="width: 200px; height: 200px"
>
</q-page>
</template>
<script setup>
//
</script>

30
src/router/index.js Normal file
View File

@@ -0,0 +1,30 @@
import { defineRouter } from '#q-app/wrappers'
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import routes from './routes'
/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Router instance.
*/
export default defineRouter(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE)
})
return Router
})

18
src/router/routes.js Normal file
View File

@@ -0,0 +1,18 @@
const routes = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/IndexPage.vue') }
]
},
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue')
}
]
export default routes