Compare commits

...

114 Commits

Author SHA1 Message Date
Javier Segarra 929623949b perf: change code position(clean code) 2024-09-09 22:19:05 +02:00
Javier Segarra 38a88bb0cc feat: changes 2024-09-09 22:18:44 +02:00
William Buezas c41d1430f7 Add menu translations 2024-09-07 19:34:22 -03:00
William Buezas 2bff9304e8 Add title view translation 2024-09-06 11:31:07 -03:00
William Buezas c53658e6e0 Add empty list 2024-09-06 11:20:09 -03:00
William Buezas e94f8c6b23 Add missed code 2024-09-06 10:53:28 -03:00
William Buezas bfbe3621d6 Resolve conflicts 2024-09-04 07:17:22 -03:00
Javier Segarra 05568280f3 Merge pull request 'Pedidos stepper' (!80) from wbuezas/hedera-web-mindshore:feature/PedidosStepper into 4922-vueMigration
Reviewed-on: #80
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
2024-09-03 14:06:01 +00:00
William Buezas 6bfa5b9715 Small changes 2024-09-03 08:17:22 -03:00
William Buezas 6631be401b Add minimal to QDate when is mobile 2024-09-02 18:27:14 -03:00
William Buezas ba2ded5c48 Add contracted prop for narrow windows 2024-09-02 17:53:43 -03:00
William Buezas 723a977ecd Small fix 2024-09-02 16:29:18 -03:00
William Buezas a1d67ebc6f update branch 2024-09-02 16:27:54 -03:00
William Buezas 7026e3416f Remove fetching of default order method 2024-09-02 12:39:03 -03:00
William Buezas 975495113d Set monday as first day of week 2024-09-01 21:43:49 -03:00
William Buezas 8e0f09cc0f Add next and back buttons custom labels 2024-09-01 21:42:09 -03:00
William Buezas 95e23c05fa Translate back and next buttons labels 2024-09-01 21:36:13 -03:00
William Buezas 6423ecfb05 Fix agency step title 2024-09-01 21:33:37 -03:00
William Buezas 3a21292030 Add validation to address step in method PICKUP 2024-09-01 21:29:57 -03:00
William Buezas f2bd3c2fa6 Add title to address step 2024-09-01 21:27:12 -03:00
William Buezas 020e0afc96 Add locale to QDate and initiate localeDates in store 2024-09-01 21:15:54 -03:00
William Buezas 766417bb73 Basket 2024-09-01 20:58:05 -03:00
William Buezas c77c2e6648 Update branch 2024-08-30 18:06:37 -03:00
William Buezas 6fe518601f WIP 2024-08-30 18:03:49 -03:00
William Buezas cb2c9871cc Stepper 2024-08-28 16:12:34 -03:00
Javier Segarra a832a1889a Merge pull request 'Modulo Administración' (!78) from wbuezas/hedera-web-mindshore:feature/Administracion into 4922-vueMigration
Reviewed-on: #78
2024-08-23 19:29:45 +00:00
William Buezas 7c4123ca0b Resolve conflicts 2024-08-23 16:23:14 -03:00
William Buezas facbe9b990 WIP 2024-08-23 16:09:50 -03:00
William Buezas 57880705d0 WIP 2024-08-23 13:43:32 -03:00
Javier Segarra ed29a1939c Merge pull request 'Mejoras sección pedidos' (!79) from wbuezas/hedera-web-mindshore:feature/MejorasPedidos into 4922-vueMigration
Reviewed-on: #79
2024-08-23 12:05:15 +00:00
Javier Segarra 5456db8add perf: date proposal 2024-08-22 23:32:47 +02:00
Javier Segarra 053b9f8457 fix: comments 2024-08-22 23:28:10 +02:00
Javier Segarra 8866331926 feat: extra-form slot for other table 2024-08-22 22:52:30 +02:00
William Buezas 33ef1da2a9 Small changes 2024-08-22 12:35:35 -03:00
Javier Segarra 2e2c83dcde perf: remove console.log 2024-08-21 13:45:43 +02:00
William Buezas 73eb3dcbee Updating VnInput 2024-08-19 08:44:40 -03:00
William Buezas 05d735702e Formatting and small changes 2024-08-19 08:21:23 -03:00
Javier Segarra 462a8a3cf8 eprf: add max value 2024-08-19 12:38:45 +02:00
Javier Segarra f821b8689a perf: improve interceptor 2024-08-19 12:38:33 +02:00
Javier Segarra e47edb9827 fix: eslint warnings 2024-08-19 12:38:17 +02:00
William Buezas 25f4f822b4 Extra improvements 2024-08-18 21:58:02 -03:00
William Buezas ef35914f34 Card list change 2024-08-18 21:39:44 -03:00
William Buezas b19bf710e8 Resolve conflicts 2024-08-18 20:33:17 -03:00
William Buezas 17a519e2ee Small changes 2024-08-18 20:10:57 -03:00
William Buezas 95a2bfb69c Add error interceptor 2024-08-17 20:25:55 -03:00
William Buezas 6e41548fdf Create jApi error interceptor 2024-08-17 20:23:15 -03:00
William Buezas c86c1cc0c0 General improvements 2024-08-17 19:59:35 -03:00
William Buezas 87c151c057 Add news details change 2024-08-17 19:36:19 -03:00
William Buezas 13af1d03a3 Change eslint config 2024-08-17 00:06:19 -03:00
William Buezas 14bef2383f News 2024-08-16 23:11:56 -03:00
Javier Segarra 5053a908f7 Merge pull request 'Vistas sección pedidos' (!77) from wbuezas/hedera-web-mindshore:feature/Pedidos into 4922-vueMigration
Reviewed-on: #77
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
2024-08-16 06:52:22 +00:00
William Buezas 7e0c591026 Add tooltips 2024-08-14 12:22:51 -03:00
William Buezas 594b17b4ab Fix teleport submit problem 2024-08-14 12:16:46 -03:00
William Buezas 887ee8aea4 Create date format util with translations 2024-08-14 11:28:15 -03:00
William Buezas 44627dbc8a Replace prompt with VnConfirm 2024-08-14 10:56:07 -03:00
William Buezas f2c8b90324 Create print service and fix slot in VnTable 2024-08-14 09:31:42 -03:00
William Buezas e0f55f8ca3 Change class casing 2024-08-14 09:09:52 -03:00
William Buezas f36eb1bd88 Create VnTable and use it 2024-08-14 09:08:46 -03:00
William Buezas b728ecaf29 Add vn date 2024-08-14 08:54:15 -03:00
William Buezas 7c96106faa Change serial column name to invoice 2024-08-14 08:47:24 -03:00
William Buezas a0fc1cfc07 use line.discount directly 2024-08-14 08:39:51 -03:00
William Buezas b66c47955c Move script tag to the start of the file 2024-08-14 08:37:04 -03:00
William Buezas ec14ca334a WIP 2024-08-14 08:30:53 -03:00
William Buezas 7837925be9 Photos view 2024-08-13 16:37:28 -03:00
William Buezas 2fb892c71a WIP 2024-08-12 11:41:06 -03:00
William Buezas 2a1cd59492 Visits view 2024-08-08 11:24:43 -03:00
William Buezas 76b99ed293 Admin section WIP 2024-08-07 17:34:00 -03:00
William Buezas 67c6f84de3 Several changes 2024-08-02 21:56:20 -03:00
William Buezas 745e9a569c Translation files, pending orders, and more changes 2024-07-28 18:45:38 -03:00
William Buezas ad2d494481 Create CardList component 2024-07-27 22:55:40 -03:00
Javier Segarra 24687e57e6 Merge pull request 'Account config and change password form' (!73) from wbuezas/hedera-web-mindshore:feature/AccountConfig into 4922-vueMigration
Reviewed-on: #73
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
2024-07-26 20:24:41 +00:00
Javier Segarra c20f48b2bf Merge branch '4922-vueMigration' into feature/AccountConfig 2024-07-26 22:23:21 +02:00
Javier Segarra 6bad41db20 feat: add password visibility 2024-07-26 22:18:17 +02:00
Javier Segarra eb0328753a Merge pull request 'Agencies packages' (!74) from wbuezas/hedera-web-mindshore:feature/Agencies into 4922-vueMigration
Reviewed-on: #74
2024-07-26 20:05:39 +00:00
Javier Segarra e067f5f7bd feat: langs button 2024-07-26 22:05:01 +02:00
Javier Segarra 34a0d93ece fix: email i18n 2024-07-26 21:55:42 +02:00
William Buezas 4256f45373 Add verificationToken as a prop to let the view handle it 2024-07-26 10:36:48 -03:00
William Buezas 93cc0d4286 Add login when password changed 2024-07-26 09:25:46 -03:00
William Buezas ef36566442 Create change password with and without token and add related features 2024-07-26 09:09:21 -03:00
William Buezas 06cd9b01d3 Change password form fields validation 2024-07-26 08:54:11 -03:00
William Buezas 7f831ae3a5 Remove unused style tag 2024-07-26 08:41:19 -03:00
William Buezas 382378e867 Agencies packages 2024-07-25 11:39:25 -03:00
William Buezas 401487dfd3 Resolve conflicts 2024-07-24 14:52:54 -03:00
William Buezas aa4ccf65f5 Change password form and several changes 2024-07-24 14:42:02 -03:00
Javier Segarra fb267b910b Merge pull request 'Address details and VnForm' (!72) from wbuezas/hedera-web-mindshore:feature/AddressDetails into 4922-vueMigration
Reviewed-on: #72
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
2024-07-24 14:19:39 +00:00
Javier Segarra 24a9c130d1 On AddressDetails: Merge branch 'feature/AddressDetails' of https://gitea.verdnatura.es/wbuezas/hedera-web-mindshore into feature/AddressDetails 2024-07-23 23:07:40 +02:00
Javier Segarra f59b37c722 Merge branch 'feature/AddressDetails' of https://gitea.verdnatura.es/wbuezas/hedera-web-mindshore into feature/AddressDetails 2024-07-23 22:39:45 +02:00
Javier Segarra 160552ff2f fix: hover AddressListCardActions 2024-07-23 22:37:22 +02:00
William Buezas 83e3e034a8 Show Addresses list actions always 2024-07-23 16:37:24 -03:00
William Buezas 61062c1418 Add app.provide api 2024-07-23 11:26:12 -03:00
William Buezas 2cbbaf619c small change 2024-07-23 11:02:39 -03:00
William Buezas ec0d783672 WIP 2024-07-23 10:58:35 -03:00
William Buezas 07c5f64265 improvements 2024-07-22 13:51:54 -03:00
William Buezas dcbc154caa Components creation: AddressDetails, VnForm, VnInput and VnSelect 2024-07-22 11:17:56 -03:00
Javier Segarra 0d3da684b4 Merge pull request 'Address List view' (!71) from wbuezas/hedera-web-mindshore:feature/AddressList into 4922-vueMigration
Reviewed-on: #71
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
2024-07-19 17:58:11 +00:00
William Buezas 8d2f041c46 Small change 2024-07-19 09:24:20 -03:00
William Buezas 28b2dd386f Address List view 2024-07-19 09:19:26 -03:00
Javier Segarra d589b89a62 Merge pull request 'Home view adjustments' (!70) from wbuezas/hedera-web-mindshore:feature/HomeViewAdjustments into 4922-vueMigration
Reviewed-on: #70
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
2024-07-19 12:17:13 +00:00
Javier Segarra 04660bd05e feat: VnImg 2024-07-19 13:58:50 +02:00
Javier Segarra 1d6ec00c78 Merge branch '4922-vueMigration' into feature/HomeViewAdjustments 2024-07-19 11:16:07 +00:00
Javier Segarra ce557dc5b9 Merge pull request 'Init config' (!68) from wbuezas/hedera-web-mindshore:feature/InitConfig into 4922-vueMigration
Reviewed-on: #68
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
2024-07-19 11:13:55 +00:00
Javier Segarra 4387a868bc perf: update 2024-07-19 09:36:25 +02:00
William Buezas 003f42dd03 Home view adjustments 2024-07-18 08:51:11 -03:00
William Buezas 9dfeb2f7ef package fix 2024-07-17 15:45:56 -03:00
William Buezas 8bea750244 Fix build 2024-07-17 15:10:27 -03:00
William Buezas bf2094163d More linting and formatting 2024-07-17 09:23:30 -03:00
William Buezas e0cc4e40ba Change components auto import casing type 2024-07-17 09:23:20 -03:00
William Buezas 47c6fe02ec Config prettier and eslint for src folder 2024-07-17 09:22:54 -03:00
Juan Ferrer 6458d8db5e #4922 Catalog & fixes 2023-01-16 08:32:48 +01:00
Juan Ferrer 0234e14c6b #4922 invoices & orders 2022-12-13 18:29:04 +01:00
Juan Ferrer 7e26aa773c refs #4922 password recovery, app store, error handler, fixes 2022-12-09 11:28:38 +01:00
Juan Ferrer 0d0be4ee5f refs #4922 Login, logout, home, layout style 2022-12-06 11:41:41 +01:00
Juan Ferrer 042b8b0309 refs #4922 Login UI 2022-11-30 18:59:07 +01:00
Juan Ferrer b7658b76cf refs #4922 Quasar added 2022-11-29 20:32:57 +01:00
122 changed files with 37409 additions and 17549 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

8
.eslintignore Normal file
View File

@ -0,0 +1,8 @@
/dist
/src-bex/www
/src-capacitor
/src-cordova
/.quasar
/node_modules
.eslintrc.js
babel.config.js

85
.eslintrc.js Normal file
View File

@ -0,0 +1,85 @@
module.exports = {
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
// This option interrupts the configuration hierarchy at this file
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
root: true,
parserOptions: {
parser: '@babel/eslint-parser',
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module' // Allows for the use of imports
},
env: {
browser: true,
'vue/setup-compiler-macros': true
},
extends: ['standard'],
plugins: ['vue', 'prettier'],
globals: {
ga: 'readonly', // Google Analytics
cordova: 'readonly',
__statics: 'readonly',
__QUASAR_SSR__: 'readonly',
__QUASAR_SSR_SERVER__: 'readonly',
__QUASAR_SSR_CLIENT__: 'readonly',
__QUASAR_SSR_PWA__: 'readonly',
process: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly'
},
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow paren-less arrow functions
'arrow-parens': 'off',
'one-var': 'off',
'no-void': 'off',
'multiline-ternary': 'off',
'import/first': 'off',
'import/named': 'error',
'import/namespace': 'error',
'import/default': 'error',
'import/export': 'error',
'import/extensions': 'off',
'import/no-unresolved': 'off',
'import/no-extraneous-dependencies': 'off',
'prefer-promise-reject-errors': 'off',
semi: 'off',
// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
overrides: [
{
files: ['src/**/*.{js,vue,scss}', 'quasar.config.js'], // Aplica ESLint solo a archivos .js, .vue y .scss dentro de src (Proyecto de quasar)
extends: [
// Base ESLint recommended rules
'eslint:recommended',
// Uncomment any of the lines below to choose desired strictness,
// but leave only one uncommented!
// See https://eslint.vuejs.org/rules/#available-rules
// 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
// https://github.com/prettier/eslint-config-prettier#installation
// usage with Prettier, provided by 'eslint-config-prettier'.
'prettier'
],
rules: {
semi: 'off',
'space-before-function-paren': 'off',
'prefer-promise-reject-errors': 'off',
'vue/no-multiple-template-root': 'off'
},
parserOptions: {
ecmaVersion: '2021'
},
plugins: ['vue']
}
]
};

View File

@ -1,15 +0,0 @@
extends: eslint:recommended
parserOptions:
ecmaVersion: 2017
sourceType: module
rules:
no-undef: 0
no-redeclare: 0
no-mixed-spaces-and-tabs: 0
no-console: 0
no-cond-assign: 0
no-unexpected-multiline: 0
brace-style: [error, 1tbs]
space-before-function-paren: [error, never]
padded-blocks: [error, never]
func-call-spacing: [error, never]

36
.gitignore vendored
View File

@ -1,4 +1,36 @@
node_modules
build/ build/
config.my.php config.my.php
.vscode/
.DS_Store
.thumbs.db
node_modules
# Quasar core related directories
.quasar
/dist
# 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
# BEX related directories and files
/src-bex/www
/src-bex/js/core
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln

3
.npmrc Normal file
View File

@ -0,0 +1,3 @@
# pnpm-related options
shamefully-hoist=true
strict-peer-dependencies=false

9
.postcssrc.js Normal file
View File

@ -0,0 +1,9 @@
/* eslint-disable */
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
plugins: [
// to edit target browsers: use "browserslist" field in package.json
require('autoprefixer')
]
}

9
.prettierrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
printWidth: 80,
tabWidth: 4,
useTabs: false,
singleQuote: true,
bracketSpacing: true,
arrowParens: 'avoid',
trailingComma: 'none'
};

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

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

22
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9000
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9000
}
]
}

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

@ -0,0 +1,9 @@
{
"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"],
"cSpell.words": ["axios", "composables"]
}

100
Jenkinsfile vendored
View File

@ -1,34 +1,25 @@
#!/usr/bin/env groovy #!/usr/bin/env groovy
def BRANCH_ENV = [
test: 'test',
master: 'production'
]
def remote = [:]
node {
stage('Setup') {
env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev'
echo "NODE_NAME: ${env.NODE_NAME}"
echo "WORKSPACE: ${env.WORKSPACE}"
}
}
pipeline { pipeline {
agent any agent any
environment { environment {
PROJECT_NAME = 'hedera-web' PROJECT_NAME = 'hedera-web'
STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}"
} }
stages { stages {
stage('Checkout') {
steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
switch (env.BRANCH_NAME) {
case 'master':
env.NODE_ENV = 'production'
env.MAIN_REPLICAS = 3
env.CRON_REPLICAS = 1
break
case 'test':
env.NODE_ENV = 'test'
env.MAIN_REPLICAS = 1
env.CRON_REPLICAS = 0
break
}
}
setEnv()
}
}
stage('Debuild') { stage('Debuild') {
when { when {
anyOf { anyOf {
@ -38,31 +29,28 @@ pipeline {
} }
agent { agent {
docker { docker {
image 'registry.verdnatura.es/debuild:2.21.3-vn2' image 'registry.verdnatura.es/verdnatura/debuild:2.23.4-vn7'
registryUrl 'https://registry.verdnatura.es/' registryUrl 'https://registry.verdnatura.es/'
registryCredentialsId 'docker-registry' registryCredentialsId 'docker-registry'
args '-v /mnt/appdata/reprepro:/reprepro'
} }
} }
steps { steps {
sh 'debuild -us -uc -b' sh 'debuild -us -uc -b'
sh 'vn-includedeb stretch' sh 'mkdir -p debuild'
} sh 'mv ../hedera-web_* debuild'
}
stage('Container') { script {
when { def files = findFiles(glob: 'debuild/*.changes')
anyOf { files.each { file -> env.CHANGES_FILE = file.name }
branch 'master' }
branch 'test'
configFileProvider([
configFile(fileId: "dput.cf", variable: 'DPUT_CONFIG')
]) {
sshagent(credentials: ['jenkins-agent']) {
sh 'dput --config "$DPUT_CONFIG" verdnatura "debuild/$CHANGES_FILE"'
}
} }
}
environment {
CREDS = credentials('docker-registry')
}
steps {
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY'
sh 'docker-compose build --build-arg BUILD_ID=$BUILD_ID --parallel'
sh 'docker-compose push'
} }
} }
stage('Deploy') { stage('Deploy') {
@ -73,15 +61,41 @@ pipeline {
} }
} }
environment { environment {
DOCKER_HOST = "${env.SWARM_HOST}" CREDS = credentials('docker-registry')
IMAGE = "$REGISTRY/verdnatura/hedera-web"
} }
steps { steps {
sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}" script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}"
env.TAG = "${packageJson.version}-build${env.BUILD_ID}"
}
sh 'docker-compose build --build-arg BUILD_ID=$BUILD_ID --parallel'
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY'
sh 'docker push $IMAGE:$TAG'
script {
if (env.BRANCH_NAME == 'master') {
sh 'docker tag $IMAGE:$TAG $IMAGE:latest'
sh 'docker push $IMAGE:latest'
}
}
withKubeConfig([
serverUrl: "$KUBERNETES_API",
credentialsId: 'kubernetes',
namespace: 'salix'
]) {
sh 'kubectl set image deployment/hedera-web-$BRANCH_NAME hedera-web-$BRANCH_NAME=$IMAGE:$TAG'
sh 'kubectl set image deployment/hedera-web-cron-$BRANCH_NAME hedera-web-cron-$BRANCH_NAME=$IMAGE:$TAG'
}
} }
} }
} }
post { post {
unsuccessful { unsuccessful {
setEnv()
sendEmail() sendEmail()
} }
} }

View File

@ -5,17 +5,26 @@ Hedera is the main web page for Verdnatura.
## Getting Started ## Getting Started
Required dependencies. Required dependencies.
* PHP >= 7.0
* Node.js >= 8.0 - PHP >= 7.0
- Node.js >= 8.0
Launch application for development. Launch application for development.
``` ```
$ npm run dev $ npm run dev
``` ```
Launch project backend.
```
$ php -S 127.0.0.1:3002 -t . index.php
Run server side method from command line. Run server side method from command line.
``` ```
$ php hedera-web.php -m method_path
$ php hedera-web.php -m method_path
``` ```
## Built with ## Built with
@ -23,3 +32,4 @@ $ php hedera-web.php -m method_path
* [Webpack](https://webpack.js.org/) * [Webpack](https://webpack.js.org/)
* [MooTools](https://mootools.net/) * [MooTools](https://mootools.net/)
* [TinyMCE](https://www.tinymce.com/) * [TinyMCE](https://www.tinymce.com/)
```

14
babel.config.js Normal file
View File

@ -0,0 +1,14 @@
/* eslint-disable */
module.exports = api => {
return {
presets: [
[
'@quasar/babel-preset-app',
api.caller(caller => caller && caller.target === 'node')
? { targets: { node: 'current' } }
: {}
]
]
}
}

View File

@ -51,4 +51,4 @@
} }
.new-text li { .new-text li {
margin: 4px 0; margin: 4px 0;
} }

View File

@ -1,123 +1,130 @@
const Result = require('./result');
var Result = require('./result');
/** /**
* This class stores the database results. * This class stores the database results.
*/ */
module.exports = new Class({ module.exports = new Class({
results: null results: null,
,error: null error: null,
/** /**
* Initilizes the resultset object. * Initilizes the resultset object.
*/ */
,initialize(results, error) { initialize(results, error) {
this.results = results; this.results = results;
this.error = error; this.error = error;
} },
/**
* Gets the query error.
*
* @return {Db.Err} the error or null if no errors hapened
*/
,getError() {
return this.error;
}
,fetch() {
if (this.error)
throw this.error;
if (this.results !== null
&& this.results.length > 0)
return this.results.shift();
return null; /**
} * Gets the query error.
*
/** * @return {Db.Err} the error or null if no errors hapened
* Fetchs the next result from the resultset. */
* getError() {
* @return {Db.Result} the result or %null if error or there are no more results return this.error;
*/ },
,fetchResult() {
var result = this.fetch();
if (result !== null) {
if (result.data instanceof Array)
return new Result(result);
else
return true;
}
return null; fetch() {
} if (this.error) {
throw this.error;
}
console.log('this.results', this.results);
if (this.results !== null && this.results.length > 0) {
return this.results.shift();
}
/** return null;
* Fetchs the first row object from the next resultset. },
*
* @return {Array} the row if success, %null otherwise
*/
,fetchObject() {
var result = this.fetch();
if (result !== null /**
&& result.data instanceof Array * Fetchs the next result from the resultset.
&& result.data.length > 0) *
return result.data[0]; * @return {Db.Result} the result or %null if error or there are no more results
*/
fetchResult() {
const result = this.fetch();
console.log('test result', result);
if (result !== null) {
if (result.data instanceof Array) {
return new Result(result);
} else {
return true;
}
}
return null; return null;
} },
/** /**
* Fetchs data from the next resultset. * Fetchs the first row object from the next resultset.
* *
* @return {Array} the data * @return {Array} the row if success, %null otherwise
*/ */
,fetchData() { fetchObject() {
var result = this.fetch(); const result = this.fetch();
if (result !== null if (
&& result.data instanceof Array) result !== null &&
return result.data; result.data instanceof Array &&
result.data.length > 0
) {
return result.data[0];
}
return null; return null;
} },
/** /**
* Fetchs the first row and column value from the next resultset. * Fetchs data from the next resultset.
* *
* @return {Object} the value if success, %null otherwise * @return {Array} the data
*/ */
,fetchValue() { fetchData() {
var row = this.fetchRow(); const result = this.fetch();
if (row instanceof Array && row.length > 0) if (result !== null && result.data instanceof Array) {
return row[0]; return result.data;
}
return null; return null;
} },
/** /**
* Fetchs the first row from the next resultset. * Fetchs the first row and column value from the next resultset.
* *
* @return {Array} the row if success, %null otherwise * @return {Object} the value if success, %null otherwise
*/ */
,fetchRow() { fetchValue() {
var result = this.fetch(); const row = this.fetchRow();
if (result !== null if (row instanceof Array && row.length > 0) {
&& result.data instanceof Array return row[0];
&& result.data.length > 0) { }
var object = result.data[0];
var row = new Array(result.columns.length); return null;
for(var i = 0; i < row.length; i++) },
row[i] = object[result.columns[i].name];
return row; /**
} * Fetchs the first row from the next resultset.
*
return null; * @return {Array} the row if success, %null otherwise
} */
fetchRow() {
const result = this.fetch();
if (
result !== null &&
result.data instanceof Array &&
result.data.length > 0
) {
const object = result.data[0];
const row = new Array(result.columns.length);
for (let i = 0; i < row.length; i++) {
row[i] = object[result.columns[i].name];
}
return row;
}
return null;
}
}); });

View File

@ -7,7 +7,7 @@
</div> </div>
<form id="form"> <form id="form">
<div class="form-group"> <div class="form-group">
<input <input
placeholder="_User" placeholder="_User"
type="text" type="text"
id="user" id="user"
@ -45,7 +45,7 @@
</form> </form>
<div class="footer"> <div class="footer">
<p> <p>
<t>Yet you are not a customer?</t> <t>Yet you are not a customer?</t>
<a href="//verdnatura.es/register/" target="_blank"><t>Sign up</t></a> <a href="//verdnatura.es/register/" target="_blank"><t>Sign up</t></a>
</p> </p>
<p class="contact"> <p class="contact">

39
jsconfig.json Normal file
View File

@ -0,0 +1,39 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src/*": [
"src/*"
],
"app/*": [
"*"
],
"components/*": [
"src/components/*"
],
"layouts/*": [
"src/layouts/*"
],
"pages/*": [
"src/pages/*"
],
"assets/*": [
"src/assets/*"
],
"boot/*": [
"src/boot/*"
],
"stores/*": [
"src/stores/*"
],
"vue$": [
"node_modules/vue/dist/vue.runtime.esm-bundler.js"
]
}
},
"exclude": [
"dist",
".quasar",
"node_modules"
]
}

43194
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +1,90 @@
{ {
"name": "hedera-web", "name": "hedera-web",
"version": "22.48.2", "version": "22.48.2",
"description": "Verdnatura web page", "description": "Verdnatura web page",
"license": "GPL-3.0", "license": "GPL-3.0",
"repository": { "productName": "Salix",
"type": "git", "author": "Verdnatura",
"url": "https://git.verdnatura.es/hedera-web" "repository": {
}, "type": "git",
"devDependencies": { "url": "https://git.verdnatura.es/hedera-web"
"@babel/preset-env": "^7.20.2", },
"archiver": "^5.3.1", "devDependencies": {
"assets-webpack-plugin": "^7.1.1", "@babel/eslint-parser": "^7.13.14",
"babel-loader": "^9.1.0", "@babel/preset-env": "^7.20.2",
"bundle-loader": "^0.5.6", "@intlify/vue-i18n-loader": "^4.2.0",
"css-loader": "^6.7.2", "@quasar/app-webpack": "^3.0.0",
"eslint": "^8.15.0", "archiver": "^5.3.1",
"file-loader": "^6.2.0", "assets-webpack-plugin": "^7.1.1",
"fs-extra": "^10.1.0", "babel-loader": "^9.1.0",
"glob": "^8.0.3", "bundle-loader": "^0.5.6",
"html-webpack-plugin": "^5.5.0", "css-loader": "^5.2.7",
"json-loader": "^0.5.7", "eslint": "^8.57.0",
"mini-css-extract-plugin": "^2.7.0", "eslint-config-prettier": "^9.1.0",
"node-sass": "^7.0.1", "eslint-config-standard": "^17.0.0",
"raw-loader": "^4.0.2", "eslint-plugin-import": "^2.19.1",
"sass-loader": "^12.6.0", "eslint-plugin-n": "^15.0.0",
"style-loader": "^3.3.1", "eslint-plugin-prettier": "^5.1.3",
"url-loader": "^4.1.1", "eslint-plugin-promise": "^6.0.0",
"webpack": "^5.75.0", "eslint-plugin-vue": "^9.27.0",
"webpack-cli": "^4.10.0", "eslint-webpack-plugin": "^3.1.1",
"webpack-dev-server": "^4.11.1", "file-loader": "^6.2.0",
"webpack-merge": "^5.8.0", "fs-extra": "^10.1.0",
"yaml-loader": "^0.5.0" "glob": "^8.0.3",
}, "html-webpack-plugin": "^5.5.0",
"dependencies": { "json-loader": "^0.5.7",
"js-yaml": "^3.12.1", "mini-css-extract-plugin": "^2.7.0",
"mootools": "^1.5.2", "node-sass": "^7.0.1",
"promise-polyfill": "^8.2.3", "postcss": "^8.4.39",
"require-yaml": "0.0.1", "postcss-import": "^13.0.0",
"tinymce": "^6.3.0" "postcss-loader": "^4.3.0",
}, "postcss-url": "^10.1.3",
"scripts": { "raw-loader": "^4.0.2",
"front": "webpack serve --open", "sass-loader": "^12.6.0",
"back": "cd ../salix && gulp backOnly", "style-loader": "^3.3.1",
"db": "cd ../vn-database && myvc run", "url-loader": "^4.1.1",
"build": "rm -rf build/ ; webpack", "webpack": "^5.75.0",
"clean": "rm -rf build/" "webpack-cli": "^4.10.0",
} "webpack-dev-server": "^4.11.1",
"webpack-merge": "^5.8.0",
"yaml-loader": "^0.5.0"
},
"dependencies": {
"@quasar/extras": "^1.0.0",
"axios": "^0.21.1",
"core-js": "^3.6.5",
"js-yaml": "^3.12.1",
"mootools": "^1.5.2",
"pinia": "^2.0.11",
"promise-polyfill": "^8.2.3",
"quasar": "^2.6.0",
"require-yaml": "0.0.1",
"tinymce": "^6.3.0",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-router": "^4.0.0"
},
"scripts": {
"front": "webpack serve --open",
"back": "cd ../vn-database && myvc start && cd ../salix && gulp backOnly",
"build": "rm -rf build/ ; webpack",
"clean": "rm -rf build/",
"lint": "eslint --ext .js,.vue ./"
},
"browserslist": [
"last 10 Chrome versions",
"last 10 Firefox versions",
"last 4 Edge versions",
"last 7 Safari versions",
"last 8 Android versions",
"last 8 ChromeAndroid versions",
"last 8 FirefoxAndroid versions",
"last 10 iOS versions",
"last 5 Opera versions"
],
"engines": {
"node": ">= 12.22.1",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
}
} }

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
xml:space="preserve"
sodipodi:docname="accessory.svg"
width="40"
height="40"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs9" /><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.595"
inkscape:cx="100"
inkscape:cy="99.860918"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<style
type="text/css"
id="style2">
.st0{fill:#1E1E1C;}
.st1{fill:none;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
</style>
<path
class="st0"
d="M 37.313228,1.0829325 H 2.714188 C 1.233722,1.0829325 0,2.2892385 0,3.7971205 v 5.099383 c 0,1.4804655 1.206306,2.7141875 2.714188,2.7141875 h 2.083619 v 0.08225 L 7.978067,33.8451 c 0.411241,2.933516 2.63194,5.071968 5.236464,5.071968 h 13.543522 c 2.63194,0 4.825223,-2.138452 5.236464,-5.071968 l 3.207676,-22.234407 h 2.083619 C 38.766278,11.610693 40,10.404387 40,8.8965055 v -5.126801 c 0,-1.480466 -1.206306,-2.686772 -2.686772,-2.686772 z m -4.386566,10.6374225 -3.152844,21.79575 c -0.274161,1.809459 -1.535298,3.125429 -3.015765,3.125429 H 13.241947 c -1.480467,0 -2.76902,-1.31597 -3.015765,-3.125429 L 7.073338,11.583275 h 25.908156 z m 4.825223,-2.8238515 c 0,0.246744 -0.191912,0.466073 -0.466073,0.466073 H 2.714188 c -0.246745,0 -0.466073,-0.191913 -0.466073,-0.466073 v -5.126799 c 0,-0.246745 0.191912,-0.466073 0.466073,-0.466073 h 34.59904 c 0.246745,0 0.466073,0.191912 0.466073,0.466073 v 5.126799 z"
id="path4"
style="stroke-width:0.27416;fill:#1a1a1a;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
xml:space="preserve"
sodipodi:docname="artificial.svg"
width="40"
height="40"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs13" /><sodipodi:namedview
id="namedview11"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.595"
inkscape:cx="100"
inkscape:cy="99.860918"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<style
type="text/css"
id="style2">
.st0{fill:#1E1E1C;}
.st1{fill:none;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
</style>
<g
id="g8"
transform="matrix(0.26820887,0,0,0.26820887,-6.5074179,-6.820887)"
style="fill:#1a1a1a;fill-opacity:1">
<path
class="st0"
d="m 113.7,83.3 c -4.1,0 -8.2,0 -12.4,0 -5.2,0 -10.4,0 -15.6,0 h -2 c -1.5,0 -1.8,0.2 -1.8,1.8 v 30.2 c 0,0.6 0,0.9 0.3,1.2 0.3,0.3 0.6,0.3 1.3,0.3 h 30.2 c 1.5,0 1.7,-0.3 1.7,-1.7 0,-10 0,-20.1 0,-30.1 0.1,-1.5 -0.2,-1.7 -1.7,-1.7 z m -6.4,17.5 v 7.6 h -0.6 c -1.3,0 -2.6,0 -3.9,0 h -1.7 -4.5 c -1.9,0 -3.9,0 -5.8,0 H 90.4 L 90.3,108 v 0 c 0,-5.5 0,-10.8 0,-16.2 v -0.4 h 0.5 c 6.4,0 11.4,0 16,0 h 0.3 l 0.2,0.3 c 0,0 0,0.1 0,0.2 0,3.1 0,6 0,8.9 z"
id="path4"
style="fill:#1a1a1a;fill-opacity:1" />
<path
class="st0"
d="m 145.9,158.2 25.8,-44.7 c 0.3,-0.5 0.6,-1 0.9,-1.6 l 0.8,-1.4 L 155.2,100 173.4,89.5 173,88.7 c -0.1,-0.3 -0.3,-0.5 -0.4,-0.8 L 146.3,42.1 c -0.4,-0.8 -0.8,-1.1 -1.1,-1.2 -0.3,-0.1 -0.8,0.1 -1.5,0.5 l -4.6,2.6 c -3.8,2.2 -7.5,4.3 -11.3,6.5 L 127.1,51 V 31.7 c 0,-0.7 0,-1.1 -0.3,-1.4 C 126.5,30 126.2,30 125.4,30 H 72.5 c -0.1,0 -0.3,0 -0.5,0 -0.5,0 -0.8,0.1 -1.1,0.3 -0.2,0.2 -0.3,0.6 -0.3,1 0,0.2 0,0.4 0,0.6 v 0.4 3.6 c 0,4.8 0,9.6 0,14.4 V 51 L 52.9,40.8 52.4,40.7 52.1,41.1 c -0.1,0.1 -0.2,0.2 -0.3,0.3 L 25.1,87.5 c -1,1.7 -0.9,2 0.8,3 L 42.3,100 32,105.8 c -2.3,1.3 -4.5,2.6 -6.8,3.9 -0.4,0.2 -0.8,0.5 -0.9,1 -0.1,0.3 0,0.7 0.3,1.1 0.1,0.2 0.2,0.3 0.3,0.5 l 25.9,45 c 0.3,0.6 0.7,1.2 1.2,1.8 l 0.3,0.3 18.1,-10.4 v 2.5 c 0,1.6 0,3.1 0,4.6 l -0.1,12.1 c 0,1.5 0.2,1.7 1.6,1.7 h 2.5 c 1.6,0 3.1,0.1 4.7,0.1 h 45.1 c 2.4,0 2.6,-0.2 2.6,-2.7 v -17.4 -0.8 l 0.6,0.3 c 2.8,1.6 5.6,3.2 8.3,4.8 l 1.9,1.1 c 2.1,1.2 4.2,2.4 6.3,3.6 0.2,0.1 0.7,0.4 1.1,0.2 0.4,0 0.6,-0.2 0.9,-0.9 z m -3.8,-10 -0.3,-0.2 -22.7,-13.1 -0.2,0.5 c -0.3,0.6 -0.3,1.1 -0.2,1.6 v 0.3 c 0,6.4 0,12.8 0,19.3 v 4.1 c 0,0.2 0,0.4 0,0.7 v 0.4 h -40 v -0.7 c 0,-1.8 0,-3.6 0,-5.3 l 0.1,-20.9 -0.7,0.3 c -1.9,0.8 -3.5,1.8 -5.2,2.8 -0.9,0.5 -1.8,1 -2.6,1.5 -1.6,0.9 -3.2,1.8 -5,2.9 l -10,5.8 -19.9,-34.6 14.6,-8.4 9,-5.2 -6.5,-3.7 -17.2,-10 0.3,-0.6 19.2,-33.1 c 0.1,-0.2 0.2,-0.3 0.3,-0.5 l 0.2,-0.3 0.5,0.2 c 1.3,0.8 2.7,1.6 4,2.4 l 17.4,10.1 c 0.3,0.2 0.7,0.4 1.2,0.1 l 0.3,-0.2 c 0.2,-0.3 0.2,-0.7 0.2,-1.2 0,-0.9 0,-1.7 0,-2.5 0,-0.8 0,-1.5 0,-2.3 L 79,38.2 h 0.6 c 2.1,0 4.2,0 6.2,0.1 h 0.3 c 2.3,0.1 4.2,0.1 5.9,0.1 5.8,0 11.4,0 18.1,0 h 7.9 0.1 c 0.1,0 0.2,0 0.3,0 h 0.3 v 27.1 l 23.6,-13.5 19.9,34.6 -23.5,13.5 23.5,13.6 -0.2,0.3 z"
id="path6"
style="fill:#1a1a1a;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
xml:space="preserve"
sodipodi:docname="flower.svg"
width="40"
height="40"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs13"><linearGradient
inkscape:collect="always"
id="linearGradient981"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop977" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop979" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient981"
id="linearGradient983"
x1="34.739397"
y1="99.599534"
x2="165.73375"
y2="99.599534"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
id="namedview11"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="4.365"
inkscape:cx="102.29095"
inkscape:cy="63.459336"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<style
type="text/css"
id="style2">
.st0{fill:#1E1E1C;}
.st1{fill:none;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
</style>
<g
id="g8"
transform="matrix(0.28530481,0,0,0.28530481,-8.5979763,-8.4162261)"
style="stroke:none;stroke-opacity:1;fill:#1a1a1a;fill-opacity:1">
<path
class="st0"
d="m 100.1,76.9 h -0.5 c -6.1,0 -11.8,2.3 -16.1,6.5 -4.4,4.3 -6.8,10.1 -6.8,16.3 0,6.2 2.4,12 6.7,16.2 4.2,4.2 10,6.5 15.9,6.5 h 0.3 0.3 c 5.9,0 11.6,-2.4 15.9,-6.5 4.3,-4.2 6.7,-10 6.7,-16.2 0,-12.5 -9.9,-22.5 -22.4,-22.8 z M 89.2,89.1 c 2.7,-2.7 6.4,-4.2 10,-4.2 h 0.4 c 3.8,-0.1 7.6,1.4 10.5,4.2 2.9,2.8 4.5,6.6 4.5,10.8 0,8.2 -6.8,14.9 -15,14.9 h -0.1 c -8,0 -14.8,-6.8 -14.8,-14.9 -0.1,-4.1 1.5,-8 4.5,-10.8 z"
id="path4"
style="stroke:none;stroke-opacity:1;fill:#1a1a1a;fill-opacity:1" />
<path
class="st0"
d="m 157.2,102 -0.2,-0.2 0.2,-0.2 c 0.1,-0.1 0.2,-0.2 0.3,-0.3 6.1,-6.5 8.9,-14.4 8.1,-23.2 -0.7,-7.9 -4.1,-14.5 -10,-19.7 -5.1,-4.4 -10.9,-6.8 -17.3,-7.3 v 0 h -0.4 c -1.7,-0.1 -3.4,0 -5.2,0.1 -0.8,0.1 -1.5,0.1 -2.2,0.2 h -0.1 c -0.1,-0.1 -0.2,-0.4 -0.4,-1 -1.7,-5.4 -4.7,-10 -8.9,-13.6 -6.8,-5.8 -14.8,-8.2 -23.6,-7 -6.5,0.8 -12.3,3.7 -17.1,8.5 -3,3 -5.3,6.6 -6.8,10.7 L 73.4,49.5 73,49.3 C 68.6,48.2 64.2,48.2 59.9,49 53.3,50.4 47.8,53.5 43.6,58.4 37.1,66.1 34.8,75 36.9,85 c 1,4.6 3,8.7 6.1,12.4 L 43.3,97.8 43,98 c -2.7,2.8 -4.8,6 -6.2,9.6 v 0 c 0,0.1 -4.5,9.7 -0.2,21.5 0,0.1 0,0.1 0.1,0.2 0,0.1 0.1,0.2 0.1,0.4 0.4,1 0.8,2 1.3,2.9 0.4,0.9 0.8,1.5 1.1,2 3.3,5.4 8.2,9.4 14.4,11.8 5,2 10.4,2.5 16,1.6 l 0.3,-0.1 0.2,0.5 c 0.9,3.2 2.3,6.1 4.1,8.6 4.4,6.2 10.2,10.2 17.4,12 0.8,0.2 3.6,0.7 7.3,0.7 2.3,0 4.9,-0.2 7.6,-0.9 l 0.3,-0.1 c 0.3,-0.1 0.5,-0.1 0.7,-0.2 0.1,0 0.2,-0.1 0.4,-0.1 l 0.2,-0.1 c 4.2,-1.4 8.1,-3.7 11.5,-7 3.1,-3 5.5,-6.6 7,-10.7 0.1,-0.2 0.1,-0.3 0.2,-0.5 l 0.1,-0.2 h 0.2 c 0.1,0 0.3,0.1 0.5,0.1 3,0.7 5.8,0.9 8.6,0.8 4.3,-0.2 8.4,-1.3 12.1,-3.3 6,-3.2 10.5,-7.8 13.3,-13.9 2.9,-6.4 3.5,-13.1 1.8,-20.1 -1.3,-4.1 -3.3,-8.1 -6.2,-11.5 z M 149,98.3 c -0.8,0.6 -1.6,1.2 -2.3,1.7 l -1.7,1.2 0.4,0.5 c 0.7,0.9 1.6,1.6 2.3,2.2 0.4,0.3 0.8,0.6 1.1,1 4.6,4.2 7,9.2 7.4,15.3 0.3,5.9 -1.5,11.1 -5.3,15.4 -4.8,5.4 -10.8,7.9 -18,7.5 -2.6,-0.2 -5.2,-0.9 -8.1,-2.2 -0.6,-0.2 -1.1,-0.5 -1.7,-0.7 l -1.9,-0.8 -1.1,5.3 c -1.9,9.3 -9.8,16.4 -19.4,17.3 -0.7,0.1 -1.4,0.1 -2.1,0.1 -5,0 -9.9,-1.8 -13.9,-5.1 -3.9,-3.3 -6.6,-7.9 -7.5,-12.9 -0.2,-1.2 -0.4,-2.5 -0.5,-3.7 l -0.1,-0.9 c 0,-0.4 -0.1,-1 -0.5,-1.5 l -0.3,-0.3 -1.8,0.6 c -0.9,0.3 -1.8,0.6 -2.7,0.9 -8.9,3.1 -19,-0.1 -24.6,-7.8 -2.6,-3.6 -4,-7.6 -4.1,-11.8 -0.3,-7.8 2.9,-14.2 9.4,-19 L 55.4,98 55,97.7 c -0.7,-0.7 -1.5,-1.4 -2.2,-2 -0.9,-0.7 -1.7,-1.5 -2.5,-2.3 -4,-4.2 -6.1,-9.1 -6.2,-14.8 -0.1,-5.4 1.6,-10.2 5,-14.3 3.5,-4.2 8,-6.8 13.4,-7.7 4.2,-0.7 8.4,-0.1 12.6,1.8 0.8,0.4 1.6,0.7 2.6,1.1 0.6,0.2 0.9,0.4 1.3,0.2 0.4,-0.2 0.5,-0.6 0.6,-1.2 0.1,-0.6 0.2,-1.2 0.3,-1.8 v -0.1 c 0.2,-1.2 0.4,-2.3 0.8,-3.4 2.8,-8.5 8.6,-13.7 17.2,-15.3 7.4,-1.4 13.9,0.7 19.5,6.3 3.5,3.5 5.6,8 6.1,13.2 0.1,0.8 0.2,1.6 0.3,2.4 l 0.2,2 0.6,-0.1 c 1,-0.2 1.9,-0.5 2.7,-0.8 l 0.1,-0.1 c 0.3,-0.1 0.5,-0.2 0.8,-0.3 8,-2.8 15.3,-1.5 21.8,3.8 4.6,3.8 7.2,8.8 7.7,14.8 0.6,7.7 -2.4,14.2 -8.7,19.2 z"
id="path6"
style="stroke:none;stroke-opacity:1;fill:#1a1a1a;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
xml:space="preserve"
sodipodi:docname="fruit.svg"
width="40"
height="40"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs9" /><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.595"
inkscape:cx="100"
inkscape:cy="99.860918"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<style
type="text/css"
id="style2">
.st0{fill:#1E1E1C;}
.st1{fill:none;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
</style>
<path
class="st0"
d="m 4.6155643,32.24963 c 1.033877,0 2.041908,0.232623 2.998243,0.67202 0.18093,0.07754 0.387705,0.180928 0.568633,0.284316 l 0.103387,0.05169 c 1.9902137,0.982183 4.2388957,1.473275 6.4875777,1.473275 1.68005,0 3.3601,-0.284317 4.936762,-0.852949 h 0.02585 l 0.05169,0.02585 c 1.60251,0.568632 3.282559,0.852948 4.988456,0.852948 0.05169,0 0.103387,0 0.155081,0 h 0.103388 c 0.02585,0 0.02585,0 0.05169,0 0.02585,0 0.05169,0 0.07754,0 h 0.02585 c 3.980425,-0.103388 7.676536,-1.75759 10.468003,-4.600753 2.791467,-2.843161 4.316438,-6.616811 4.342284,-10.597237 0,-0.284317 -0.103387,-0.542786 -0.310162,-0.749561 -0.206776,-0.206776 -0.465245,-0.310163 -0.723717,-0.310163 h -9.434126 l -0.02585,-0.103388 c -0.28431,-2.50715 -1.188952,-4.910914 -2.662226,-6.952821 -0.232622,-0.336009 -0.516938,-0.697866 -0.852948,-1.059723 l -0.05169,-0.05169 0.05169,-0.07754 c 1.550815,-1.8351313 1.628356,-3.1274773 1.628356,-3.3859473 0,-0.542785 -0.413551,-0.982183 -0.982183,-1.008029 0,0 0,0 0,0 -0.516938,0 -1.00803,0.439397 -1.059723,0.956336 0,0 -0.07754,0.74956 -1.03388,1.938519 l -0.02585,0.02585 -0.07754,0.02585 -0.05169,-0.02585 C 21.493607,6.2993277 17.797496,5.0586757 13.997999,5.2654507 10.04342,5.4722267 6.4248503,7.2298167 3.7884643,10.176366 c -4.03211997,4.497365 -4.936762,10.933249 -2.274529,16.361101 l 0.05169,0.103388 c 0.103387,0.180931 0.206775,0.361857 0.310163,0.594479 0.568632,1.240652 0.775407,2.610541 0.594479,3.954581 -0.05169,0.310163 0.05169,0.646172 0.284316,0.878795 0.232622,0.232622 0.568632,0.33601 0.878795,0.284316 0.310163,-0.07754 0.646173,-0.103388 0.982183,-0.103388 z m 33.2391427,-11.682808 0.07754,0.07754 v 0.05169 c -0.232622,2.636385 -1.240652,5.117691 -2.920704,7.185445 l -0.02585,0.02585 h -0.103388 l -0.05169,-0.02585 -5.221077,-5.22108 c -0.387704,-0.387704 -1.033876,-0.439397 -1.42158,-0.103387 -0.232622,0.180928 -0.361859,0.465244 -0.361859,0.74956 0,0.284316 0.10339,0.568635 0.310162,0.77541 l 5.324467,5.324465 v 0.05169 0.05169 l -0.02585,0.02585 c -2.016059,1.757593 -4.497363,2.843163 -7.133751,3.127479 h -0.05169 l -0.07754,-0.05169 v -0.05169 -8.271015 c 0,-0.568632 -0.413551,-1.033877 -0.930489,-1.08557 -0.284316,-0.02585 -0.568632,0.07754 -0.775408,0.258469 -0.206775,0.206775 -0.33601,0.465244 -0.33601,0.74956 v 8.426097 l -0.07754,0.07754 h -0.05169 c -1.266499,-0.07754 -2.481304,-0.33601 -3.670265,-0.749561 -0.129232,-0.07754 -0.232622,-0.10339 -0.336009,-0.129237 -1.240652,-0.491089 -2.377916,-1.163111 -3.411793,-1.990212 l -0.02585,-0.02585 v -0.103382 l 0.02585,-0.05169 5.557089,-5.557086 c 0.387704,-0.387704 0.439398,-1.033879 0.103385,-1.421583 -0.180926,-0.232622 -0.465242,-0.361857 -0.749558,-0.361857 -0.284316,-0.02585 -0.568635,0.103388 -0.77541,0.310163 l -5.660475,5.660477 h -0.05169 -0.05169 l -0.02585,-0.02585 c -1.860989,-2.119449 -2.972406,-4.729988 -3.230875,-7.547301 v -0.05169 l 0.05169,-0.07754 h 0.05169 26.053699 z m -34.5056417,5.014302 -0.02585,-0.02585 C 1.0745333,20.90283 1.8499413,15.397433 5.3134283,11.546242 7.5879573,9.0132437 10.689589,7.5141217 14.101382,7.3331937 c 3.385947,-0.180928 6.668506,0.956336 9.201504,3.2308653 0.129234,0.103388 0.232622,0.206776 0.361857,0.33601 l 0.180931,0.180929 c 0.155079,0.155081 0.310163,0.310163 0.465242,0.491091 l 0.02585,0.02585 c 0.310163,0.33601 0.594479,0.723714 0.878795,1.085571 1.214805,1.68005 1.990212,3.670263 2.248681,5.738017 v 0.05169 l -0.05169,0.07754 h -0.05169 -16.774653 c -0.284316,0 -0.542785,0.103388 -0.7237137,0.310163 -0.206775,0.206776 -0.310163,0.465245 -0.310163,0.749562 0,5.221077 2.6363857,10.002759 7.0562097,12.845919 l 0.05169,0.02585 -0.02585,0.155081 -0.07754,0.02585 c -0.620326,0.07754 -1.214806,0.129231 -1.835132,0.129231 -1.912672,0 -3.77365,-0.413548 -5.5053937,-1.266498 h -0.02585 c -0.232619,-0.15508 -0.516934,-0.284314 -0.749557,-0.387702 -1.188958,-0.568632 -2.455457,-0.852948 -3.747804,-0.852948 h -0.103388 v -0.103388 c 0,-1.292348 -0.310163,-2.558847 -0.827101,-3.721958 -0.129235,-0.361857 -0.258469,-0.620326 -0.413551,-0.878795 z"
id="path4"
style="stroke-width:0.258469;fill:#1a1a1a;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
xml:space="preserve"
sodipodi:docname="greenery.svg"
width="40"
height="40"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs9" /><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.595"
inkscape:cx="100"
inkscape:cy="99.860918"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<style
type="text/css"
id="style2">
.st0{fill:#1E1E1C;}
.st1{fill:none;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
</style>
<path
class="st0"
d="M 34.045254,21.098493 C 33.702189,16.753004 30.814725,12.321749 25.46863,7.9190818 21.15173,4.3740778 16.405999,1.8582678 14.54773,0.94342876 l -0.02859,-0.05718 h -0.02859 l -0.02859,-0.02859 c -0.428827,-0.228707 -0.714715,-0.343062 -0.800481,-0.400239 L 12.575107,-2.4007925e-7 12.031921,1.0577838 C 5.4279214,14.065662 4.1700164,23.642891 8.2867954,29.503584 c 3.6307706,5.174562 10.2919476,5.946458 13.9227186,5.946458 0.571774,0 1.114961,-0.02859 1.600969,-0.05718 h 0.05718 l 0.02859,0.05718 c 0.886252,1.572381 1.743914,2.944641 2.601576,4.088191 0.200121,0.257299 0.486009,0.428831 0.829074,0.45742 0.343065,0.02859 0.68613,-0.08577 0.943428,-0.285888 l 0.02859,-0.02859 c 0.486009,-0.428832 0.543186,-1.14355 0.142944,-1.658148 -0.743308,-1.000606 -1.543792,-2.201333 -2.315689,-3.602181 l -0.05718,-0.08577 0.08577,-0.05718 c 5.631983,-4.031013 8.262147,-8.462268 7.890494,-13.179411 z M 22.466812,32.99141 c -0.05718,0 -0.114355,0 -0.200121,0 -7.433073,0 -10.69219,-3.058995 -11.950095,-4.888675 -3.1733496,-4.54561 -2.3156876,-12.493281 2.51581,-22.9567612 l 0.114355,-0.285888 0.08577,0.285888 c 1.686736,6.4610562 5.145974,18.6684502 9.520052,27.6739042 l 0.08577,0.142944 z m 2.601576,-0.943428 -0.114355,0.08577 -0.05718,-0.114355 C 20.608544,23.271238 17.092128,10.949489 15.376804,4.4026668 l -0.05718,-0.22871 0.22871,0.114355 c 2.630165,1.458026 6.461056,3.802303 9.720173,6.7469432 3.945247,3.516416 6.060814,6.975654 6.318113,10.291948 0.285887,3.659359 -1.915446,7.29013 -6.518234,10.720779 z"
id="path4"
style="stroke-width:0.285887;fill:#1a1a1a;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
xml:space="preserve"
sodipodi:docname="handmade.svg"
width="40"
height="40"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs13" /><sodipodi:namedview
id="namedview11"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.595"
inkscape:cx="100"
inkscape:cy="99.860918"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<style
type="text/css"
id="style2">
.st0{fill:#1E1E1C;}
.st1{fill:none;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
</style>
<g
id="g8"
transform="matrix(0.28579156,0,0,0.28579156,-8.7130268,-8.5559862)"
style="fill:#1a1a1a;fill-opacity:1">
<path
class="st0"
d="m 165,128.6 c 1.6,-6.6 2,-12.7 1.5,-18.7 -0.6,-5.6 -2,-10.4 -4.5,-14.8 -4.2,-7.5 -10.6,-12.8 -19,-15.5 -1,-0.3 -2,-0.6 -3.1,-0.8 l -1.8,-0.4 1.9,-0.3 c 1.4,-0.2 2.8,-0.5 4.1,-0.7 l 0.7,-0.1 c 4.4,-0.8 8.9,-1.6 13.3,-2.9 3.3,-1 6.2,-2.7 8.8,-4.5 l 0.8,-0.6 -0.7,-0.7 c -0.6,-0.6 -1.3,-1.3 -1.9,-1.9 -2,-2 -4,-4 -6.2,-5.8 -7.8,-6.3 -15,-9.6 -22.9,-10.6 -5.7,-0.7 -11.2,-0.1 -16.2,1.9 -1.6,0.6 -3.1,1.4 -4.2,2 l -0.3,0.2 -0.2,-0.3 c -1.2,-2.2 -2.8,-4.2 -4.9,-6 -4.1,-3.4 -9,-5 -14.7,-4.6 H 95.1 L 95,43.1 C 94,40.3 92.4,37.8 90.1,35.6 83.4,29.1 73.3,28.1 65.4,33.1 62.1,35.2 59.6,38 58,41.6 L 57.8,42.1 57.3,42 c -1.9,-0.3 -3.8,-0.3 -5.9,0 -4.9,0.6 -9.5,3.2 -12.8,7.2 -3.5,4.3 -5,9.7 -4.3,15.3 0.4,3.4 1.6,6.4 3.5,9.1 0,0.1 0.1,0.1 0.1,0.1 l 0.1,0.2 -0.2,0.3 c -3.4,4.2 -4.9,8.9 -4.5,14.2 0.5,6.8 3.7,12.2 9.6,15.9 3.5,2.3 7.7,3.2 12.3,2.8 l 0.3,-0.1 0.1,0.3 c 1.2,3.4 3.2,6.2 5.8,8.4 3.4,2.9 7.2,4.4 11.3,4.7 0.3,0 0.4,0.1 0.7,0.5 l 6.5,8.8 c 2.5,3.3 5,6.8 7.5,10.2 0.5,0.7 0.5,1.1 0.3,1.6 -0.4,0.9 -0.6,1.9 -0.9,2.7 v 0.1 c -0.1,0.4 -0.2,0.8 -0.3,1.2 l -2,6.5 c -1.3,4.1 -2.5,8.2 -3.8,12.2 -0.5,1.6 -0.3,3 0.4,4.1 0.8,1.1 2.1,1.6 3.8,1.6 H 109 c 1,0 2,0 3,0 2.7,0 5.4,0 8.1,0 1.2,0 2.2,-0.3 3,-1 1.7,-1.5 1.5,-3.4 1,-4.9 l -1.8,-5.9 c -1.7,-5.6 -3.4,-11.3 -5.1,-17.1 -0.1,-0.5 -0.1,-0.8 0.2,-1.3 4.3,-6.8 8.5,-13.7 12.7,-20.4 l 5.2,-8.3 c 0.2,-0.3 0.4,-0.7 0.7,-1 l 0.3,-0.4 7.9,9.1 c 3.1,3.6 6.4,7.4 10,10.7 2.5,2.3 5.5,3.8 8.4,5.2 l 0.9,0.4 0.7,-2.9 c 0.2,-1.2 0.5,-2.3 0.8,-3.5 z M 113.9,75 c 2.4,-3.5 3.6,-7.4 3.5,-11.7 0,-0.5 0.1,-0.7 0.6,-1 5.2,-3.3 10.5,-4.6 16.5,-4 5.4,0.6 10.5,2.7 16,6.5 0.7,0.5 1.5,1 2.3,1.7 l 0.6,0.5 -0.7,0.2 c -1.5,0.4 -3,0.7 -4.3,0.9 -3.1,0.6 -6.1,1.1 -9.1,1.6 -4,0.7 -8.1,1.4 -12.2,2.3 -3.5,0.7 -8.1,1.8 -12.7,3.5 l -1.1,0.4 z m -39.6,37.4 c -2.9,0 -5.7,-1.1 -7.9,-3.3 -2.2,-2.1 -3.5,-4.8 -3.8,-8.2 -0.1,-0.7 -0.1,-1.3 -0.2,-2 L 62.2,97 H 61.3 C 60.1,97.1 59,97.6 58,98 l -0.4,0.2 c -3,1.2 -6.4,1.1 -9.3,-0.3 -3,-1.4 -5.4,-4.1 -6.4,-7.3 -1.5,-4.7 0.2,-10 4.2,-13 l 4,-3 -0.5,-0.7 C 49,73 48.2,72.4 47.4,71.8 l -0.1,-0.1 c -0.2,-0.2 -0.5,-0.4 -0.7,-0.5 -3,-2.5 -4.5,-5.7 -4.4,-9.3 0.1,-5.3 3,-9.4 8,-11.2 2.8,-1 5.6,-0.9 8.5,0.3 l 4.7,2 0.3,-0.9 c 0.4,-1.1 0.6,-2.2 0.7,-3.3 l 0.2,-1 c 0.8,-4.4 4.5,-9.3 10.9,-9.6 4.2,-0.2 7.7,1.4 10.3,5 1.4,1.9 2.1,4.2 2.3,7.1 0.1,0.9 0.1,1.8 0.4,2.7 L 88.7,53.8 91,53 c 0.8,-0.3 1.6,-0.6 2.4,-0.8 5.5,-2 11.6,0.4 14.3,5.5 2.2,4.1 2,8.3 -0.6,12.5 -1,1.6 -2.5,2.8 -4.2,3.9 l -0.5,0.4 c -1,0.7 -1.4,1.1 -1.4,1.7 0,0.6 0.4,1.1 1.3,1.8 1.6,1.4 3.2,2.8 4.3,4.7 1.9,3.1 2.1,7.1 0.6,10.7 -1.5,3.6 -4.4,6.1 -8.3,7 -2.3,0.6 -4.7,0.3 -7.1,-0.7 -0.7,-0.3 -1.4,-0.6 -2.1,-0.9 L 87.2,97.7 87,98.4 c -0.4,1 -0.5,1.9 -0.7,2.8 l -0.1,0.7 c -0.7,4 -2.7,7 -6.1,8.9 -1.9,1 -3.9,1.6 -5.8,1.6 z m 46,7.2 c -3.9,6.3 -7.4,11.9 -11,17.7 -0.8,1.3 -0.9,2.6 -0.4,4.3 l 4.5,15.1 c 0.5,1.5 0.9,3 1.3,4.5 l 0.1,0.5 h -0.5 c -2.1,0 -4.2,0 -6.3,0 h -2.7 c -4.7,0 -9.7,0 -14.8,0.2 H 90 l 0.1,-0.5 c 0.2,-0.7 0.4,-1.4 0.6,-2.1 0.1,-0.4 0.3,-0.9 0.4,-1.3 1.8,-6 3.4,-11 4.9,-15.7 0.7,-2.1 0.3,-4 -1,-5.8 -3.2,-4.3 -6.5,-8.7 -9.4,-12.7 l -3.5,-4.7 0.4,-0.2 c 3.4,-1.6 6,-3.8 8.1,-6.7 0.7,-1 1.3,-2.1 1.8,-3.1 l 0.2,-0.5 0.5,0.1 c 3,0.5 5.9,0.3 8.6,-0.5 6.9,-2.1 11.5,-6.6 13.7,-13.5 l 0.1,-0.5 0.5,0.1 c 3.1,0.5 5.7,2.3 8.3,4.2 1.4,1 2.6,2.2 3.9,3.3 0.5,0.5 1.1,1 1.7,1.4 l 0.3,0.2 z m 38.1,-3.2 c -0.1,1.6 -0.2,3.1 -0.4,4.6 l -0.1,0.8 -1.3,-1.4 c -2.8,-3 -5.5,-6.2 -8.1,-9.2 -2,-2.3 -4.2,-4.9 -6.4,-7.3 -3.5,-3.8 -7.3,-7.9 -11.7,-11.4 -2.4,-1.9 -4.7,-3.4 -7,-4.5 l -0.9,-0.4 0.9,-0.3 c 0.8,-0.2 1.6,-0.4 2.5,-0.6 6.4,-1.1 12.4,-0.4 17.7,2.2 7.1,3.5 11.7,9.3 13.7,17.5 1,3.2 1.3,6.5 1.1,10 z"
id="path4"
style="fill:#1a1a1a;fill-opacity:1" />
<path
class="st0"
d="m 75.4,59.4 h -0.2 -0.1 c -8.7,0.1 -15.5,7.1 -15.5,15.9 0,4.3 1.6,8.3 4.6,11.3 2.9,2.9 6.9,4.6 11.2,4.6 8.8,0 15.8,-7 15.8,-15.8 0,-4.3 -1.6,-8.3 -4.6,-11.2 -3,-3.1 -7,-4.8 -11.2,-4.8 z m -0.1,23.5 c -2,0 -3.9,-0.8 -5.3,-2.3 -1.4,-1.5 -2.2,-3.4 -2.2,-5.4 0.1,-4.3 3.3,-7.6 7.6,-7.6 2,0 3.8,0.8 5.2,2.2 1.5,1.5 2.3,3.5 2.3,5.5 0,4.2 -3.4,7.6 -7.6,7.6 z"
id="path6"
style="fill:#1a1a1a;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
xml:space="preserve"
sodipodi:docname="handmadeArtificial.svg"
width="40"
height="40"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs13" /><sodipodi:namedview
id="namedview11"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.595"
inkscape:cx="100"
inkscape:cy="99.860918"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<style
type="text/css"
id="style2">
.st0{fill:#1E1E1C;}
.st1{fill:none;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
</style>
<g
id="g8"
transform="matrix(0.2875629,0,0,0.2875629,-8.75629,-8.7994244)"
style="fill:#1a1a1a;fill-opacity:1">
<path
class="st0"
d="m 92.8,87.4 c 0,-0.2 0,-0.3 0,-0.4 V 65.7 c 0,-0.5 -0.1,-0.8 -0.3,-0.9 -0.2,-0.2 -0.5,-0.3 -1,-0.3 -0.1,0 -0.3,0 -0.4,0 h -20 -0.3 c -0.2,0 -0.4,0 -0.5,0 -0.4,0 -0.7,0.2 -0.8,0.2 -0.2,0.2 -0.3,0.5 -0.3,0.8 0,0.2 0,0.3 0,0.5 V 88 H 92.7 Z M 85.3,72.3 c 0,0.9 0,1.8 0,2.7 v 2.3 c 0,0.9 0,1.8 0,2.8 v 0.4 h -0.4 c -2.6,0 -5.2,0 -7.9,0 h -0.4 v -0.4 c 0,-2.6 0,-5.2 0,-7.9 V 71.8 H 77 c 2.6,0 5.1,0 7.9,0 h 0.2 z"
id="path4"
style="fill:#1a1a1a;fill-opacity:1" />
<path
class="st0"
d="m 167.4,121.4 c -0.1,-0.4 -0.2,-0.7 -0.4,-1.1 L 153.1,81.9 C 152.2,79.8 150.9,80 150.9,80 l -16.9,4 1.2,-0.9 9.6,-6.7 v 0 l 19.6,-13.8 -39.3,-9.8 v 0 c -0.6,-0.1 -1.2,-0.3 -1.7,-0.4 -1,-0.2 -1.4,0.2 -1.8,0.9 -0.1,0.1 -0.1,0.3 -0.2,0.4 v 0 l -0.4,0.7 -9,-15.8 c -0.4,-0.8 -0.7,-1.1 -1.1,-1.2 -0.4,-0.1 -0.8,0.1 -1.5,0.5 l -2.7,1.5 c -2,1.2 -4,2.3 -6,3.5 l -0.6,0.3 -0.1,-0.6 v -2.3 c 0,-1.4 0,-2.7 0,-4.1 0,-0.5 0,-1 0,-1.5 0,-1.2 0,-2.4 -0.1,-3.5 V 30.6 H 64.1 C 62.4,30.7 62,32 62,32.7 V 33 c 0,3.2 0,6.4 0,9.6 v 0.6 L 61.4,42.9 C 60.7,42.5 60,42.1 59.3,41.7 l -1.6,-0.9 c -1.8,-1 -3.7,-2.1 -5.5,-3.2 -0.3,-0.2 -0.7,-0.4 -1.1,-0.2 -0.4,0.1 -0.6,0.6 -0.7,0.8 0,0.1 -0.1,0.2 -0.2,0.4 l -18.3,31.3 0.4,0.2 c 1.7,1.1 3.5,2.1 5.3,3.1 0.2,0.1 0.4,0.2 0.5,0.3 v 0 l 5,2.5 -2.8,1.6 c 0,0 -0.1,0 -0.1,0.1 l -8.3,4.8 0.2,0.4 c 1.9,3.7 17.8,31.4 18.8,32.4 l 0.2,0.3 6.2,-3.5 4.7,-3 -0.2,6.8 v 0 c 0,1.6 0,3.1 0,4.6 0,0.3 0,0.8 0.3,1.2 0.3,0.3 0.8,0.3 1,0.3 0.5,0 1.1,0 1.7,0.1 h 0.1 c 0.6,0.1 1.2,0.1 1.7,0.1 0.7,0 1.4,0 2.1,0 0.9,0 1.8,0 2.7,0 0.1,0 0.1,0 0.2,0 0.7,0 1.2,0.3 1.8,0.9 4,5.1 8.4,10.6 13.4,16.7 0.5,0.6 0.6,1.1 0.4,1.8 -1.9,5.9 -3.9,11.7 -5.8,17.6 l -2.8,7.9 c -0.2,0.6 -0.1,1.2 0.2,1.8 0.4,0.5 0.9,0.8 1.6,0.8 h 44.2 l -0.2,-1 c -0.1,-0.2 -0.1,-0.4 -0.1,-0.6 l -1.8,-6.1 c -1.9,-6.6 -3.9,-13.4 -5.9,-20.3 -0.3,-1 -0.2,-1.7 0.4,-2.7 4.4,-7 8.8,-14.1 13.2,-21.1 l 3.8,-6.1 c 0.1,-0.1 0.2,-0.3 0.2,-0.4 l 0.3,0.1 0.1,-0.4 c 0.2,0 0.4,0.1 0.8,0.3 l 30.3,11.3 c 0.3,0.1 0.6,0.2 1,0.3 l 1.4,0.5 z M 129.3,70.5 c 0.5,-0.3 0.6,-0.8 0.4,-1.3 l -0.9,-1.5 c -0.7,-1.3 -1.5,-2.5 -2.2,-3.8 l -2.6,-4 16.5,4 v 0 l 0.7,0.2 5.8,1.9 -2.6,1.7 -4.6,3 c -4.6,3 -9.2,5.9 -13.8,8.9 -0.2,0.2 -0.4,0.2 -0.6,0.2 -0.2,0 -0.3,-0.1 -0.5,-0.2 -1.5,-0.9 -3,-1.8 -4.5,-2.7 l -1.1,-0.6 z m -59,43.9 h -0.9 v -0.5 -17.6 l -0.7,0.3 c -1.7,0.8 -3.3,1.8 -4.9,2.7 l -0.1,0.1 c -0.9,0.5 -1.8,1.1 -2.7,1.6 -1.8,1 -3.7,2.1 -5.3,3.1 l -2,1.2 -0.6,-1.1 c -0.6,-1 -1.2,-2 -1.8,-3 -3,-5.1 -5.9,-10.3 -8.8,-15.4 L 42.2,85.3 42.7,85 56,77.3 c 0.2,-0.1 0.4,-0.3 0.7,-0.4 L 57.8,76.2 43.7,68 C 42.9,67.5 42.6,66.5 43.1,65.7 L 53,48.6 c 0.5,-0.8 1.5,-1.1 2.3,-0.6 L 68,55.3 c 0.3,0.2 0.6,0.2 0.9,0 0.3,-0.2 0.5,-0.5 0.5,-0.8 v -0.8 c 0,-1.2 0.1,-2.2 0.1,-3.3 0,-2.4 0,-4.8 0,-7.2 V 38 H 70 c 7.3,0 14.7,0 22,0 h 0.5 v 16.5 c 0,0.3 0.2,0.7 0.5,0.8 0.3,0.1 0.7,0.2 1,0 L 106.8,48 c 0.8,-0.5 1.9,-0.2 2.3,0.6 l 6.8,11.9 c 1.2,2 2.3,4 3.5,6.1 l 0.3,0.6 -14.3,8.2 c -0.3,0.2 -0.4,0.5 -0.4,0.8 0,0.3 0.2,0.6 0.5,0.8 l 12.8,7.4 c 0.8,0.5 1.1,1.5 0.6,2.3 l -9.9,17.1 c -0.2,0.4 -0.6,0.7 -1,0.8 -0.4,0.1 -0.9,0.1 -1.3,-0.2 L 93.2,96.6 92.9,97 c -0.4,0.4 -0.4,0.9 -0.3,1.4 v 8.4 3.7 c 0,1.3 0,2.4 -0.2,3.6 l -0.1,0.3 h -0.5 c -0.1,0 -0.2,0 -0.3,0 -7,0 -14.1,0.1 -21.2,0 z m 55.8,-4.5 -9.5,15.2 c -2.7,4.2 -5.3,8.5 -8,12.7 -0.5,0.8 -0.6,1.5 -0.3,2.5 1.5,5.3 3.1,10.5 4.6,15.8 l 1.7,5.9 h -0.5 c 0,0 -0.1,0 -0.1,0 h -0.1 l -20.2,-0.1 c -0.7,0 -1.4,0 -2.1,0 h -3.1 l 1.3,-4.1 c 1.8,-5.7 3.8,-11.6 5.7,-17.3 0.4,-1.1 0.2,-1.8 -0.5,-2.7 -3.4,-4.2 -6.7,-8.4 -10.7,-13.3 l -1.9,-2.4 h 16.2 c 0.7,0 1,0 1.2,-0.3 0.3,-0.3 0.3,-0.6 0.3,-1.4 v -11 l 0.6,0.4 c 0.2,0.1 0.3,0.2 0.5,0.3 1.6,0.9 3.1,1.8 4.7,2.7 l 3.9,2.3 c 1.1,0.6 1.4,0.5 2,-0.5 l 5.5,-9.4 c 0.1,-0.1 0.1,-0.2 0.2,-0.3 l 0.2,-0.2 0.6,0.2 c 2.4,0.9 4.7,1.8 7.1,2.6 0.5,0.2 0.9,0.6 1,1 0.1,0.4 -0.1,0.9 -0.3,1.4 z m 21.6,-2.2 -26.3,-9.8 0.3,-0.6 c 0.5,-0.9 1,-1.8 1.5,-2.7 0.1,-0.2 0.3,-0.2 0.7,-0.3 l 16.3,-3.9 c 2,-0.5 3.9,-0.9 5.9,-1.4 l 1,-0.1 1,1 V 90 c 1.5,4.1 2.9,8.1 4.4,12.2 l 3.6,8.7 z"
id="path6"
style="fill:#1a1a1a;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
xml:space="preserve"
sodipodi:docname="mortuary.svg"
width="40"
height="40"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs9" /><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.595"
inkscape:cx="100"
inkscape:cy="99.860918"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<style
type="text/css"
id="style2">
.st0{fill:#1E1E1C;}
.st1{fill:none;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
</style>
<path
class="st0"
d="m 35.428571,16.4 c 0,-8.085714 -6.228572,-14.714286 -14.142857,-15.371429 V 0.314286 C 21.285715,0.142857 21.142858,0 20.971429,0 H 19.314286 C 19.142858,0 19,0.142857 19,0.314286 V 1 C 10.942858,1.514286 4.5714285,8.228571 4.5714285,16.4 c 0,5.6 3,10.514286 7.4857145,13.228571 L 9.3714285,37.4 c -0.171429,0.542857 0.08571,1.114286 0.628571,1.285714 l 0.2285725,0.08571 c 0.542856,0.171428 1.114286,-0.08571 1.285714,-0.628572 l 2.6,-7.485714 c 1.428572,0.6 2.999999,0.971428 4.628571,1.114286 v 7.2 C 18.742858,39.542857 19.2,40 19.771429,40 h 0.257143 C 20.6,40 21.057143,39.542857 21.057143,38.971429 V 31.8 c 1.657143,-0.114286 3.257143,-0.485714 4.714286,-1.085714 l 2.599999,7.514285 c 0.17143,0.542858 0.771429,0.8 1.285714,0.628572 l 0.228572,-0.08571 C 30.428571,38.6 30.685715,38 30.514286,37.485714 L 27.828572,29.714286 C 32.371428,27.028571 35.428571,22.057143 35.428571,16.4 Z m -2.285714,0 c 0,0.4 -0.02857,0.8 -0.05714,1.2 -1.6,-0.142857 -3.114285,-0.542857 -4.6,-1.085714 0,0 0,-0.02857 0,-0.02857 C 28.485715,11.8 24.685715,8 20,8 c -0.942857,0 -1.885715,0.142857 -2.771429,0.457143 -1.342857,-1.428572 -2.399999,-2.771429 -3.142856,-3.8 1.771428,-0.885714 3.771428,-1.4 5.914285,-1.4 7.257143,0 13.142857,5.885714 13.142857,13.142857 z M 19.085715,10.342857 c 0.285713,-0.05714 0.6,-0.05714 0.914285,-0.05714 3.085715,0 5.628571,2.257143 6.114285,5.2 -2.657142,-1.4 -5.057142,-3.257143 -7.02857,-5.142857 z M 12.114286,5.885714 C 13.228572,7.4 15.057143,9.685714 17.457143,12 c 4.857143,4.628571 10.085715,7.342857 15.199999,7.885714 -0.314285,1.142857 -0.771428,2.2 -1.371427,3.2 -11.2,-0.2 -19.742857,-11.971428 -21.6285725,-14.8 C 10.371429,7.371429 11.2,6.571429 12.114286,5.885714 Z m 1.914285,11.2 c 2.371428,2.285715 4.828572,4.114286 7.314286,5.485715 -0.428572,0.08571 -0.885714,0.142857 -1.342857,0.142857 -3.314285,0 -6,-2.6 -6.200001,-5.857143 0.08572,0.08571 0.142858,0.142857 0.228572,0.228571 z m 5.057144,12.428572 C 12.4,29.057143 7.0571425,23.485714 6.8571425,16.8 c -0.05714,-2.314286 0.457143,-4.485714 1.457143,-6.4 0.742857,1.057143 1.8857145,2.6 3.4000005,4.285714 -0.142857,0.6 -0.2,1.228572 -0.2,1.828572 C 11.514286,21.2 15.314285,25 20,25 c 1.485714,0 2.914285,-0.371429 4.171428,-1.085714 1.828571,0.714285 3.685715,1.2 5.514286,1.371428 -2.571429,2.828572 -6.4,4.514286 -10.599999,4.228572 z"
id="path4"
style="stroke-width:0.285714;fill:#1a1a1a;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40mm"
height="40mm"
viewBox="0 0 40 40"
version="1.1"
id="svg1600"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="pets.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs1594" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.0684205"
inkscape:cx="49.699837"
inkscape:cy="46.277881"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="0" />
<metadata
id="metadata1597">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g885"
style="fill:#1a1a1a;fill-opacity:1">
<path
id="path8"
style="fill:#1a1a1a;stroke-width:0.061654;fill-opacity:1"
d="m 19.703707,14.502266 v 0.0062 c -3.970522,0.06782 -7.694429,1.566152 -10.5120202,4.211112 -2.8854103,2.706613 -4.4694945,6.301341 -4.4694945,10.117727 v 0.301792 h 0.00568 c 0.1294733,4.451422 3.6995645,8.002625 9.5875287,9.519317 1.886615,0.487066 3.964124,0.746207 6.017203,0.746207 h 0.0925 c 2.108569,-0.01233 4.204824,-0.283607 6.060615,-0.801502 5.622848,-1.559849 8.829214,-5.074293 8.792223,-9.649024 -0.03699,-3.890372 -1.646645,-7.52801 -4.538221,-10.25312 -2.873078,-2.706614 -6.689241,-4.19871 -10.739912,-4.19871 z m 0.191202,2.306319 h 0.0987 c 3.471124,0 6.726416,1.270111 9.167917,3.569807 2.435336,2.293533 3.791643,5.345174 3.816302,8.600509 0.02466,3.452627 -2.564741,6.159413 -7.114809,7.423319 -1.670828,0.462407 -3.551369,0.709001 -5.450314,0.715203 h -0.0739 c -1.868119,0 -3.748181,-0.234566 -5.455998,-0.672311 C 9.9258243,35.168873 7.0587142,32.450255 7.0093913,28.929807 v -0.08061 c 0,-3.175187 1.3317326,-6.177643 3.7424067,-8.446514 2.42917,-2.281201 5.653491,-3.557105 9.143111,-3.594095 z" />
<path
id="path26"
style="fill:#1a1a1a;stroke-width:0.061654;fill-opacity:1"
d="M 26.238191,0.59665452 C 25.201227,0.56851579 24.157001,1.0061208 23.235274,1.8627271 22.224144,2.8060343 21.46583,4.1686855 21.102071,5.7100374 20.738312,7.251389 20.799933,8.8113797 21.27467,10.09995 c 0.536391,1.455036 1.529238,2.422794 2.799312,2.724899 0.265113,0.06165 0.536424,0.0925 0.807703,0.0925 1.004962,1e-6 2.015954,-0.437841 2.916101,-1.264005 1.011129,-0.943308 1.769443,-2.3059584 2.133203,-3.8473105 C 30.294749,6.2646819 30.233125,4.7052079 29.758391,3.4166373 29.215834,1.9616013 28.223466,0.99332729 26.953392,0.69122239 26.71641,0.63457769 26.477491,0.60314808 26.238191,0.59665452 Z m -0.0863,2.29546728 c 0.09248,-3e-7 0.18495,0.01251 0.2651,0.031006 0.499396,0.1171429 0.912079,0.573587 1.177189,1.2826089 0.320601,0.875488 0.357614,1.9664889 0.0925,3.070097 -0.258947,1.103608 -0.782698,2.0593724 -1.460893,2.6944092 -0.554886,0.5117281 -1.134612,0.7336421 -1.634009,0.6164991 -0.493233,-0.117142 -0.906394,-0.57307 -1.171504,-1.2820916 -0.3206,-0.875488 -0.357614,-1.9670056 -0.0925,-3.0706136 0.258947,-1.1036078 0.783213,-2.0593725 1.461409,-2.6944092 0.456238,-0.425413 0.931128,-0.6475055 1.362708,-0.6475058 z" />
<path
id="path32"
style="fill:#1a1a1a;stroke-width:0.061654;fill-opacity:1"
d="m 36.206575,6.0790071 c -0.733356,0.010554 -1.501195,0.2351881 -2.257226,0.6686931 -1.196092,0.6905254 -2.23805,1.8499745 -2.940905,3.2680178 -0.709025,1.42421 -0.998387,2.95298 -0.819589,4.3217 0.197294,1.535186 0.949338,2.700355 2.1146,3.279903 0.493231,0.240451 1.02331,0.363802 1.578197,0.363802 0.746014,0 1.535129,-0.221683 2.305802,-0.665593 1.196091,-0.690526 2.238049,-1.849974 2.940907,-3.268017 0.70902,-1.424209 0.998902,-2.952979 0.820105,-4.3217002 C 39.751174,8.190627 38.999128,7.0254581 37.833866,6.4459098 37.32676,6.1923575 36.776964,6.0707992 36.206575,6.0790071 Z m 0.04857,2.2965007 c 0.203459,0 0.394742,0.043388 0.561208,0.1297078 0.456239,0.22812 0.758661,0.7645595 0.85731,1.5229044 0.117145,0.924811 -0.09285,1.99131 -0.598413,3.008602 -0.50556,1.017292 -1.226796,1.837195 -2.028298,2.299601 -0.659699,0.37609 -1.275956,0.462214 -1.732196,0.234094 -0.456239,-0.22812 -0.758671,-0.770725 -0.857311,-1.522904 -0.117144,-0.924811 0.09233,-1.99131 0.597895,-3.008602 0.505563,-1.017292 1.227315,-1.8371952 2.028817,-2.2996011 0.419248,-0.240451 0.819563,-0.3638021 1.170988,-0.3638021 z" />
<path
id="path26-6"
style="fill:#1a1a1a;stroke-width:0.061654;fill-opacity:1"
d="m 13.762467,0.59665452 c -0.239299,0.006494 -0.478218,0.0379232 -0.715202,0.0945679 -1.270074,0.3021049 -2.262958,1.27037888 -2.805513,2.72541488 -0.4747359,1.2885703 -0.5363581,2.8480446 -0.172599,4.3893962 0.363761,1.5413521 1.122075,2.9040025 2.133203,3.8473105 0.900148,0.826164 1.911656,1.264006 2.916618,1.264005 0.271279,0 0.542072,-0.03085 0.807185,-0.0925 1.270075,-0.302105 2.262922,-1.269863 2.799313,-2.724899 C 19.200209,8.8113797 19.26183,7.2513893 18.898071,5.7100374 18.534312,4.1686855 17.775998,2.8060343 16.764868,1.8627271 15.84314,1.0061207 14.799432,0.56851579 13.762467,0.59665452 Z m 0.0863,2.29546728 c 0.43158,0 0.905953,0.2220925 1.362191,0.6475055 0.678196,0.6350367 1.202462,1.5908014 1.461409,2.6944092 0.265113,1.103608 0.228099,2.1951256 -0.0925,3.0706136 C 16.314756,10.013672 15.901595,10.4696 15.408362,10.586742 14.908965,10.703885 14.329756,10.481971 13.77487,9.9702426 13.096676,9.3352058 12.572407,8.3794414 12.31346,7.2758334 c -0.265113,-1.1036081 -0.228099,-2.194609 0.0925,-3.070097 0.26511,-0.7090219 0.678309,-1.165466 1.177706,-1.2826089 0.08015,-0.018497 0.17262,-0.031006 0.2651,-0.031006 z" />
<path
id="path32-9"
style="fill:#1a1a1a;stroke-width:0.061654;fill-opacity:1"
d="M 3.7935669,6.0790071 C 3.2231793,6.0707997 2.6733809,6.1923575 2.166276,6.4459098 1.001015,7.0254579 0.24896743,8.190627 0.05167643,9.7258128 c -0.178799,1.3687212 0.11056822,2.8974912 0.81958822,4.3217002 0.70285695,1.418043 1.74481575,2.577491 2.94090565,3.268017 0.7706752,0.44391 1.5603043,0.665593 2.3063192,0.665593 0.554886,0 1.0849652,-0.123351 1.5781983,-0.363802 1.165261,-0.579548 1.9173055,-1.744717 2.1145994,-3.279903 0.1787962,-1.36872 -0.1110818,-2.89749 -0.8201048,-4.3217 C 8.2883263,8.597675 7.2463667,7.4382256 6.0502767,6.7477002 5.2942444,6.3141952 4.5269224,6.0895602 3.7935669,6.0790071 Z m -0.049093,2.2965007 c 0.351427,0 0.7522567,0.1233511 1.1715046,0.3638021 0.801502,0.4624059 1.5227369,1.2823091 2.0283001,2.2996011 0.5055639,1.017292 0.7155568,2.083791 0.5984128,3.008602 -0.09864,0.752179 -0.4010707,1.294784 -0.8573119,1.522904 C 6.229142,15.798537 5.6128851,15.712413 4.9531859,15.336323 4.1516839,14.873917 3.4299313,14.054014 2.9243692,13.036722 2.4188063,12.01943 2.20933,10.952931 2.326473,10.02812 2.4251229,9.2697751 2.727544,8.7333356 3.1837849,8.5052156 3.3502499,8.4188955 3.5410153,8.3755078 3.7444739,8.3755078 Z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
xml:space="preserve"
sodipodi:docname="preserved.svg"
width="40"
height="40"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs9" /><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.595"
inkscape:cx="100"
inkscape:cy="99.860918"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<style
type="text/css"
id="style2">
.st0{fill:#1E1E1C;}
.st1{fill:none;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
</style>
<path
class="st0"
d="m 19.999996,39.999996 c 0.632411,0 1.121092,-0.517427 1.121092,-1.121092 v -8.39382 h 0.114984 c 2.874596,-0.08624 5.116781,-0.977363 6.669062,-2.644628 2.673374,-2.817104 2.443407,-6.927776 2.357169,-7.732663 v 0 c -0.05749,-0.603665 -0.546173,-1.092346 -1.149839,-1.149838 -0.287459,-0.02875 -1.322314,-0.114984 -2.644628,0.08624 l -0.488681,0.08624 0.431189,-0.258714 c 0.546174,-0.344951 1.034855,-0.747395 1.466044,-1.20733 2.673374,-2.817104 2.443407,-6.927776 2.357169,-7.732663 C 30.176067,9.328063 29.687384,8.839382 29.083719,8.78189 28.537545,8.7244 26.58282,8.609414 24.484365,9.356809 L 24.31189,9.414299 24.34064,9.213077 C 24.455623,8.609412 24.513115,7.977001 24.513115,7.373336 24.398131,3.492632 21.408552,0.790512 20.833633,0.301831 c -0.488682,-0.402444 -1.149839,-0.402444 -1.63852,0 -0.603665,0.459935 -3.593245,3.162055 -3.708228,7.042759 -0.02875,0.632411 0.02875,1.236076 0.172475,1.868487 l 0.02875,0.201222 -0.172475,-0.05749 C 13.41718,8.609414 11.433709,8.724398 10.887535,8.78189 10.28387,8.83938 9.7951894,9.328063 9.7376974,9.931728 c -0.08624,0.804887 -0.316205,4.915559 2.3571686,7.732663 0.431189,0.431189 0.891124,0.833633 1.437297,1.178584 l 0.402444,0.258714 -0.488681,-0.08624 c -0.632411,-0.08624 -1.178585,-0.114984 -1.609774,-0.114984 -0.431189,0 -0.747395,0.02875 -0.919871,0.02875 -0.603665,0.05749 -1.0923456,0.546173 -1.1498376,1.149838 -0.08624,0.804887 -0.316206,4.886813 2.3571686,7.732663 1.552281,1.63852 3.794466,2.529644 6.640316,2.644628 h 0.114984 v 8.39382 c 0,0.632411 0.488681,1.149838 1.121092,1.149838 z M 27.93388,21.171394 h 0.114984 v 0.114984 c -0.02875,1.20733 -0.28746,3.420769 -1.782249,5.001796 -1.121093,1.20733 -2.788358,1.839742 -4.973051,1.954725 H 21.17858 v -0.114983 c 0.08624,-2.299677 0.776141,-4.053181 2.012217,-5.203019 1.523536,-1.49479 3.621991,-1.753503 4.743083,-1.753503 z M 27.87639,10.966579 h 0.114984 v 0.114984 c -0.02875,1.20733 -0.287459,3.420769 -1.782249,5.001796 -1.121093,1.178585 -2.788358,1.839742 -4.973051,1.925979 H 21.12109 v -0.114983 c 0.08624,-2.299677 0.776141,-4.05318 2.012217,-5.203019 1.552282,-1.437298 3.650737,-1.696011 4.743083,-1.724757 z M 17.757811,7.40208 c 0.05749,-2.155947 1.379806,-3.880704 2.184693,-4.714337 l 0.08624,-0.08624 0.08624,0.08624 c 0.776141,0.833633 2.098455,2.55839 2.184693,4.714337 0.05749,1.696012 -0.689903,3.392023 -2.155947,5.030543 l -0.08624,0.08624 -0.08624,-0.08624 C 18.418968,10.794103 17.700319,9.098092 17.757811,7.40208 Z m -4.024434,8.681279 c -1.466044,-1.552281 -1.753503,-3.794466 -1.782249,-5.001796 v -0.114984 h 0.114984 c 1.35106,0.02875 3.277039,0.344951 4.743083,1.724757 v 0 c 1.236076,1.149839 1.897233,2.903342 2.012217,5.203019 v 0.114983 h -0.114984 c -2.184693,-0.08624 -3.851958,-0.747394 -4.973051,-1.925979 z m 5.001797,12.130794 c -2.184693,-0.08624 -3.851959,-0.747395 -4.973051,-1.925979 -1.466044,-1.552281 -1.753503,-3.794466 -1.782249,-5.001796 v -0.114984 h 0.114984 c 1.121092,0.02875 3.219547,0.287459 4.743083,1.724757 1.236076,1.149839 1.897233,2.903342 2.012217,5.203019 v 0.114983 z M 18.418968,21.228886 C 18.074017,20.91268 17.700319,20.625221 17.26913,20.337761 l -0.431189,-0.258714 0.488681,0.05749 c 0.833633,0.114984 1.638519,0.14373 2.500898,0.114984 0.02875,0 0.05749,0 0.08624,0 h 0.05749 0.05749 c 0.02875,0 0.05749,0 0.08624,0 0.201221,0 0.373697,0 0.574919,0 0.689903,0 1.379806,-0.05749 2.040963,-0.14373 l 0.517427,-0.08624 -0.431189,0.28746 c -0.43119,0.258714 -0.804887,0.574919 -1.149839,0.891125 -0.574919,0.546173 -1.092346,1.236076 -1.523535,2.012217 l -0.08624,0.172476 -0.08624,-0.172476 c -0.459931,-0.747391 -0.977359,-1.437294 -1.552278,-1.983467 z"
id="path4"
style="stroke-width:0.28746;fill-opacity:1;fill:#1a1a1a" />
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
xml:space="preserve"
sodipodi:docname="treatments.svg"
width="40"
height="40"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs13" /><sodipodi:namedview
id="namedview11"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.595"
inkscape:cx="100"
inkscape:cy="99.860918"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<style
type="text/css"
id="style2">
.st0{fill:#1E1E1C;}
.st1{fill:none;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;stroke:#1E1E1C;stroke-width:8;stroke-miterlimit:10;}
</style>
<g
id="g8"
transform="matrix(0.28715004,0,0,0.28715004,-9.3036616,-8.6719315)"
style="fill:#1a1a1a;fill-opacity:1">
<path
class="st0"
d="m 88.4,48.2 c -10.7,3.1 -16.5,10.9 -16.5,22 v 99.3 h 60.3 c 0,-0.1 0,-0.2 0,-0.3 0,-8.8 0,-17.6 0,-26.4 0,-24.2 0,-49.3 -0.1,-74.1 0,-7.7 -3.7,-13.9 -10.6,-18.1 v 0 c -1.3,-0.8 -2.8,-1.4 -4.4,-2 H 117 c -0.7,-0.3 -1.4,-0.6 -2.2,-0.9 l -0.2,-0.1 V 30.2 H 89.3 V 47.8 L 88.8,48 c -0.2,0.1 -0.3,0.2 -0.4,0.2 z m 35.7,113.4 H 79.9 V 73.2 h 44.2 z M 97.4,38.2 h 9.3 v 9.4 h -9.3 z m -4.3,17.4 h 3.4 c 4.8,0 9.7,-0.1 14.5,0 5.2,0.1 9.2,2.6 11.8,7.4 0.2,0.3 0.3,0.7 0.5,1 v 0.1 c 0.1,0.2 0,0.3 0,0.4 0,0 0,0.1 0,0.1 v 0.3 H 80.5 l 0.2,-0.5 c 0.7,-2.2 2,-4.1 3.9,-5.7 2.5,-2 5.4,-3.1 8.5,-3.1 z"
id="path4"
style="fill:#1a1a1a;fill-opacity:1" />
<path
class="st0"
d="m 114.7,130.7 c 0.1,-3.1 0,-6.2 0,-9.3 v -1 c 0,-1.1 0,-2.2 0,-3.2 v -0.1 c 0,-1.1 0,-2.3 0,-3.4 v -0.2 c 0,-3 0,-6.1 0,-9.1 0,-1.4 -0.2,-2.6 -0.4,-3.6 -1.4,-5.5 -6.5,-9.5 -12.2,-9.5 -0.5,0 -1,0 -1.4,0.1 -6.5,0.8 -11.3,6.1 -11.4,12.5 -0.1,8.4 0,17.1 0,24.8 v 1.9 c 0,0.9 0.1,1.9 0.4,2.9 1.5,6.1 7.3,10.2 13.5,9.6 6.5,-0.7 11.4,-6 11.5,-12.4 z m -7.9,-9.7 c 0,3 0,6 0,9 0,3 -1.9,5.2 -4.7,5.2 -1.3,0 -2.4,-0.5 -3.3,-1.3 -1,-0.9 -1.5,-2.3 -1.5,-3.8 0,-8.5 0,-17.2 0,-25.7 0,-1.5 0.5,-2.9 1.5,-3.8 0.9,-0.8 2,-1.3 3.3,-1.3 2.8,0 4.7,2.2 4.7,5.2 0,3 0,6 0,9 v 3.8 z"
id="path6"
style="fill:#1a1a1a;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

72
public/statics/icon.svg Normal file
View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="187"
height="187"
viewBox="0 0 187 187"
enable-background="new 0 0 595 842"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="logo.svg"><metadata
id="metadata21"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs19" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview17"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="4.6900433"
inkscape:cx="83.335292"
inkscape:cy="99.203526"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" /><g
id="Background"
transform="translate(-211.7456,-282.24899)" /><g
id="Guides"
transform="translate(-211.7456,-282.24899)" /><g
id="g7"
transform="matrix(1.0030446,0,0,1.0030446,-212.39029,-288.74375)"
style="fill:#8ed300;fill-opacity:1"><path
style="fill:#8ed300;fill-opacity:1"
inkscape:connector-curvature="0"
id="path9"
d="m 339.611,301.147 c 1.324,-0.375 2.663,-0.656 4.017,-0.838 l 54.55,-7.391 -1.039,30.924 c -0.862,25.488 -15.732,48.394 -34.025,53.571 -1.319,0.374 -2.654,0.654 -4.003,0.837 l -54.551,7.379 1.038,-30.923 c 0.864,-25.481 15.725,-48.38 34.013,-53.559 z" /><path
style="fill:#8ed300;fill-opacity:1"
inkscape:connector-curvature="0"
id="path11"
d="m 304.353,399.358 27.265,-3.692 c 10.052,-1.368 17.809,8.612 17.351,22.267 l -0.523,15.469 -27.265,3.692 c -10.041,1.366 -17.811,-8.612 -17.354,-22.279 l 0.526,-15.457 z" /><path
style="fill:#8ed300;fill-opacity:1"
inkscape:connector-curvature="0"
id="path13"
d="m 212.72,326.05 49.089,-6.647 c 18.083,-2.444 32.068,15.502 31.238,40.103 l -0.941,27.826 -49.09,6.647 c -18.081,2.456 -32.057,-15.506 -31.236,-40.09 l 0.94,-27.839 z" /><path
style="fill:#8ed300;fill-opacity:1"
inkscape:connector-curvature="0"
id="path15"
d="m 248.296,407.657 c 0.966,-0.272 1.943,-0.478 2.93,-0.611 l 39.76,-5.383 -0.75,22.539 c -0.624,18.584 -11.458,35.275 -24.792,39.049 -0.966,0.272 -1.943,0.48 -2.931,0.613 l -39.772,5.385 0.761,-22.542 c 0.625,-18.573 11.461,-35.274 24.794,-39.05 z" /></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="226.229px"
height="31.038px"
viewBox="0 0 226.229 31.038"
enable-background="new 0 0 226.229 31.038"
xml:space="preserve"
id="svg2"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="logo-dark.svg"><metadata
id="metadata61"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs59" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1013"
id="namedview57"
showgrid="false"
inkscape:zoom="3.1818851"
inkscape:cx="89.825797"
inkscape:cy="26.457641"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" /><g
id="Background" /><g
id="Guides" /><g
id="g864"><g
style="fill:#ffffff"
id="g9"><path
style="fill-rule:evenodd;fill:#ffffff"
inkscape:connector-curvature="0"
id="path11"
d="M 10.417,30.321 0,0 h 8.233 l 4.26,15.582 0.349,1.276 c 0.521,1.866 0.918,3.431 1.191,4.693 0.15,-0.618 0.335,-1.345 0.555,-2.182 0.219,-0.837 0.528,-1.935 0.925,-3.293 L 19.981,0 h 8.19 l -10.5,30.321 h -7.254 z"
clip-rule="evenodd" /></g><g
style="fill:#8ed300;fill-opacity:1"
id="g13"><path
inkscape:connector-curvature="0"
style="fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
id="path15"
d="m 139.809,19.787 c -0.665,0.357 -1.748,0.686 -3.25,0.988 -0.727,0.137 -1.283,0.254 -1.667,0.35 -0.95,0.247 -1.661,0.563 -2.134,0.947 -0.472,0.384 -0.799,0.899 -0.979,1.544 -0.223,0.796 -0.155,1.438 0.204,1.925 0.359,0.488 0.945,0.731 1.757,0.731 1.252,0 2.375,-0.36 3.369,-1.081 0.994,-0.721 1.653,-1.665 1.98,-2.831 l 0.72,-2.573 z m 5.106,10.534 h -7.458 c 0.017,-0.356 0.048,-0.726 0.094,-1.11 l 0.159,-1.192 c -1.318,1.026 -2.627,1.786 -3.927,2.279 -1.299,0.493 -2.643,0.739 -4.031,0.739 -2.158,0 -3.7,-0.593 -4.625,-1.779 -0.925,-1.187 -1.106,-2.788 -0.542,-4.804 0.519,-1.851 1.431,-3.356 2.737,-4.515 1.307,-1.159 3.021,-1.972 5.142,-2.438 1.169,-0.247 2.641,-0.515 4.413,-0.803 2.646,-0.412 4.082,-1.016 4.304,-1.812 l 0.151,-0.539 c 0.182,-0.65 0.076,-1.145 -0.317,-1.483 -0.393,-0.339 -1.071,-0.508 -2.033,-0.508 -1.045,0 -1.934,0.214 -2.666,0.643 -0.731,0.428 -1.289,1.058 -1.673,1.887 h -6.748 c 1.065,-2.53 2.64,-4.413 4.723,-5.65 2.083,-1.237 4.724,-1.856 7.923,-1.856 1.991,0 3.602,0.241 4.833,0.722 1.231,0.481 2.095,1.209 2.59,2.185 0.339,0.701 0.483,1.536 0.432,2.504 -0.052,0.969 -0.377,2.525 -0.978,4.669 l -2.375,8.483 c -0.284,1.014 -0.416,1.812 -0.396,2.395 0.02,0.583 0.188,0.962 0.503,1.141 l -0.235,0.842 z"
clip-rule="evenodd" /></g><g
style="fill:#8ed300;fill-opacity:1"
id="g17"><path
inkscape:connector-curvature="0"
style="fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
id="path19"
d="m 185.7,30.321 6.27,-22.393 h 7.049 l -1.097,3.918 c 1.213,-1.537 2.502,-2.659 3.867,-3.366 1.365,-0.707 2.951,-1.074 4.758,-1.101 l -2.03,7.25 c -0.304,-0.042 -0.608,-0.072 -0.912,-0.093 -0.303,-0.02 -0.592,-0.03 -0.867,-0.03 -1.126,0 -2.104,0.168 -2.932,0.504 -0.829,0.336 -1.561,0.854 -2.197,1.555 -0.406,0.467 -0.789,1.136 -1.149,2.007 -0.361,0.872 -0.814,2.282 -1.359,4.232 l -2.104,7.516 H 185.7 z"
clip-rule="evenodd" /></g><g
style="fill:#8ed300;fill-opacity:1"
id="g21"><path
inkscape:connector-curvature="0"
style="fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
id="path23"
d="m 217.631,19.787 c -0.664,0.357 -1.748,0.686 -3.25,0.988 -0.727,0.137 -1.282,0.254 -1.667,0.35 -0.95,0.247 -1.661,0.563 -2.134,0.947 -0.472,0.384 -0.799,0.899 -0.979,1.544 -0.223,0.796 -0.155,1.438 0.205,1.925 0.359,0.488 0.945,0.731 1.757,0.731 1.252,0 2.375,-0.36 3.369,-1.081 0.994,-0.721 1.654,-1.665 1.98,-2.831 l 0.719,-2.573 z m 5.106,10.534 h -7.458 c 0.017,-0.356 0.048,-0.726 0.094,-1.11 l 0.159,-1.192 c -1.318,1.026 -2.627,1.786 -3.927,2.279 -1.299,0.493 -2.643,0.739 -4.031,0.739 -2.158,0 -3.7,-0.593 -4.625,-1.779 -0.926,-1.187 -1.106,-2.788 -0.542,-4.804 0.519,-1.851 1.431,-3.356 2.737,-4.515 1.306,-1.159 3.02,-1.972 5.142,-2.438 1.169,-0.247 2.641,-0.515 4.413,-0.803 2.647,-0.412 4.082,-1.016 4.304,-1.812 l 0.151,-0.539 c 0.182,-0.65 0.077,-1.145 -0.317,-1.483 -0.393,-0.339 -1.071,-0.508 -2.033,-0.508 -1.045,0 -1.934,0.214 -2.666,0.643 -0.731,0.428 -1.289,1.058 -1.672,1.887 h -6.748 c 1.065,-2.53 2.64,-4.413 4.723,-5.65 2.083,-1.237 4.724,-1.856 7.923,-1.856 1.99,0 3.601,0.241 4.833,0.722 1.232,0.481 2.095,1.209 2.591,2.185 0.339,0.701 0.483,1.536 0.431,2.504 -0.051,0.969 -0.377,2.525 -0.978,4.669 l -2.375,8.483 c -0.284,1.014 -0.416,1.812 -0.396,2.395 0.02,0.583 0.188,0.962 0.503,1.141 l -0.236,0.842 z"
clip-rule="evenodd" /></g><g
style="fill:#8ed300;fill-opacity:1"
id="g25"><path
inkscape:connector-curvature="0"
style="fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
id="path27"
d="m 188.386,7.928 -6.269,22.393 h -7.174 l 0.864,-3.085 c -1.227,1.246 -2.476,2.163 -3.746,2.751 -1.27,0.588 -2.625,0.882 -4.067,0.882 -2.471,0 -4.154,-0.634 -5.048,-1.901 -0.895,-1.268 -0.993,-3.149 -0.294,-5.644 l 4.31,-15.396 h 7.338 l -3.508,12.53 c -0.516,1.842 -0.641,3.109 -0.375,3.803 0.266,0.694 0.967,1.041 2.105,1.041 1.275,0 2.323,-0.422 3.142,-1.267 0.819,-0.845 1.497,-2.223 2.031,-4.133 l 3.353,-11.974 h 7.338 z"
clip-rule="evenodd" /></g><g
style="fill:#8ed300;fill-opacity:1"
id="g29"><path
inkscape:connector-curvature="0"
style="fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
id="path31"
d="m 149.937,12.356 1.239,-4.428 h 2.995 l 1.771,-6.326 h 7.338 l -1.771,6.326 h 3.753 l -1.24,4.428 h -3.753 l -2.716,9.702 c -0.416,1.483 -0.498,2.465 -0.247,2.946 0.25,0.48 0.905,0.721 1.964,0.721 l 0.549,-0.011 0.39,-0.031 -1.31,4.678 c -0.811,0.148 -1.596,0.263 -2.354,0.344 -0.758,0.081 -1.48,0.122 -2.167,0.122 -2.543,0 -4.108,-0.621 -4.695,-1.863 -0.587,-1.242 -0.313,-3.887 0.82,-7.936 l 2.428,-8.672 h -2.994 z"
clip-rule="evenodd" /></g><g
style="fill:#ffffff"
id="g33"><path
style="fill:#ffffff;fill-rule:evenodd"
inkscape:connector-curvature="0"
id="path35"
d="m 73.875,18.896 c -0.561,2.004 -0.616,3.537 -0.167,4.601 0.449,1.064 1.375,1.595 2.774,1.595 1.399,0 2.605,-0.524 3.62,-1.574 1.015,-1.05 1.806,-2.59 2.375,-4.622 0.526,-1.879 0.556,-3.334 0.09,-4.363 -0.466,-1.029 -1.393,-1.543 -2.778,-1.543 -1.304,0 -2.487,0.528 -3.551,1.585 -1.064,1.057 -1.852,2.496 -2.363,4.321 z M 96.513,0 88.024,30.321 h -7.337 l 0.824,-2.944 c -1.166,1.22 -2.369,2.121 -3.61,2.703 -1.241,0.582 -2.583,0.874 -4.025,0.874 -2.802,0 -4.772,-1.081 -5.912,-3.243 -1.139,-2.162 -1.218,-4.993 -0.238,-8.493 0.988,-3.528 2.668,-6.404 5.042,-8.627 2.374,-2.224 4.927,-3.336 7.661,-3.336 1.47,0 2.695,0.296 3.676,0.887 0.981,0.591 1.681,1.465 2.099,2.62 L 89.217,0 h 7.296 z"
clip-rule="evenodd" /><g
style="fill:#ffffff"
id="g37"><path
style="fill-rule:evenodd;fill:#ffffff"
inkscape:connector-curvature="0"
id="path39"
d="m 73.875,18.896 c -0.561,2.004 -0.616,3.537 -0.167,4.601 0.449,1.064 1.375,1.595 2.774,1.595 1.399,0 2.605,-0.524 3.62,-1.574 1.015,-1.05 1.806,-2.59 2.375,-4.622 0.526,-1.879 0.556,-3.334 0.09,-4.363 -0.466,-1.029 -1.393,-1.543 -2.778,-1.543 -1.304,0 -2.487,0.528 -3.551,1.585 -1.064,1.057 -1.852,2.496 -2.363,4.321 z M 96.513,0 88.024,30.321 h -7.337 l 0.824,-2.944 c -1.166,1.22 -2.369,2.121 -3.61,2.703 -1.241,0.582 -2.583,0.874 -4.025,0.874 -2.802,0 -4.772,-1.081 -5.912,-3.243 -1.139,-2.162 -1.218,-4.993 -0.238,-8.493 0.988,-3.528 2.668,-6.404 5.042,-8.627 2.374,-2.224 4.927,-3.336 7.661,-3.336 1.47,0 2.695,0.296 3.676,0.887 0.981,0.591 1.681,1.465 2.099,2.62 L 89.217,0 h 7.296 z"
clip-rule="evenodd" /></g></g><g
style="fill:#ffffff"
id="g41"><path
style="fill-rule:evenodd;fill:#ffffff"
inkscape:connector-curvature="0"
id="path43"
d="M 46.488,30.321 52.757,7.928 h 7.049 l -1.098,3.918 C 59.921,10.309 61.21,9.187 62.576,8.48 63.942,7.773 68.591,7.406 70.398,7.379 l -2.03,7.25 c -0.304,-0.042 -0.608,-0.072 -0.911,-0.093 -0.304,-0.02 -0.592,-0.03 -0.867,-0.03 -1.126,0 -5.167,0.168 -5.997,0.504 -0.829,0.336 -1.561,0.854 -2.196,1.555 -0.406,0.467 -0.789,1.136 -1.149,2.007 -0.361,0.872 -0.814,2.282 -1.36,4.232 l -2.104,7.516 h -7.296 z"
clip-rule="evenodd" /></g><g
style="fill:#ffffff"
id="g45"><path
style="fill:#ffffff;fill-rule:evenodd"
inkscape:connector-curvature="0"
id="path47"
d="m 32.673,16.742 8.351,-0.021 c 0.375,-1.436 0.308,-2.558 -0.201,-3.365 -0.509,-0.807 -1.402,-1.211 -2.68,-1.211 -1.209,0 -2.285,0.397 -3.229,1.19 -0.944,0.793 -1.69,1.93 -2.241,3.407 z m 6.144,6.536 h 7.043 c -1.347,2.456 -3.172,4.356 -5.477,5.7 -2.305,1.345 -4.885,2.017 -7.74,2.017 -3.473,0 -5.923,-1.054 -7.351,-3.161 -1.427,-2.107 -1.632,-4.98 -0.613,-8.618 1.038,-3.707 2.875,-6.641 5.512,-8.803 2.637,-2.163 5.678,-3.244 9.123,-3.244 3.555,0 6.04,1.099 7.456,3.298 1.417,2.198 1.582,5.234 0.498,9.109 l -0.239,0.814 -0.167,0.484 H 31.721 c -0.441,1.575 -0.438,2.777 0.01,3.606 0.448,0.829 1.332,1.244 2.65,1.244 0.975,0 1.836,-0.206 2.583,-0.617 0.747,-0.411 1.366,-1.021 1.853,-1.829 z"
clip-rule="evenodd" /><g
style="fill:#ffffff"
id="g49"><path
style="fill-rule:evenodd;fill:#ffffff"
inkscape:connector-curvature="0"
id="path51"
d="m 32.673,16.742 8.351,-0.021 c 0.375,-1.436 0.308,-2.558 -0.201,-3.365 -0.509,-0.807 -1.402,-1.211 -2.68,-1.211 -1.209,0 -2.285,0.397 -3.229,1.19 -0.944,0.793 -1.69,1.93 -2.241,3.407 z m 6.144,6.536 h 7.043 c -1.347,2.456 -3.172,4.356 -5.477,5.7 -2.305,1.345 -4.885,2.017 -7.74,2.017 -3.473,0 -5.923,-1.054 -7.351,-3.161 -1.427,-2.107 -1.632,-4.98 -0.613,-8.618 1.038,-3.707 2.875,-6.641 5.512,-8.803 2.637,-2.163 5.678,-3.244 9.123,-3.244 3.555,0 6.04,1.099 7.456,3.298 1.417,2.198 1.582,5.234 0.498,9.109 l -0.239,0.814 -0.167,0.484 H 31.721 c -0.441,1.575 -0.438,2.777 0.01,3.606 0.448,0.829 1.332,1.244 2.65,1.244 0.975,0 1.836,-0.206 2.583,-0.617 0.747,-0.411 1.366,-1.021 1.853,-1.829 z"
clip-rule="evenodd" /></g></g><g
style="fill:#8ed300;fill-opacity:1"
id="g53"><path
inkscape:connector-curvature="0"
style="fill:#8ed300;fill-opacity:1"
id="path55"
d="m 112.881,30.643 -6.404,-18.639 -6.455,18.639 h -7.254 l 9.565,-30.321 h 8.19 l 4.434,15.582 0.35,1.276 c 0.521,1.866 0.917,3.431 1.191,4.693 l 0.555,-2.182 c 0.219,-0.837 0.528,-1.935 0.925,-3.293 l 4.468,-16.076 h 8.19 l -10.501,30.321 h -7.254 z" /></g></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

141
public/statics/logo.svg Normal file
View File

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="226.229px"
height="31.038px"
viewBox="0 0 226.229 31.038"
enable-background="new 0 0 226.229 31.038"
xml:space="preserve"
id="svg2"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="logo.svg"><metadata
id="metadata61"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs59">
</defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1013"
id="namedview57"
showgrid="false"
inkscape:zoom="3.1818851"
inkscape:cx="102.98898"
inkscape:cy="24.064335"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Background">
</g>
<g
id="Guides">
</g>
<g
id="g916"><path
d="M 10.417,30.321 0,0 h 8.233 l 4.26,15.582 0.349,1.276 c 0.521,1.866 0.918,3.431 1.191,4.693 0.15,-0.618 0.335,-1.345 0.555,-2.182 0.219,-0.837 0.528,-1.935 0.925,-3.293 L 19.981,0 h 8.19 l -10.5,30.321 z"
id="path11"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill-rule:evenodd" /><path
d="m 139.809,19.787 c -0.665,0.357 -1.748,0.686 -3.25,0.988 -0.727,0.137 -1.283,0.254 -1.667,0.35 -0.95,0.247 -1.661,0.563 -2.134,0.947 -0.472,0.384 -0.799,0.899 -0.979,1.544 -0.223,0.796 -0.155,1.438 0.204,1.925 0.359,0.488 0.945,0.731 1.757,0.731 1.252,0 2.375,-0.36 3.369,-1.081 0.994,-0.721 1.653,-1.665 1.98,-2.831 z m 5.106,10.534 h -7.458 c 0.017,-0.356 0.048,-0.726 0.094,-1.11 l 0.159,-1.192 c -1.318,1.026 -2.627,1.786 -3.927,2.279 -1.299,0.493 -2.643,0.739 -4.031,0.739 -2.158,0 -3.7,-0.593 -4.625,-1.779 -0.925,-1.187 -1.106,-2.788 -0.542,-4.804 0.519,-1.851 1.431,-3.356 2.737,-4.515 1.307,-1.159 3.021,-1.972 5.142,-2.438 1.169,-0.247 2.641,-0.515 4.413,-0.803 2.646,-0.412 4.082,-1.016 4.304,-1.812 l 0.151,-0.539 c 0.182,-0.65 0.076,-1.145 -0.317,-1.483 -0.393,-0.339 -1.071,-0.508 -2.033,-0.508 -1.045,0 -1.934,0.214 -2.666,0.643 -0.731,0.428 -1.289,1.058 -1.673,1.887 h -6.748 c 1.065,-2.53 2.64,-4.413 4.723,-5.65 2.083,-1.237 4.724,-1.856 7.923,-1.856 1.991,0 3.602,0.241 4.833,0.722 1.231,0.481 2.095,1.209 2.59,2.185 0.339,0.701 0.483,1.536 0.432,2.504 -0.052,0.969 -0.377,2.525 -0.978,4.669 l -2.375,8.483 c -0.284,1.014 -0.416,1.812 -0.396,2.395 0.02,0.583 0.188,0.962 0.503,1.141 z"
id="path15"
style="clip-rule:evenodd;fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
inkscape:connector-curvature="0" /><path
d="m 185.7,30.321 6.27,-22.393 h 7.049 l -1.097,3.918 c 1.213,-1.537 2.502,-2.659 3.867,-3.366 1.365,-0.707 2.951,-1.074 4.758,-1.101 l -2.03,7.25 c -0.304,-0.042 -0.608,-0.072 -0.912,-0.093 -0.303,-0.02 -0.592,-0.03 -0.867,-0.03 -1.126,0 -2.104,0.168 -2.932,0.504 -0.829,0.336 -1.561,0.854 -2.197,1.555 -0.406,0.467 -0.789,1.136 -1.149,2.007 -0.361,0.872 -0.814,2.282 -1.359,4.232 l -2.104,7.516 H 185.7 Z"
id="path19"
style="clip-rule:evenodd;fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
inkscape:connector-curvature="0" /><path
d="m 217.631,19.787 c -0.664,0.357 -1.748,0.686 -3.25,0.988 -0.727,0.137 -1.282,0.254 -1.667,0.35 -0.95,0.247 -1.661,0.563 -2.134,0.947 -0.472,0.384 -0.799,0.899 -0.979,1.544 -0.223,0.796 -0.155,1.438 0.205,1.925 0.359,0.488 0.945,0.731 1.757,0.731 1.252,0 2.375,-0.36 3.369,-1.081 0.994,-0.721 1.654,-1.665 1.98,-2.831 z m 5.106,10.534 h -7.458 c 0.017,-0.356 0.048,-0.726 0.094,-1.11 l 0.159,-1.192 c -1.318,1.026 -2.627,1.786 -3.927,2.279 -1.299,0.493 -2.643,0.739 -4.031,0.739 -2.158,0 -3.7,-0.593 -4.625,-1.779 -0.926,-1.187 -1.106,-2.788 -0.542,-4.804 0.519,-1.851 1.431,-3.356 2.737,-4.515 1.306,-1.159 3.02,-1.972 5.142,-2.438 1.169,-0.247 2.641,-0.515 4.413,-0.803 2.647,-0.412 4.082,-1.016 4.304,-1.812 l 0.151,-0.539 c 0.182,-0.65 0.077,-1.145 -0.317,-1.483 -0.393,-0.339 -1.071,-0.508 -2.033,-0.508 -1.045,0 -1.934,0.214 -2.666,0.643 -0.731,0.428 -1.289,1.058 -1.672,1.887 h -6.748 c 1.065,-2.53 2.64,-4.413 4.723,-5.65 2.083,-1.237 4.724,-1.856 7.923,-1.856 1.99,0 3.601,0.241 4.833,0.722 1.232,0.481 2.095,1.209 2.591,2.185 0.339,0.701 0.483,1.536 0.431,2.504 -0.051,0.969 -0.377,2.525 -0.978,4.669 l -2.375,8.483 c -0.284,1.014 -0.416,1.812 -0.396,2.395 0.02,0.583 0.188,0.962 0.503,1.141 z"
id="path23"
style="clip-rule:evenodd;fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
inkscape:connector-curvature="0" /><path
d="m 188.386,7.928 -6.269,22.393 h -7.174 l 0.864,-3.085 c -1.227,1.246 -2.476,2.163 -3.746,2.751 -1.27,0.588 -2.625,0.882 -4.067,0.882 -2.471,0 -4.154,-0.634 -5.048,-1.901 -0.895,-1.268 -0.993,-3.149 -0.294,-5.644 l 4.31,-15.396 h 7.338 l -3.508,12.53 c -0.516,1.842 -0.641,3.109 -0.375,3.803 0.266,0.694 0.967,1.041 2.105,1.041 1.275,0 2.323,-0.422 3.142,-1.267 0.819,-0.845 1.497,-2.223 2.031,-4.133 l 3.353,-11.974 z"
id="path27"
style="clip-rule:evenodd;fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
inkscape:connector-curvature="0" /><path
d="m 149.937,12.356 1.239,-4.428 h 2.995 l 1.771,-6.326 h 7.338 l -1.771,6.326 h 3.753 l -1.24,4.428 h -3.753 l -2.716,9.702 c -0.416,1.483 -0.498,2.465 -0.247,2.946 0.25,0.48 0.905,0.721 1.964,0.721 l 0.549,-0.011 0.39,-0.031 -1.31,4.678 c -0.811,0.148 -1.596,0.263 -2.354,0.344 -0.758,0.081 -1.48,0.122 -2.167,0.122 -2.543,0 -4.108,-0.621 -4.695,-1.863 -0.587,-1.242 -0.313,-3.887 0.82,-7.936 l 2.428,-8.672 z"
id="path31"
style="clip-rule:evenodd;fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
inkscape:connector-curvature="0" /><path
d="m 73.875,18.896 c -0.561,2.004 -0.616,3.537 -0.167,4.601 0.449,1.064 1.375,1.595 2.774,1.595 1.399,0 2.605,-0.524 3.62,-1.574 1.015,-1.05 1.806,-2.59 2.375,-4.622 0.526,-1.879 0.556,-3.334 0.09,-4.363 -0.466,-1.029 -1.393,-1.543 -2.778,-1.543 -1.304,0 -2.487,0.528 -3.551,1.585 -1.064,1.057 -1.852,2.496 -2.363,4.321 z M 96.513,0 88.024,30.321 h -7.337 l 0.824,-2.944 c -1.166,1.22 -2.369,2.121 -3.61,2.703 -1.241,0.582 -2.583,0.874 -4.025,0.874 -2.802,0 -4.772,-1.081 -5.912,-3.243 -1.139,-2.162 -1.218,-4.993 -0.238,-8.493 0.988,-3.528 2.668,-6.404 5.042,-8.627 2.374,-2.224 4.927,-3.336 7.661,-3.336 1.47,0 2.695,0.296 3.676,0.887 0.981,0.591 1.681,1.465 2.099,2.62 L 89.217,0 Z"
id="path35"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd" /><path
style="clip-rule:evenodd;fill-rule:evenodd"
inkscape:connector-curvature="0"
id="path39"
d="m 73.875,18.896 c -0.561,2.004 -0.616,3.537 -0.167,4.601 0.449,1.064 1.375,1.595 2.774,1.595 1.399,0 2.605,-0.524 3.62,-1.574 1.015,-1.05 1.806,-2.59 2.375,-4.622 0.526,-1.879 0.556,-3.334 0.09,-4.363 -0.466,-1.029 -1.393,-1.543 -2.778,-1.543 -1.304,0 -2.487,0.528 -3.551,1.585 -1.064,1.057 -1.852,2.496 -2.363,4.321 z M 96.513,0 88.024,30.321 h -7.337 l 0.824,-2.944 c -1.166,1.22 -2.369,2.121 -3.61,2.703 -1.241,0.582 -2.583,0.874 -4.025,0.874 -2.802,0 -4.772,-1.081 -5.912,-3.243 -1.139,-2.162 -1.218,-4.993 -0.238,-8.493 0.988,-3.528 2.668,-6.404 5.042,-8.627 2.374,-2.224 4.927,-3.336 7.661,-3.336 1.47,0 2.695,0.296 3.676,0.887 0.981,0.591 1.681,1.465 2.099,2.62 L 89.217,0 Z" /><path
d="M 46.488,30.321 52.757,7.928 h 7.049 l -1.098,3.918 C 59.921,10.309 61.21,9.187 62.576,8.48 63.942,7.773 68.591,7.406 70.398,7.379 l -2.03,7.25 c -0.304,-0.042 -0.608,-0.072 -0.911,-0.093 -0.304,-0.02 -0.592,-0.03 -0.867,-0.03 -1.126,0 -5.167,0.168 -5.997,0.504 -0.829,0.336 -1.561,0.854 -2.196,1.555 -0.406,0.467 -0.789,1.136 -1.149,2.007 -0.361,0.872 -0.814,2.282 -1.36,4.232 l -2.104,7.516 h -7.296 z"
id="path43"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill-rule:evenodd" /><path
d="m 32.673,16.742 8.351,-0.021 c 0.375,-1.436 0.308,-2.558 -0.201,-3.365 -0.509,-0.807 -1.402,-1.211 -2.68,-1.211 -1.209,0 -2.285,0.397 -3.229,1.19 -0.944,0.793 -1.69,1.93 -2.241,3.407 z m 6.144,6.536 h 7.043 c -1.347,2.456 -3.172,4.356 -5.477,5.7 -2.305,1.345 -4.885,2.017 -7.74,2.017 -3.473,0 -5.923,-1.054 -7.351,-3.161 -1.427,-2.107 -1.632,-4.98 -0.613,-8.618 1.038,-3.707 2.875,-6.641 5.512,-8.803 2.637,-2.163 5.678,-3.244 9.123,-3.244 3.555,0 6.04,1.099 7.456,3.298 1.417,2.198 1.582,5.234 0.498,9.109 l -0.239,0.814 -0.167,0.484 H 31.721 c -0.441,1.575 -0.438,2.777 0.01,3.606 0.448,0.829 1.332,1.244 2.65,1.244 0.975,0 1.836,-0.206 2.583,-0.617 0.747,-0.411 1.366,-1.021 1.853,-1.829 z"
id="path47"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd" /><path
style="clip-rule:evenodd;fill-rule:evenodd"
inkscape:connector-curvature="0"
id="path51"
d="m 32.673,16.742 8.351,-0.021 c 0.375,-1.436 0.308,-2.558 -0.201,-3.365 -0.509,-0.807 -1.402,-1.211 -2.68,-1.211 -1.209,0 -2.285,0.397 -3.229,1.19 -0.944,0.793 -1.69,1.93 -2.241,3.407 z m 6.144,6.536 h 7.043 c -1.347,2.456 -3.172,4.356 -5.477,5.7 -2.305,1.345 -4.885,2.017 -7.74,2.017 -3.473,0 -5.923,-1.054 -7.351,-3.161 -1.427,-2.107 -1.632,-4.98 -0.613,-8.618 1.038,-3.707 2.875,-6.641 5.512,-8.803 2.637,-2.163 5.678,-3.244 9.123,-3.244 3.555,0 6.04,1.099 7.456,3.298 1.417,2.198 1.582,5.234 0.498,9.109 l -0.239,0.814 -0.167,0.484 H 31.721 c -0.441,1.575 -0.438,2.777 0.01,3.606 0.448,0.829 1.332,1.244 2.65,1.244 0.975,0 1.836,-0.206 2.583,-0.617 0.747,-0.411 1.366,-1.021 1.853,-1.829 z" /><path
d="m 112.881,30.643 -6.404,-18.639 -6.455,18.639 h -7.254 l 9.565,-30.321 h 8.19 l 4.434,15.582 0.35,1.276 c 0.521,1.866 0.917,3.431 1.191,4.693 l 0.555,-2.182 c 0.219,-0.837 0.528,-1.935 0.925,-3.293 l 4.468,-16.076 h 8.19 l -10.501,30.321 z"
id="path55"
style="fill:#8ed300;fill-opacity:1"
inkscape:connector-curvature="0" /></g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

256
quasar.config.js Normal file
View File

@ -0,0 +1,256 @@
/* eslint-env node */
/*
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
* the ES6 features that are supported by your Node version. https://node.green/
*/
// Configuration for your app
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js
const ESLintPlugin = require('eslint-webpack-plugin');
const path = require('path');
const { configure } = require('quasar/wrappers');
module.exports = configure(function (ctx) {
return {
// https://v2.quasar.dev/quasar-cli-webpack/supporting-ts
supportTS: false,
// https://v2.quasar.dev/quasar-cli-webpack/prefetch-feature
// preFetch: true,
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-webpack/boot-files
boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'],
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
css: ['app.scss', 'width.scss', 'responsive.scss'],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
// 'mdi-v5',
// '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-webpack/quasar-config-js#Property%3A-build
build: {
vueRouterMode: 'hash', // available values: 'hash', 'history'
// transpile: false,
// publicPath: '/',
// Add dependencies for transpiling with Babel (Array of string/regex)
// (from node_modules, which are by default not transpiled).
// Applies only if "transpile" is set to true.
// transpileDependencies: [],
// rtl: true, // https://quasar.dev/options/rtl-support
// preloadChunks: true,
// showProgress: false,
// gzip: true,
// analyze: true,
// Options below are automatically set depending on the env, set them if you want to override
// extractCSS: false,
// https://v2.quasar.dev/quasar-cli-webpack/handling-webpack
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpack(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]);
chain.module
.rule('i18n-resource')
.test(/\.(json5?|ya?ml)$/)
.include.add(path.resolve(__dirname, './src/i18n'))
.end()
.type('javascript/auto')
.use('i18n-resource')
.loader('@intlify/vue-i18n-loader');
chain.module
.rule('i18n')
.resourceQuery(/blockType=i18n/)
.type('javascript/auto')
.use('i18n')
.loader('@intlify/vue-i18n-loader');
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-devServer
devServer: {
server: {
type: 'http'
},
port: 8080,
open: false,
// static: __dirname,
headers: { 'Access-Control-Allow-Origin': '*' },
// stats: { chunks: false },
proxy: {
'/api': 'http://localhost:3000',
'/': {
target: 'http://localhost:3002',
bypass: req => (req.path !== '/' ? req.path : null)
}
}
},
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework
framework: {
config: {},
autoImportComponentCase: 'pascal',
// 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: ['Notify', 'Dialog']
},
// animations: 'all', // --- includes all animations
// https://quasar.dev/options/animations
animations: [],
// https://v2.quasar.dev/quasar-cli-webpack/developing-ssr/configuring-ssr
ssr: {
pwa: false,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
maxAge: 1000 * 60 * 60 * 24 * 30,
// Tell browser when a file from the server should expire from cache (in ms)
chainWebpackWebserver(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
middlewares: [
ctx.prod ? 'compression' : '',
'render' // keep this as last one
]
},
// https://v2.quasar.dev/quasar-cli-webpack/developing-pwa/configuring-pwa
pwa: {
workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
workboxOptions: {}, // only for GenerateSW
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
// if using workbox in InjectManifest mode
chainWebpackCustomSW(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
manifest: {
name: 'Hedera',
short_name: 'Hedera',
description: "Verdnatura's webshop",
display: 'standalone',
orientation: 'portrait',
background_color: '#ffffff',
theme_color: '#027be3',
icons: [
{
src: 'icons/icon-128x128.png',
sizes: '128x128',
type: 'image/png'
},
{
src: 'icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'icons/icon-256x256.png',
sizes: '256x256',
type: 'image/png'
},
{
src: 'icons/icon-384x384.png',
sizes: '384x384',
type: 'image/png'
},
{
src: 'icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/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-webpack/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-electron-apps/configuring-electron
electron: {
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: 'hedera-web'
},
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpackMain(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
chainWebpackPreload(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
}
}
};
});

11
src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup>
import { useAppStore } from 'stores/app';
import { onBeforeMount } from 'vue';
const appStore = useAppStore();
onBeforeMount(() => appStore.init());
</script>
<template>
<router-view />
</template>

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

10
src/boot/app.js Normal file
View File

@ -0,0 +1,10 @@
import { boot } from 'quasar/wrappers';
import { useAppStore } from 'stores/app';
import { useUserStore } from 'stores/user';
export default boot(({ app }) => {
const props = app.config.globalProperties;
const userStore = useUserStore();
props.$app = useAppStore();
props.$user = userStore.user;
});

67
src/boot/axios.js Normal file
View File

@ -0,0 +1,67 @@
import { boot } from 'quasar/wrappers';
import { Connection } from '../js/db/connection';
import { useUserStore } from 'stores/user';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const { notify } = useNotify();
// Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here;
// If any client changes this (global) instance, it might be a
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
const api = axios.create({
baseURL: `//${location.hostname}:${location.port}/api/`
});
const jApi = new Connection();
const onRequestError = error => {
return Promise.reject(error);
};
const onResponseError = error => {
let message = error.message;
const response = error.response;
const responseData = response && response.data;
const responseError = responseData && response.data.error;
if (responseError) {
message = responseError.message;
}
notify(message, 'negative');
return Promise.reject(error);
};
export default boot(({ app }) => {
const userStore = useUserStore();
function addToken(config) {
if (userStore.token) {
config.headers.Authorization = userStore.token;
}
return config;
}
api.interceptors.request.use(addToken, onRequestError);
api.interceptors.response.use(response => response, onResponseError);
jApi.use(addToken);
jApi.useErrorInterceptor(onResponseError);
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios;
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api;
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
app.config.globalProperties.$jApi = jApi;
app.provide('jApi', jApi);
app.provide('api', api);
});
export { api, jApi };

66
src/boot/error-handler.js Normal file
View File

@ -0,0 +1,66 @@
export default async ({ app }) => {
/*
window.addEventListener('error',
e => onWindowError(e));
window.addEventListener('unhandledrejection',
e => onWindowRejection(e));
,onWindowError(event) {
errorHandler(event.error);
}
,onWindowRejection(event) {
errorHandler(event.reason);
}
*/
app.config.errorHandler = (err, vm, info) => {
errorHandler(err, vm)
}
function errorHandler (err, vm) {
let message
let tMessage
let res = err.response
// XXX: Compatibility with old JSON service
if (err.name === 'JsonException') {
res = {
status: err.statusCode,
data: { error: { message: err.message } }
}
}
if (res) {
const status = res.status
if (status >= 400 && status < 500) {
switch (status) {
case 401:
tMessage = 'loginFailed'
break
case 403:
tMessage = 'authenticationRequired'
vm.$router.push('/login')
break
case 404:
tMessage = 'notFound'
break
default:
message = res.data.error.message
}
} else if (status >= 500) {
tMessage = 'internalServerError'
}
} else {
tMessage = 'somethingWentWrong'
console.error(err)
}
if (tMessage) {
message = vm.$t(tMessage)
}
vm.$q.notify({
message,
type: 'negative'
})
}
}

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

@ -0,0 +1,22 @@
import { boot } from 'quasar/wrappers';
import { createI18n } from 'vue-i18n';
import messages from 'src/i18n';
const i18n = createI18n({
locale: navigator.language || navigator.userLanguage,
fallbackLocale: 'en-US',
globalInjection: true,
missingWarn: false,
fallbackWarn: false,
legacy: false,
silentTranslationWarn: true,
silentFallbackWarn: true,
messages
});
export default boot(({ app }) => {
// Set i18n instance on app
app.use(i18n);
});
export { i18n };

20
src/boot/vnDate.js Normal file
View File

@ -0,0 +1,20 @@
import { boot } from 'quasar/wrappers';
export default boot(() => {
Date.vnUTC = () => {
const env = process.env.NODE_ENV;
if (!env || env === 'development') {
return new Date(Date.UTC(2001, 0, 1, 11));
}
return new Date();
};
Date.vnNew = () => {
return new Date(Date.vnUTC());
};
Date.vnNow = () => {
return new Date(Date.vnUTC()).getTime();
};
});

View File

@ -0,0 +1,286 @@
<script setup>
import { ref, inject, onMounted, computed, Teleport } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
import useNotify from 'src/composables/useNotify.js';
import {
generateUpdateSqlQuery,
generateInsertSqlQuery
} from 'src/js/db/sqlService.js';
const props = defineProps({
title: {
type: String,
default: ''
},
table: {
type: String,
default: ''
},
schema: {
type: String,
default: ''
},
// Objeto que define las pks de la tabla. Usado para generar las queries sql correspondientes.
// Debe ser definido como un objeto de pares key-value, donde la clave es el nombre de la columna de la pk.
pks: {
type: Object,
default: () => {}
},
createModelDefault: {
type: Object,
default: () => ({
field: '',
value: ''
})
},
// Objeto que contiene la consulta SQL y los parámetros necesarios para obtener los datos iniciales del formulario.
// `query` debe ser una cadena de texto que representa la consulta SQL.
// `params` es un objeto que mapea los parámetros de la consulta a sus valores.
fetchFormDataSql: {
type: Object,
default: () => ({
query: '',
params: {}
})
},
// Objeto con los datos iniciales del form, si este objeto es definido, no se ejecuta la query fetchFormDataSql
formInitialData: {
type: Object,
default: () => {}
},
// Array de columnas que no se deben actualizar
columnsToIgnoreUpdate: {
type: Array,
default: () => []
},
autoLoad: {
type: Boolean,
default: true
},
isEditMode: {
type: Boolean,
default: true
},
defaultActions: {
type: Boolean,
default: true
},
showBottomActions: {
type: Boolean,
default: false
},
saveFn: {
type: Function,
default: null
},
separationBetweenInputs: {
type: String,
default: 'xs'
}
});
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const jApi = inject('jApi');
const { notify } = useNotify();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const loading = ref(false);
const formData = ref({});
const addressFormRef = ref(null);
const modelInfo = ref(null);
// Array de nombre de columnas de la tabla
const tableColumns = computed(
() => modelInfo.value?.columns.map(col => col.name) || []
);
// Array de nombre de columnas que fueron actualizadas y no estan en columnsToIgnoreUpdate
const updatedColumns = computed(() => {
return tableColumns.value.filter(
colName =>
modelInfo.value?.data[0][colName] !== formData.value[colName] &&
!props.columnsToIgnoreUpdate.includes(colName)
);
});
const hasChanges = computed(() => !!updatedColumns.value.length);
const separationBetweenInputs = computed(() => {
return `q-gutter-y-${props.separationBetweenInputs}`;
});
const fetchFormData = async () => {
if (!props.fetchFormDataSql.query) return;
loading.value = true;
const { results } = await jApi.execQuery(
props.fetchFormDataSql.query,
props.fetchFormDataSql.params
);
modelInfo.value = results[0];
if (!modelInfo.value.data[0]) {
modelInfo.value.data[0] = {};
// Si no existen datos iniciales, se inicializan con null, en base a las columnas de la tabla
modelInfo.value.columns.forEach(
col => (modelInfo.value.data[0][col.name] = null)
);
}
formData.value = { ...modelInfo.value.data[0] };
loading.value = false;
};
const onSubmitSuccess = () => {
emit('onDataSaved');
notify(t('dataSaved'), 'positive');
};
const submit = async () => {
try {
if (props.saveFn) {
await props.saveFn(formData.value);
} else {
if (!hasChanges.value) {
return;
}
const sqlQuery = generateSqlQuery();
await jApi.execQuery(sqlQuery, props.pks);
modelInfo.value.data[0] = { ...formData.value };
}
onSubmitSuccess();
} catch (error) {
console.error('Error:', error);
}
};
const generateSqlQuery = () => {
if (props.isEditMode) {
return generateUpdateSqlQuery(
props.schema,
props.table,
props.pks,
updatedColumns.value,
formData.value
);
} else {
return generateInsertSqlQuery(
props.schema,
props.table,
formData.value,
updatedColumns.value,
props.createModelDefault
);
}
};
onMounted(async () => {
if (!props.formInitialData) {
fetchFormData();
} else {
formData.value = { ...props.formInitialData };
// Como no se ejecuta la query fetchFormData y no se obtienen las columnas de la tabla, se inicializan con las keys del objeto formInitialData
modelInfo.value = {
columns: Object.keys(props.formInitialData).map(col => ({
name: col
})),
data: [props.formInitialData]
};
}
});
defineExpose({
formData,
submit
});
</script>
<template>
<QCard
class="form-container"
v-bind="$attrs"
>
<QForm
v-if="!loading"
ref="addressFormRef"
class="form"
:class="separationBetweenInputs"
>
<span v-if="title" class="text-h6 text-bold">
{{ title }}
</span>
<slot
name="form"
:data="formData"
/>
<slot
name="extraForm"
:data="formData"
/>
<component
v-if="isHeaderMounted"
:is="showBottomActions ? 'div' : Teleport"
to="#actions"
class="flex row justify-end q-gutter-x-sm"
:class="{ 'q-mt-md': showBottomActions }"
>
<QBtn
v-if="defaultActions && showBottomActions"
:label="t('cancel')"
:icon="showBottomActions ? undefined : 'check'"
rounded
no-caps
flat
v-close-popup
>
<QTooltip>{{ t('cancel') }}</QTooltip>
</QBtn>
<QBtn
v-if="defaultActions"
:label="t('save')"
:icon="showBottomActions ? undefined : 'check'"
rounded
no-caps
flat
:disabled="!showBottomActions && !updatedColumns.length"
@click="submit()"
>
<QTooltip>{{ t('save') }}</QTooltip>
</QBtn>
<slot name="actions" :data="formData" />
</component>
</QForm>
<QSpinner
v-else
color="primary"
size="3em"
:thickness="2"
/>
</QCard>
</template>
<style lang="scss" scoped>
.no-form-container {
padding: 0 !important;
box-shadow: none;
border: none;
}
.form-container {
width: 100%;
height: max-content;
padding: 0 !important;
max-width: 544px;
display: flex;
justify-content: center;
}
.form {
display: flex;
flex-direction: column;
padding: 32px;
width: 100%;
}
</style>

View File

@ -0,0 +1,139 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const emit = defineEmits([
'update:modelValue',
'update:options',
'keyup.enter',
'remove'
]);
const props = defineProps({
modelValue: {
type: [String, Number],
default: null
},
isOutlined: {
type: Boolean,
default: false
},
info: {
type: String,
default: ''
},
clearable: {
type: Boolean,
default: true
}
});
const { t } = useI18n();
const requiredFieldRule = val => !!val || t('globals.fieldRequired');
const vnInputRef = ref(null);
const value = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
}
});
const hover = ref(false);
const styleAttrs = computed(() => {
return props.isOutlined
? { dense: true, outlined: true, rounded: true }
: {};
});
const focus = () => {
vnInputRef.value.focus();
};
defineExpose({
focus
});
const inputRules = [
val => {
const { min, max } = vnInputRef.value.$attrs;
if (min >= 0) {
if (Math.floor(val) < min) return t('inputMin', { value: min });
}
if (max > 0) {
if (Math.floor(val) > max) return t('inputMax', { value: max });
}
}
];
</script>
<template>
<div
:rules="$attrs.required ? [requiredFieldRule] : null"
@mouseover="hover = true"
@mouseleave="hover = false"
>
<QInput
ref="vnInputRef"
v-model="value"
v-bind="{ ...$attrs, ...styleAttrs }"
:type="$attrs.type"
:class="{ required: $attrs.required }"
:clearable="false"
:rules="inputRules"
:lazy-rules="true"
hide-bottom-space
@keyup.enter="emit('keyup.enter')"
>
<template
v-if="$slots.prepend"
#prepend
>
<slot name="prepend" />
</template>
<template #append>
<slot
v-if="$slots.append && !$attrs.disabled"
name="append"
/>
<QIcon
v-if="hover && value && !$attrs.disabled && props.clearable"
name="close"
size="xs"
@click="
() => {
value = null;
emit('remove');
}
"
/>
<QIcon
v-if="info"
name="info"
>
<QTooltip max-width="350px">
{{ info }}
</QTooltip>
</QIcon>
</template>
</QInput>
</div>
</template>
<i18n lang="yaml">
en-US:
inputMin: Must be more than {value}
inputMax: Must be less than {value}
es-ES:
inputMin: Debe ser mayor a {value}
inputMax: Debe ser menor a {value}
ca-ES:
inputMin: Ha de ser més gran que {value}
inputMax: Ha de ser menys que {value}
fr-FR:
inputMin: Doit être supérieur à {value}
inputMax: Doit être supérieur à {value}
pt-PT:
inputMin: Deve ser maior que {value}
inputMax: Deve ser maior que {value}
</i18n>

View File

@ -0,0 +1,174 @@
<script setup>
import { onMounted, watch, computed, ref } from 'vue';
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
const model = defineModel({ type: String });
const props = defineProps({
isOutlined: {
type: Boolean,
default: false
}
});
const { t } = useI18n();
const requiredFieldRule = val => !!val || t('globals.fieldRequired');
const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref();
const hover = ref();
const mask = ref();
onMounted(() => {
// fix quasar bug
mask.value = '##/##/####';
});
const styleAttrs = computed(() => {
return props.isOutlined
? {
dense: true,
outlined: true,
rounded: true
}
: {};
});
const formattedDate = computed({
get() {
if (!model.value) return model.value;
return date.formatDate(new Date(model.value), dateFormat);
},
set(value) {
if (value === model.value) return;
let newDate;
if (value) {
// parse input
if (value.includes('/')) {
if (value.length === 6) {
value = value + new Date().getFullYear();
}
if (value.length >= 10) {
if (value.at(2) === '/') {
value = value.split('/').reverse().join('/');
}
value = date.formatDate(
new Date(value).toISOString(),
'YYYY-MM-DDTHH:mm:ss.SSSZ'
);
}
}
const [year, month, day] = value.split('-').map(e => parseInt(e));
newDate = new Date(year, month - 1, day);
if (model.value) {
const orgDate =
model.value instanceof Date
? model.value
: new Date(model.value);
newDate.setHours(
orgDate.getHours(),
orgDate.getMinutes(),
orgDate.getSeconds(),
orgDate.getMilliseconds()
);
}
}
if (!isNaN(newDate)) model.value = newDate.toISOString();
}
});
const popupDate = computed(() =>
model.value
? date.formatDate(new Date(model.value), 'YYYY/MM/DD')
: model.value
);
watch(
() => model.value,
val => (formattedDate.value = val),
{ immediate: true }
);
</script>
<template>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
v-model="formattedDate"
class="vn-input-date"
:mask="mask"
placeholder="dd/mm/aaaa"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: $attrs.required }"
:rules="$attrs.required ? [requiredFieldRule] : null"
:clearable="false"
>
<template #append>
<QIcon
name="close"
size="xs"
v-if="
($attrs.clearable == undefined || $attrs.clearable) &&
hover &&
model &&
!$attrs.disable
"
@click="
model = null;
isPopupOpen = false;
"
/>
<QIcon
name="event"
class="cursor-pointer"
@click="isPopupOpen = !isPopupOpen"
:title="t('openDate')"
/>
</template>
<QMenu
transition-show="scale"
transition-hide="scale"
v-model="isPopupOpen"
anchor="bottom left"
self="top start"
:no-focus="true"
:no-parent-event="true"
>
<QDate
v-model="popupDate"
:landscape="true"
:today-btn="true"
color="accent"
@update:model-value="
date => {
formattedDate = date;
isPopupOpen = false;
}
"
/>
</QMenu>
</QInput>
</div>
</template>
<style lang="scss">
.vn-input-date.q-field--standard.q-field--readonly .q-field__control:before {
border-bottom-style: solid;
}
.vn-input-date.q-field--outlined.q-field--readonly .q-field__control:before {
border-style: solid;
}
</style>
<i18n lang="yaml">
en-US:
openDate: Open date
es-ES:
openDate: Abrir fecha
ca-ES:
openDate: Obrir data
fr-FR:
openDate: Ouvrir la date
pt-PT:
openDate: Abrir data
</i18n>

View File

@ -0,0 +1,195 @@
<script setup>
import { ref, toRefs, computed, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
const emit = defineEmits(['update:modelValue', 'update:options']);
const $props = defineProps({
modelValue: {
type: [String, Number, Object],
default: null
},
options: {
type: Array,
default: () => []
},
optionLabel: {
type: [String],
default: 'name'
},
optionValue: {
type: String,
default: 'id'
},
optionFilter: {
type: String,
default: null
},
dataQuery: {
type: String,
default: ''
},
filterOptions: {
type: [Array],
default: () => []
},
isClearable: {
type: Boolean,
default: true
},
defaultFilter: {
type: Boolean,
default: true
},
fields: {
type: Array,
default: null
},
where: {
type: Object,
default: null
},
sortBy: {
type: String,
default: null
},
limit: {
type: [Number, String],
default: '30'
},
focusOnMount: {
type: Boolean,
default: false
},
useLike: {
type: Boolean,
default: true
}
});
const { t } = useI18n();
const requiredFieldRule = val => val ?? t('globals.fieldRequired');
const { optionLabel, optionValue, options } = toRefs($props);
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const vnSelectRef = ref();
const lastVal = ref();
const value = computed({
get() {
return $props.modelValue;
},
set(value) {
emit('update:modelValue', value);
}
});
watch(options, newValue => {
setOptions(newValue);
});
onMounted(() => {
setOptions(options.value);
if ($props.focusOnMount) {
setTimeout(() => vnSelectRef.value.showPopup(), 300);
}
});
function setOptions(data) {
myOptions.value = JSON.parse(JSON.stringify(data));
myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
}
function filter(val, options) {
const search = val.toString().toLowerCase();
if (!search) return options;
return options.filter(row => {
if ($props.filterOptions.length) {
return $props.filterOptions.some(prop => {
const propValue = String(row[prop]).toLowerCase();
return propValue.includes(search);
});
}
const id = row.id;
const optionLabel = String(row[$props.optionLabel]).toLowerCase();
return id === search || optionLabel.includes(search);
});
}
async function filterHandler(val, update) {
if (!val && lastVal.value === val) {
lastVal.value = val;
return update();
}
lastVal.value = val;
if (!$props.defaultFilter) return update();
const newOptions = filter(val, myOptionsOriginal.value);
update(
() => {
myOptions.value = newOptions;
},
ref => {
if (val !== '' && ref.options.length > 0) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
}
);
}
</script>
<template>
<QSelect
v-model="value"
:options="myOptions"
:option-label="optionLabel"
:option-value="optionValue"
v-bind="$attrs"
emit-value
map-options
use-input
@filter="filterHandler"
hide-selected
fill-input
ref="vnSelectRef"
lazy-rules
:class="{ required: $attrs.required }"
:rules="$attrs.required ? [requiredFieldRule] : null"
virtual-scroll-slice-size="options.length"
>
<template
v-if="isClearable"
#append
>
<QIcon
v-show="value"
name="close"
@click.stop="value = null"
class="cursor-pointer"
size="xs"
/>
</template>
<template
v-for="(_, slotName) in $slots"
#[slotName]="slotData"
:key="slotName"
>
<slot
:name="slotName"
v-bind="slotData ?? {}"
:key="slotName"
/>
</template>
</QSelect>
</template>
<style scoped lang="scss">
.q-field--outlined {
max-width: 100%;
}
</style>

View File

@ -0,0 +1,59 @@
<script setup>
const props = defineProps({
clickable: { type: Boolean, default: true },
rounded: { type: Boolean, default: true }
});
const emit = defineEmits(['click']);
const handleClick = () => {
if (props.clickable) {
emit('click');
}
};
</script>
<template>
<QItem
v-bind="$attrs"
v-ripple="clickable"
:clickable="clickable"
class="full-width row items-center justify-between card no-border-radius bg-white"
:class="{ 'cursor-pointer': clickable, 'no-radius': !rounded }"
@click="handleClick()"
>
<div class="no-padding content-container col-10">
<slot name="prepend" />
<div class="content">
<slot name="content" />
</div>
</div>
<div class="no-padding flex full-width justify-center">
<slot name="actions" />
</div>
</QItem>
</template>
<style lang="scss" scoped>
.card {
border-bottom: 1px solid $gray-light;
padding: 20px;
}
.content {
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
* {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.content-container {
display: flex;
}
</style>

View File

@ -0,0 +1,287 @@
<script setup>
import { ref, inject, onMounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import VnForm from 'src/components/common/VnForm.vue';
import { useUserStore } from 'stores/user';
import useNotify from 'src/composables/useNotify.js';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const props = defineProps({
verificationToken: {
type: String,
default: ''
}
});
const emit = defineEmits(['onPasswordChanged']);
const showOldPwd = ref(false);
const showNewPwd = ref(false);
const showCopyPwd = ref(false);
const { t } = useI18n();
const api = inject('api');
const userStore = useUserStore();
const { notify } = useNotify();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const oldPasswordRef = ref(null);
const newPasswordRef = ref(null);
const passwordRequirementsDialogRef = ref(null);
const vnFormRef = ref(null);
const repeatPassword = ref('');
const passwordRequirements = ref(null);
const formData = ref({
userId: userStore?.user?.id,
oldPassword: '',
newPassword: ''
});
const changePassword = async () => {
if (!formData.value.newPassword || !repeatPassword.value) {
notify(t('passwordEmpty'), 'negative');
throw new Error('Password empty');
}
if (formData.value.newPassword !== repeatPassword.value) {
notify(t('passwordsDoNotMatch'), 'negative');
throw new Error('Passwords do not match');
}
if (props.verificationToken) {
await changePasswordWithToken();
} else {
await changePasswordWithoutToken();
}
};
const changePasswordWithToken = async () => {
const headers = {
Authorization: props.verificationToken
};
await api.post('VnUsers/reset-password', formData.value, { headers });
};
const changePasswordWithoutToken = async () => {
await api.patch('Accounts/change-password', formData.value);
};
const getPasswordRequirements = async () => {
try {
const { data } = await api.get('UserPasswords/findOne');
passwordRequirements.value = data;
} catch (error) {
console.error(error);
}
};
const login = async () => {
await userStore.login(userStore.user.name, formData.value.newPassword);
};
const onPasswordChanged = async () => {
await login();
emit('onPasswordChanged');
};
onMounted(async () => {
getPasswordRequirements();
await nextTick();
if (props.verificationToken) {
newPasswordRef.value.focus();
} else {
oldPasswordRef.value.focus();
}
});
</script>
<template>
<VnForm
ref="vnFormRef"
:title="t('changePassword')"
:form-initial-data="formData"
:save-fn="changePassword"
show-bottom-actions
:default-actions="false"
style="max-width: 300px"
@on-data-saved="onPasswordChanged()"
>
<template #form>
<VnInput
v-if="!verificationToken"
ref="oldPasswordRef"
v-model="formData.oldPassword"
:type="!showOldPwd ? 'password' : 'text'"
:label="t('oldPassword')"
>
<template #append>
<QIcon
:name="showOldPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showOldPwd = !showOldPwd"
/>
</template>
</VnInput>
<VnInput
ref="newPasswordRef"
v-model="formData.newPassword"
:type="!showNewPwd ? 'password' : 'text'"
:label="t('newPassword')"
>
<template #append>
<QIcon
:name="showNewPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showNewPwd = !showNewPwd"
/>
</template>
</VnInput>
<VnInput
v-model="repeatPassword"
:type="!showCopyPwd ? 'password' : 'text'"
:label="t('repeatPassword')"
>
<template #append>
<QIcon
:name="showCopyPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showCopyPwd = !showCopyPwd"
/>
</template>
</VnInput>
</template>
<template v-if="isHeaderMounted" #actions>
<QBtn
:label="t('requirements')"
rounded
no-caps
flat
@click="passwordRequirementsDialogRef.show()"
/>
<QBtn
:label="t('modify')"
rounded
no-caps
flat
@click="vnFormRef.submit()"
/>
</template>
</VnForm>
<QDialog ref="passwordRequirementsDialogRef">
<QCard class="q-px-md q-py-lg column items-center">
<span class="text-h6 text-bold q-mb-md">
{{ t('passwordRequirements') }}
</span>
<div class="column" style="max-width: max-content">
<span>
{{
t('charactersLong', {
length: passwordRequirements.length
})
}}
</span>
<span>
{{
t('alphabeticCharacters', {
nAlpha: passwordRequirements.nAlpha
})
}}
</span>
<span>
{{
t('capitalLetters', {
nUpper: passwordRequirements.nUpper
})
}}
</span>
<span>
{{ t('digits', { nDigits: passwordRequirements.nDigits }) }}
</span>
<span>
{{ t('symbols', { nPunct: passwordRequirements.nPunct }) }}
</span>
</div>
</QCard>
</QDialog>
</template>
<i18n lang="yaml">
en-US:
changePassword: Change password
newPassword: New password
oldPassword: Old password
repeatPassword: Repeat password
modify: Modify
requirements: Requirements
passwordRequirements: Password requirements
charactersLong: '{length} characters long'
alphabeticCharacters: '{nAlpha} alphabetic characters'
capitalLetters: '{nUpper} capital letters'
digits: '{nDigits} digits'
symbols: '{nPunct} symbols. Ej: $%&.'
passwordsDoNotMatch: Passwords do not match
passwordEmpty: Password empty
es-ES:
changePassword: Cambiar contraseña
newPassword: Nueva contraseña
oldPassword: Contraseña antigua
repeatPassword: Repetir contraseña
modify: Modificar
requirements: Requisitos
passwordRequirements: Requisitos de contraseña
charactersLong: '{length} caracteres de longitud'
alphabeticCharacters: '{nAlpha} caracteres alfabéticos'
capitalLetters: '{nUpper} letras mayúsculas'
digits: '{nDigits} dígitos'
symbols: '{nPunct} símbolos. Ej: $%&.'
passwordsDoNotMatch: ¡Las contraseñas no coinciden!
passwordEmpty: Contraseña vacía
ca-ES:
changePassword: Canviar contrasenya
newPassword: Nova contrasenya
oldPassword: Contrasenya antiga
repeatPassword: Repetir contrasenya
modify: Modificar
requirements: Requisits
passwordRequirements: Requisits de contrasenya
charactersLong: '{length} caràcters de longitud'
alphabeticCharacters: '{nAlpha} caràcters alfabètics'
capitalLetters: '{nUpper} lletres majúscules'
digits: '{nDigits} dígits'
symbols: '{nPunct} símbols. Ej: $%&.'
passwordsDoNotMatch: Les contrasenyes no coincideixen!
passwordEmpty: Contrasenya buida
fr-FR:
changePassword: Changer le mot de passe
newPassword: Nouveau mot de passe
oldPassword: Ancien mot de passe
repeatPassword: Répéter le mot de passe
modify: Modifier
requirements: Exigences
passwordRequirements: Mot de passe exigences
charactersLong: '{length} caractères de longueur'
alphabeticCharacters: '{nAlpha} caractères alphabétiques'
capitalLetters: '{nUpper} lettres majuscules'
digits: '{nDigits} chiffres'
symbols: '{nPunct} symboles. Ej: $%&.'
passwordsDoNotMatch: Les mots de passe ne correspondent pas!
passwordEmpty: Mots de passe vides
pt-PT:
changePassword: Alterar palavra-passe
newPassword: Nova palavra-passe
oldPassword: Palavra-passe antiga
repeatPassword: Repetir palavra-passe
modify: Modificar
requirements: Requisitos
passwordRequirements: Requisitos de palavra-passe
charactersLong: '{length} caracteres de comprimento'
alphabeticCharacters: '{nAlpha} caracteres alfabéticos'
capitalLetters: '{nUpper} letras maiúsculas'
digits: '{nDigits} dígitos'
symbols: '{nPunct} símbolos. Ej: $%&.'
passwordsDoNotMatch: As palavras-passe não coincidem!
passwordEmpty: Palavra-passe vazia
</i18n>

View File

@ -0,0 +1,125 @@
<script setup>
import { ref, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import useNotify from 'src/composables/useNotify.js';
const props = defineProps({
schema: {
type: String,
default: ''
},
imageName: {
type: String,
default: ''
}
});
const emit = defineEmits(['close']);
const api = inject('api');
const { t } = useI18n();
const { notify } = useNotify();
const inputFileRef = ref(null);
const loading = ref(false);
const name = ref(props.imageName ?? '');
const file = ref(null);
const onSubmit = async () => {
try {
loading.value = true;
const formData = new FormData();
formData.append('name', name.value);
formData.append('image', file.value);
formData.append('schema', props.schema);
formData.append('srv', 'json:image/upload');
await api({
method: 'post',
url: location.origin,
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
});
notify(t('imageAdded'), 'positive');
emit('close');
} catch (error) {
console.error('Error uploading image:', error);
} finally {
loading.value = false;
}
};
</script>
<template>
<QForm @submit="onSubmit">
<QCard class="q-pa-lg">
<VnInput v-model="name" :label="t('name')" />
<QFile
ref="inputFileRef"
:label="t('file')"
v-model="file"
:multiple="false"
class="q-mb-xs"
>
<template #append>
<QIcon
name="attach_file"
class="cursor-pointer"
@click="inputFileRef.pickFiles()"
/>
</template>
</QFile>
<div class="flex row justify-end q-gutter-x-sm">
<QSpinner
v-if="loading"
color="primary"
size="3em"
:thickness="2"
/>
<QBtn
v-else
type="submit"
:label="t('send')"
flat
class="q-mt-md"
rounded
/>
</div>
</QCard>
</QForm>
</template>
<style lang="scss" scoped></style>
<i18n lang="yaml">
en-US:
name: Name
file: File
send: Send
imageAdded: Image added successfully
es-ES:
name: Nombre
file: Archivo
send: Enviar
imageAdded: Imagen añadida correctamente
ca-ES:
name: Nom
file: Arxiu
send: Enviar
imageAdded: Imatge afegida correctament
fr-FR:
name: Nom
file: Fichier
send: Envoyer
imageAdded: Image ajoutée correctement
pt-PT:
name: Nome
file: Arquivo
send: Enviar
imageAdded: Imagen adicionada corretamente
</i18n>

View File

@ -0,0 +1,122 @@
<script setup>
import { ref } from 'vue';
import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
icon: {
type: String,
default: null
},
title: {
type: String,
default: null
},
message: {
type: String,
default: null
},
data: {
type: Object,
required: false,
default: null
},
promise: {
type: Function,
required: false,
default: null
}
});
defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const { dialogRef, onDialogOK } = useDialogPluginComponent();
const title = props.title || t('confirm');
const message = props.message || t('wantToContinue');
const isLoading = ref(false);
async function confirm() {
isLoading.value = true;
if (props.promise) {
try {
await props.promise(props.data);
} finally {
isLoading.value = false;
}
}
onDialogOK(props.data);
}
</script>
<template>
<QDialog ref="dialogRef">
<QCard class="q-pa-sm">
<QCardSection class="row items-center q-pb-none">
<QAvatar
:icon="icon"
color="primary"
text-color="white"
size="xl"
v-if="icon"
/>
<span class="text-h6 text-grey">{{ title }}</span>
<QSpace />
<QBtn
icon="close"
:disable="isLoading"
flat
round
dense
v-close-popup
/>
</QCardSection>
<QCardSection class="row items-center">
<span v-html="message" />
<slot name="customHTML" />
</QCardSection>
<QCardActions align="right">
<QBtn
:label="t('cancel')"
color="primary"
:disable="isLoading"
flat
v-close-popup
/>
<QBtn
:label="t('confirm')"
color="primary"
:loading="isLoading"
@click="confirm()"
unelevated
autofocus
/>
</QCardActions>
</QCard>
</QDialog>
</template>
<style lang="scss" scoped>
.q-card {
min-width: 350px;
}
</style>
<i18n lang="yaml">
en-US:
wantToContinue: Are you sure you want to continue?
cancel: Cancel
es-ES:
wantToContinue: ¿Seguro que quieres continuar?
cancel: Cancelar
ca-ES:
wantToContinue: Segur que vols continuar?
cancel: Cancel·lar
fr-FR:
wantToContinue: Êtes-vous sûr de vouloir continuer?
cancel: Annuler
pt-PT:
wantToContinue: Tem a certeza de que deseja continuar?
cancel: Cancelar
</i18n>

183
src/components/ui/VnImg.vue Normal file
View File

@ -0,0 +1,183 @@
<script setup>
import { ref, computed } from 'vue';
import { useAppStore } from 'stores/app';
import ImageEditor from 'src/components/ui/ImageEditor.vue';
import { useI18n } from 'vue-i18n';
const props = defineProps({
baseURL: {
type: String,
default: null
},
storage: {
type: [String, Number],
default: 'Images'
},
size: {
type: String,
default: 'full'
},
zoomSize: {
type: String,
required: false,
default: 'lg'
},
id: {
type: Number,
required: true
},
rounded: {
type: Boolean,
default: false
},
fullRounded: {
type: Boolean,
default: false
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '100%'
},
editable: {
type: Boolean,
default: false
},
editSchema: {
type: String,
default: ''
},
editImageName: {
type: String,
default: ''
},
alwaysShowEditButton: {
type: Boolean,
default: false
}
});
const { t } = useI18n();
const app = useAppStore();
const showZoom = ref(false);
const showEditForm = ref(false);
const url = computed(() => {
return `${props.baseURL ?? app.imageUrl}/${props.storage}/${props.size}/${props.id}`;
});
</script>
<template>
<div class="relative-position main-image-container">
<QBtn
v-if="props.editable"
icon="add_a_photo"
class="show-edit-button absolute-top-left"
:class="{ hide: !props.alwaysShowEditButton }"
round
text-color="black"
@click.stop.prevent="showEditForm = !showEditForm"
>
<QTooltip>{{ t('addOrEditImage') }}</QTooltip>
</QBtn>
<QImg
:class="{
zoomIn: props.zoomSize,
rounded: props.rounded,
'full-rounded': props.fullRounded
}"
class="main-image"
:src="url"
v-bind="$attrs"
@click="showZoom = !showZoom"
spinner-color="primary"
:width="props.width"
:height="props.height"
draggable
>
<template #error>
<div
class="full-width full-height flex justify-center items-center"
>
<QIcon name="image" size="sm" />
</div>
</template>
</QImg>
</div>
<QDialog v-if="props.zoomSize" v-model="showZoom">
<QImg
:src="url"
size="full"
class="img_zoom"
v-bind="$attrs"
spinner-color="primary"
draggable
/>
</QDialog>
<QDialog v-if="props.editable" v-model="showEditForm">
<ImageEditor
class="all-pointer-events"
:schema="props.editSchema"
:image-name="props.editImageName"
@close="showEditForm = false"
/>
</QDialog>
</template>
<style lang="scss" scoped>
.main-image-container {
&:hover {
.show-edit-button {
visibility: visible !important;
}
.main-image {
filter: brightness(80%);
}
}
}
.main-image {
&.zoomIn {
cursor: zoom-in;
}
min-width: 50px;
}
.hide {
visibility: hidden;
}
.show-edit-button {
cursor: pointer;
background-color: $gray-light;
z-index: 1;
}
.rounded {
border-radius: 0.6em;
}
.full-rounded {
border-radius: 50px;
}
.img_zoom {
border-radius: 0%;
}
</style>
<i18n lang="yaml">
en-US:
addOrEditImage: Add or update an image
es-ES:
addOrEditImage: Añadir o actualizar imagen
ca-ES:
addOrEditImage: Afegir o actualitzar Imatge
fr-FR:
addOrEditImage: Ajouter our mettre à jour l'image
pt-PT:
addOrEditImage: Adicionar ou atualizar imagem
</i18n>

View File

@ -0,0 +1,91 @@
<script setup>
import { onMounted, ref, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
const props = defineProps({
searchFn: {
type: Function,
default: null
},
placeholder: {
type: String,
default: 'Search'
},
sqlQuery: {
type: String,
default: null
},
searchField: {
type: String,
default: 'search'
}
});
const emit = defineEmits(['onSearch', 'onSearchError']);
const jApi = inject('jApi');
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const searchTerm = ref('');
const search = async () => {
try {
let data = null;
router.replace({
query: searchTerm.value ? { search: searchTerm.value } : {}
});
if (props.sqlQuery) {
data = await jApi.query(props.sqlQuery, {
[props.searchField]: searchTerm.value
});
} else if (props.searchFn) {
data = props.searchFn(searchTerm.value);
}
emit('onSearch', data);
} catch (error) {
console.error('Error searching:', error);
emit('onSearchError');
}
};
onMounted(() => {
if (route.query.search) {
searchTerm.value = route.query.search;
search();
}
});
</script>
<template>
<VnInput
v-model="searchTerm"
@keyup.enter="search()"
:placeholder="props.placeholder || t('search')"
bg-color="white"
is-outlined
:clearable="false"
>
<template #prepend>
<QIcon name="search" class="cursor-pointer" @click="search()" />
</template>
</VnInput>
</template>
<i18n lang="yaml">
en-US:
search: Search
es-ES:
search: Buscar
ca-ES:
search: Cercar
fr-FR:
search: Rechercher
pt-PT:
search: Pesquisar
</i18n>

View File

@ -0,0 +1,41 @@
<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
noDataLabel: {
type: String,
default: ''
},
hideBottom: {
type: Boolean,
default: false
},
rowsPerPageOptions: {
type: Array,
default: () => [0]
}
});
</script>
<template>
<QTable
v-bind="$attrs"
:no-data-label="props.noDataLabel || t('noData')"
:hide-bottom="props.hideBottom"
:rows-per-page-options="props.rowsPerPageOptions"
table-header-class="vntable-header-default"
>
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps" />
</template>
</QTable>
</template>
<style lang="scss">
.vntable-header-default {
background-color: $accent !important;
color: white;
}
</style>

View File

@ -0,0 +1,22 @@
import { Notify } from 'quasar';
import { i18n } from 'src/boot/i18n';
export default function useNotify() {
const notify = (message, type, icon) => {
const defaultIcons = {
warning: 'warning',
negative: 'error',
positive: 'check'
};
Notify.create({
message: i18n.global.t(message),
type,
icon: icon || defaultIcons[type]
});
};
return {
notify
};
}

View File

@ -0,0 +1,38 @@
import { useUserStore } from 'stores/user';
import axios from 'axios';
import { useQuasar } from 'quasar';
export function usePrintService() {
const quasar = useQuasar();
const userStore = useUserStore();
const token = userStore.token;
function sendEmail(path, params) {
return axios.post(path, params).then(() =>
quasar.notify({
message: 'Notification sent',
type: 'positive',
icon: 'check'
})
);
}
function openReport(path, params) {
params = Object.assign(
{
access_token: token
},
params
);
const query = new URLSearchParams(params).toString();
window.open(`api/${path}?${query}`);
}
return {
sendEmail,
openReport
};
}

View File

@ -0,0 +1,23 @@
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import { useQuasar } from 'quasar';
export function useVnConfirm() {
const quasar = useQuasar();
const openConfirmationModal = (title, message, promise, successFn) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title,
message,
promise
}
})
.onOk(async () => {
if (successFn) successFn();
});
};
return { openConfirmationModal };
}

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

@ -0,0 +1,56 @@
// app global css in SCSS form
@font-face {
font-family: Poppins;
src: url(./poppins.ttf) format('truetype');
}
@font-face {
font-family: 'Open Sans';
src: url(./opensans.ttf) format('truetype');
}
@mixin mobile {
@media screen and (max-width: 960px) {
@content;
}
}
body {
font-family: 'Poppins', 'Verdana', 'Sans';
background-color: #fafafa;
}
a.link {
text-decoration: none;
color: #6a1;
&:hover {
text-decoration: underline;
}
}
.default-radius {
border-radius: 0.6em;
}
.q-card {
border-radius: 0.6em !important;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
}
.q-table__container {
border-radius: 0.6em !important;
}
.q-page-sticky.fixed-bottom-right {
margin: 18px;
}
.no-border-radius {
border-radius: 0 !important;
}
.no-padding {
padding: 0 !important;
}
input[type='number'] {
-moz-appearance: textfield;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}

BIN
src/css/opensans.ttf Normal file

Binary file not shown.

BIN
src/css/poppins.ttf Normal file

Binary file not shown.

View File

@ -0,0 +1,33 @@
// 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: #1a1a1a;
$secondary: #26a69a;
$accent: #8cc63f;
$gray-light: #ddd;
$dark: #1d1d1d;
$dark-page: #121212;
$positive: #21ba45;
$negative: #c10015;
$info: #31ccec;
$warning: #f2c037;
// Width
$width-xs: 400px;
$width-sm: 544px;
$width-md: 800px;
$width-lg: 1280px;
$width-xl: 1600px;

5
src/css/responsive.scss Normal file
View File

@ -0,0 +1,5 @@
@mixin mobile {
@media screen and (max-width: 1023px) {
@content;
}
}

24
src/css/width.scss Normal file
View File

@ -0,0 +1,24 @@
%margin-auto {
margin-left: auto;
margin-right: auto;
}
.vn-w-xs {
@extend %margin-auto;
max-width: $width-xs;
}
.vn-w-sm {
@extend %margin-auto;
max-width: $width-sm;
}
.vn-w-md {
@extend %margin-auto;
max-width: $width-md;
}
.vn-w-lg {
@extend %margin-auto;
max-width: $width-lg;
}
.vn-w-xl {
@extend %margin-auto;
max-width: $width-xl;
}

86
src/i18n/ca-ES/index.js Normal file
View File

@ -0,0 +1,86 @@
export default {
date: {
days: [
'Diumenge',
'Dilluns',
'Dimarts',
'Dimecres',
'Dijous',
'Divendres',
'Dissabte'
],
daysShort: ['Dg', 'Dl', 'Dt', 'Dc', 'Dj', 'Dv', 'Ds'],
months: [
'Gener',
'Febrer',
'Març',
'Abril',
'Maig',
'Juny',
'Juliol',
'Agost',
'Setembre',
'Octubre',
'Novembre',
'Desembre'
],
monthsShort: [
'Gen',
'Feb',
'Mar',
'Abr',
'Mai',
'Jun',
'Jul',
'Ago',
'Set',
'Oct',
'Nov',
'Des'
]
},
of: 'de',
// Sections titles
Home: 'Inici',
Orders: 'Comandes',
Ticket: `Detall de l'encarrec`,
'Pending orders': 'Comandes pendents',
'Last orders': 'Comandes confirmades',
Invoices: 'Factures',
Basket: 'Cistella',
Catalog: 'Catàleg',
Administration: 'Administració',
'Control panel': 'Panell de control',
Users: 'Usuaris',
Connections: 'Connexions',
Visits: 'Visites',
News: 'Notícies',
Photos: 'Imatges',
Images: 'Imatges',
Items: 'Articles',
Agencies: 'Agències',
Reports: 'Informes',
Configuration: 'Configuració',
Shelves: 'Prestatgeries',
Account: 'Compte',
Addresses: 'Adreces',
Confirm: 'Confirmar',
Checkout: `Configurar l'encarrec`,
'Address details': 'Configuració',
'Admin news details': `Afegir o editar notícia`,
//
orderLoadedIntoBasket: 'Comanda carregada a la cistella!',
loadAnOrder:
'Si us plau carrega una comanda pendent a la cistella o en comença una de nova',
at: 'a les',
back: 'Tornar',
next: 'Següent',
remove: 'Esborrar',
agency: 'Agència',
noData: 'Sense dades',
confirm: 'Confirmar',
delete: 'Esborrar',
confirmDelete: 'Estàs segur que vols esborrar la línia?',
emptyList: 'Llista buida'
};

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

@ -0,0 +1,120 @@
// This is just an example,
// so you can safely delete all default props below
export default {
failed: 'Action failed',
success: 'Action was successful',
internalServerError: 'Internal server error',
somethingWentWrong: 'Something went wrong',
loginFailed: 'Login failed',
authenticationRequired: 'Authentication required',
notFound: 'Not found',
today: 'Today',
yesterday: 'Yesterday',
tomorrow: 'Tomorrow',
date: {
days: [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
],
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
months: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
],
monthsShort: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
},
// Sections titles
Home: 'Home',
Orders: 'Orders',
Ticket: 'Detalle del pedido',
'Pending orders': 'Pending orders',
'Last orders': 'Confirmed orders',
Invoices: 'Invoices',
Basket: 'Basket',
Catalog: 'Catalog',
Administration: 'Administration',
'Control panel': 'Control panel',
Users: 'Users',
Connections: 'Connections',
Visits: 'Visits',
News: 'News',
Photos: 'Images',
Images: 'Images',
Items: 'Items',
Agencies: 'Agencies',
Reports: 'Reports',
Configuration: 'Configuration',
Shelves: 'Shelves',
Account: 'Account',
Addresses: 'Addresses',
Confirm: 'Confirm',
Checkout: 'Configure order',
'Address details': 'Configuration',
'Admin news details': 'Add or edit new',
//
orderLoadedIntoBasket: 'Order loaded into basket!',
loadAnOrder: 'Please load a pending order to the cart or start a new one',
at: 'at',
back: 'Back',
next: 'Next',
remove: 'Remove',
agency: 'Agency',
noData: 'No data',
confirm: 'Confirm',
delete: 'Delete',
confirmDelete: 'Are you sure you want to delete the line?',
emptyList: 'Empty list',
orders: 'Orders',
order: 'Pending order',
ticket: 'Order',
conditions: 'Conditions',
about: 'About us',
admin: 'Administration',
panel: 'Control panel',
users: 'Users',
connections: 'Connections',
visits: 'Visits',
news: 'News',
newEdit: 'Edit new',
images: 'Images',
items: 'Items',
config: 'Configuration',
user: 'User',
addresses: 'Addresses',
addressEdit: 'Edit address',
dataSaved: 'Data saved',
save: 'Save',
cancel: 'Cancel',
of: 'of'
};

136
src/i18n/es-ES/index.js Normal file
View File

@ -0,0 +1,136 @@
export default {
failed: 'Acción fallida',
success: 'Acción exitosa',
internalServerError: 'Error interno del servidor',
somethingWentWrong: 'Algo salió mal',
loginFailed: 'Usuario o contraseña incorrectos',
authenticationRequired: 'Autenticación requerida',
notFound: 'No encontrado',
today: 'Hoy',
yesterday: 'Ayer',
tomorrow: 'Mañana',
language: 'Idioma',
langs: {
en: 'Inglés',
es: 'Español',
ca: 'Catalán',
fr: 'Francés',
mn: 'Ruso',
pt: 'Portugés'
},
date: {
days: [
'Domingo',
'Lunes',
'Martes',
'Miércoles',
'Jueves',
'Viernes',
'Sábado'
],
daysShort: ['Do', 'Lu', 'Mi', 'Mi', 'Ju', 'Vi', 'Sa'],
months: [
'Enero',
'Febrero',
'Marzo',
'Abril',
'Mayo',
'Junio',
'Julio',
'Agosto',
'Septiembre',
'Octubre',
'Noviembre',
'Diciembre'
],
monthsShort: [
'Ene',
'Feb',
'Mar',
'Abr',
'May',
'Jun',
'Jul',
'Ago',
'Sep',
'Oct',
'Nov',
'Dic'
]
},
// Sections titles
Home: 'Inicio',
Orders: 'Pedidos',
Ticket: 'Pedido',
'Pending orders': 'Pedidos pendientes',
'Last orders': 'Pedidos confirmados',
Invoices: 'Facturas',
Basket: 'Cesta',
Catalog: 'Catálogo',
Administration: 'Administración',
'Control panel': 'Panel de control',
Users: 'Usuarios',
Connections: 'Conexiones',
Visits: 'Visitas',
News: 'Noticias',
Photos: 'Imágenes',
Images: 'Imágenes',
Items: 'Artículos',
Agencies: 'Agencias',
Reports: 'Informes',
Configuration: 'Configuración',
Shelves: 'Estanterías',
Account: 'Cuenta',
Addresses: 'Direcciones',
Confirm: 'Confirmar',
Checkout: 'Configurar pedido',
'Address details': 'Configuración',
'Admin news details': 'Añadir o editar noticia',
//
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
loadAnOrder:
'Por favor carga un pedido pendiente en la cesta o empieza uno nuevo',
at: 'a las',
back: 'Volver',
next: 'Siguiente',
remove: 'Borrar',
agency: 'Agencia',
noData: 'Sin datos',
confirm: 'Confirmar',
delete: 'Borrar',
confirmDelete: '¿Estás seguro de que quieres borrar la línea?',
emptyList: 'Lista vacía',
orders: 'Pedidos',
order: 'Pedido pendiente',
ticket: 'Pedido',
conditions: 'Condiciones',
about: 'Sobre nosotros',
admin: 'Administración',
panel: 'Panel de control',
users: 'Usuarios',
connections: 'Conexiones',
visits: 'Visitas',
news: 'Noticias',
newEdit: 'Editar noticia',
images: 'Imágenes',
items: 'Artículos',
config: 'Configuración',
user: 'Usuario',
password: 'Contraseña',
remindMe: 'Recuérdame',
logInAsGuest: 'Entrar como invitado',
logIn: 'Iniciar sesión',
loginMail: "{'info'}{'@'}{'verdnatura.es'}",
loginPhone: '+34 963 242 100',
haveForgottenPassword: '¿Has olvidado tu contraseña?',
notACustomerYet: '¿Todavía no eres cliente?',
signUp: 'Registrarme',
addresses: 'Direcciones',
addressEdit: 'Editar dirección',
dataSaved: 'Datos guardados',
save: 'Guardar',
cancel: 'Cancelar',
of: 'de'
};

85
src/i18n/fr-FR/index.js Normal file
View File

@ -0,0 +1,85 @@
export default {
date: {
days: [
'Dimanche',
'Lundi',
'Mardi',
'Mercredi',
'Jeudi',
'Vendredi',
'Samedi'
],
daysShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
months: [
'Janvier',
'Février',
'Mars',
'Avril',
'Mai',
'Juin',
'Juillet',
'Août',
'Septembre',
'Octobre',
'Novembre',
'Décembre'
],
monthsShort: [
'Jan',
'Fév',
'Mar',
'Avr',
'Mai',
'Juin',
'Juil',
'Aoû',
'Sep',
'Oct',
'Nov',
'Déc'
]
},
of: 'de',
// Sections titles
Home: 'Accueil',
Orders: 'Commandes',
Ticket: 'Détail de la commande',
'Pending orders': 'Commandes en attente',
'Last orders': 'Commandes confirmées',
Invoices: 'Factures',
Basket: 'Panier',
Catalog: 'Catalogue',
Administration: 'Administration',
'Control panel': 'Panneau de configuration',
Users: 'Utilisateurs',
Connections: 'Connexions',
Visits: 'Visites',
News: 'Nouvelles',
Photos: 'Images',
Images: 'Images',
Items: 'Articles',
Agencies: 'Agences',
Reports: 'Rapports',
Configuration: 'Configuration',
Shelves: 'Étagères',
Account: 'Compte',
Addresses: 'Adresses',
Confirm: 'Confirmer',
Checkout: 'Configurer la commande',
'Address details': 'Configuration',
'Admin news details': 'Ajouter ou éditer une nouvelle',
//
orderLoadedIntoBasket: 'Commande chargée dans le panier!',
loadAnOrder:
'Veuillez télécharger une commande en attente dans le panier ou en démarrer une nouvelle',
at: 'à',
back: 'Retour',
next: 'Suivant',
remove: 'Effacer',
agency: 'Agence',
noData: 'Aucune donnée',
confirm: 'Confirmer',
delete: 'Effacer',
confirmDelete: 'Voulez-vous vraiment supprimer la ligne?'
};

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

@ -0,0 +1,13 @@
import enUS from './en-US';
import esES from './es-ES';
import frFR from './fr-FR';
import ptPT from './pt-PT';
import caES from './ca-ES';
export default {
'en-US': enUS,
'es-ES': esES,
'fr-FR': frFR,
'pt-PT': ptPT,
'ca-ES': caES
};

83
src/i18n/pt-PT/index.js Normal file
View File

@ -0,0 +1,83 @@
export default {
date: {
days: [
'Domingo',
'Segunda-feira',
'Terça-feira',
'Quarta-feira',
'Quinta-feira',
'Sexta-feira',
'Sábado'
],
daysShort: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'],
months: [
'Janeiro',
'Fevereiro',
'Março',
'Abril',
'Maio',
'Junho',
'Julho',
'Agosto',
'Setembro',
'Outubro',
'Novembro',
'Dezembro'
],
monthsShort: [
'Jan',
'Fev',
'Mar',
'Abr',
'Mai',
'Jun',
'Jul',
'Ago',
'Set',
'Out',
'Nov',
'Dez'
]
},
of: 'de',
// Sections titles
Home: 'Início',
Orders: 'Pedidos',
Ticket: 'Detalhe do pedido',
'Pending orders': 'Pedidos pendentes',
'Last orders': 'Pedidos confirmados',
Invoices: 'Faturas',
Basket: 'Carrinho',
Catalog: 'Catálogo',
Administration: 'Administração',
'Control panel': 'Painel de controle',
Users: 'Usuários',
Connections: 'Conexões',
Visits: 'Visitas',
News: 'Notícias',
Photos: 'Imagens',
Images: 'Imagens',
Items: 'Artigos',
Agencies: 'Agências',
Reports: 'Informes',
Configuration: 'Configuração',
Shelves: 'Estantes',
Account: 'Conta',
Addresses: 'Moradas',
Confirm: 'Confirme',
Checkout: 'Configurar encomenda',
'Address details': 'Configuração',
'Admin news details': 'Adicionar ou editar notícia',
//
orderLoadedIntoBasket: 'Pedido carregado na cesta!',
loadAnOrder: 'Carregue um pedido pendente no carrinho ou inicie um novo',
at: 'às',
back: 'Voltar',
next: 'Seguinte',
remove: 'Eliminar',
agency: 'Agência',
noData: 'Sem dados',
confirm: 'Confirme',
delete: 'Eliminar',
confirmDelete: 'Tens certeza que queres eliminar esta linha?'
};

22
src/index.template.html Normal file
View File

@ -0,0 +1,22 @@
<!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>
<!-- DO NOT touch the following DIV -->
<div id="q-app"></div>
</body>
</html>

187
src/js/db/connection.js Normal file
View File

@ -0,0 +1,187 @@
import { JsonConnection } from '../vn/json-connection';
import { ResultSet } from './result-set';
/**
* Simulates a connection to a database by making asynchronous requests to a
* remote REST service that returns the results in JSON format.
* Using this class can perform any operation that can be done with a database,
* like open/close a connection or selecion/updating queries.
*
* Warning! You should set a well defined dababase level privileges to use this
* class or you could have a serious security hole in you application becasuse
* the user can send any statement to the server. For example: DROP DATABASE
*/
const Flag = {
NOT_NULL: 1,
PRI_KEY: 2,
AI: 512 | 2 | 1
};
const Type = {
BOOLEAN: 1,
INTEGER: 3,
DOUBLE: 4,
STRING: 5,
DATE: 8,
DATE_TIME: 9
};
export class Connection extends JsonConnection {
static Flag = Flag;
static Type = Type;
/**
* Runs a SQL query on the database.
*
* @param {String} sql The SQL statement
* @return {ResultSet} The result
*/
async execSql(sql) {
const json = await this.send('core/query', { sql });
const results = [];
let err;
if (json) {
try {
if (json && json instanceof Array) {
for (let i = 0; i < json.length; i++) {
if (json[i] !== true) {
const rows = json[i].data;
const columns = json[i].columns;
const data = new Array(rows.length);
results.push({
data,
columns,
tables: json[i].tables
});
for (let j = 0; j < rows.length; j++) {
const row = (data[j] = {});
for (let k = 0; k < columns.length; k++) {
row[columns[k].name] = rows[j][k];
}
}
for (let j = 0; j < columns.length; j++) {
let castFunc = null;
const col = columns[j];
switch (col.type) {
case Type.DATE:
case Type.DATE_TIME:
case Type.TIMESTAMP:
castFunc = this.valueToDate;
break;
}
if (castFunc !== null) {
if (col.def != null) {
col.def = castFunc(col.def);
}
for (let k = 0; k < data.length; k++) {
if (data[k][col.name] != null) {
data[k][col.name] = castFunc(
data[k][col.name]
);
}
}
}
}
} else {
results.push(json[i]);
}
}
}
} catch (e) {
err = e;
}
}
return new ResultSet(results, err);
}
/**
* Runs a query on the database.
*
* @param {String} query The SQL statement
* @param {Object} params The query params
* @return {ResultSet} The result
*/
async execQuery(query, params) {
const sql = query.replace(/#\w+/g, key => {
const value = params[key.substring(1)];
return value ? this.renderValue(value) : key;
});
return await this.execSql(sql);
}
async query(query, params) {
const res = await this.execQuery(query, params);
return res.fetchData();
}
async getObject(query, params) {
const res = await this.execQuery(query, params);
return res.fetchObject();
}
async getValue(query, params) {
const res = await this.execQuery(query, params);
return res.fetchValue();
}
renderValue(v) {
switch (typeof v) {
case 'number':
return v;
case 'boolean':
return v ? 'TRUE' : 'FALSE';
case 'string':
return "'" + v.replace(this.regexp, this.replaceFunc) + "'";
default:
if (v instanceof Date) {
if (!isNaN(v.getTime())) {
const unixTime = parseInt(fixTz(v).getTime() / 1000);
return 'DATE(FROM_UNIXTIME(' + unixTime + '))';
} else {
return '0000-00-00';
}
} else {
return 'NULL';
}
}
}
/*
* Parses a value to date.
*/
valueToDate(value) {
return fixTz(new Date(value));
}
}
// TODO: Read time zone from db configuration
const tz = { timeZone: 'Europe/Madrid' };
const isLocal =
Intl.DateTimeFormat().resolvedOptions().timeZone === tz.timeZone;
function fixTz(date) {
if (isLocal) return date;
const localDate = new Date(date.toLocaleString('en-US', tz));
const hasTime =
localDate.getHours() ||
localDate.getMinutes() ||
localDate.getSeconds() ||
localDate.getMilliseconds();
if (!hasTime) {
date.setHours(date.getHours() + 12);
date.setHours(0, 0, 0, 0);
}
return date;
}

128
src/js/db/result-set.js Normal file
View File

@ -0,0 +1,128 @@
import { Result } from './result';
/**
* This class stores the database results.
*/
export class ResultSet {
results = null;
error = null;
/**
* Initilizes the resultset object.
*/
constructor(results, error) {
this.results = results;
this.error = error;
}
/**
* Gets the query error.
*
* @return {Db.Err} the error or null if no errors hapened
*/
getError() {
return this.error;
}
fetch() {
if (this.error) {
throw this.error;
}
if (this.results !== null && this.results.length > 0) {
return this.results.shift();
}
return null;
}
/**
* Fetchs the next result from the resultset.
*
* @return {Db.Result} the result or %null if error or there are no more results
*/
fetchResult() {
const result = this.fetch();
if (result !== null) {
if (result.data instanceof Array) {
return new Result(result);
} else {
return true;
}
}
return null;
}
/**
* Fetchs the first row object from the next resultset.
*
* @return {Array} the row if success, %null otherwise
*/
fetchObject() {
const result = this.fetch();
if (
result !== null &&
result.data instanceof Array &&
result.data.length > 0
) {
return result.data[0];
}
return null;
}
/**
* Fetchs data from the next resultset.
*
* @return {Array} the data
*/
fetchData() {
const result = this.fetch();
if (result !== null && result.data instanceof Array) {
return result.data;
}
return null;
}
/**
* Fetchs the first row and column value from the next resultset.
*
* @return {Object} the value if success, %null otherwise
*/
fetchValue() {
const row = this.fetchRow();
if (row instanceof Array && row.length > 0) {
return row[0];
}
return null;
}
/**
* Fetchs the first row from the next resultset.
*
* @return {Array} the row if success, %null otherwise
*/
fetchRow() {
const result = this.fetch();
if (
result !== null &&
result.data instanceof Array &&
result.data.length > 0
) {
const object = result.data[0];
const row = new Array(result.columns.length);
for (let i = 0; i < row.length; i++) {
row[i] = object[result.columns[i].name];
}
return row;
}
return null;
}
}

65
src/js/db/result.js Normal file
View File

@ -0,0 +1,65 @@
/**
* This class stores a database result.
*/
export class Result {
/**
* Initilizes the result object.
*/
constructor (result) {
this.data = result.data
this.tables = result.tables
this.columns = result.columns
this.row = -1
if (this.columns) {
this.columnMap = {}
for (let i = 0; i < this.columns.length; i++) {
const col = this.columns[i]
col.index = i
this.columnMap[col.name] = col
}
} else {
this.columnMap = null
}
}
/**
* Gets a value from de result.
*
* @param {String} columnName The column name
* @return {Object} The cell value
*/
get (columnName) {
return this.data[this.row][columnName]
}
/**
* Gets a row.
*
* @return {Object} The cell value
*/
getObject () {
return this.data[this.row]
}
/**
* Resets the result iterator.
*/
reset () {
this.row = -1
}
/**
* Moves the internal iterator to the next row.
*/
next () {
this.row++
if (this.row >= this.data.length) {
return false
}
return true
}
}

58
src/js/db/sqlService.js Normal file
View File

@ -0,0 +1,58 @@
const sanitizeValue = value => {
if (typeof value === 'string') {
return `'${value}'`;
} else if (value === null) {
return 'NULL';
}
return value;
};
export const generateUpdateSqlQuery = (
schema,
table,
pks,
columnsUpdated,
formData
) => {
const setClauses = columnsUpdated
.map(colName => `${colName} = ${sanitizeValue(formData[colName])}`)
.join(', ');
const whereClause = Object.keys(pks)
.map(pk => `${pk} = ${pks[pk]}`)
.join(' AND ');
return `
START TRANSACTION;
UPDATE ${schema}.${table} SET ${setClauses} WHERE (${whereClause});
SELECT ${columnsUpdated.join(', ')} FROM ${schema}.${table} WHERE (${whereClause});
COMMIT;
`;
};
export const generateInsertSqlQuery = (
schema,
table,
formData,
columnsUpdated,
createModelDefault
) => {
const columns = createModelDefault.field
? [createModelDefault.field, ...columnsUpdated].join(', ')
: columnsUpdated.join(', ');
const values = createModelDefault.value
? [
createModelDefault.value,
...columnsUpdated.map(colName => sanitizeValue(formData[colName]))
].join(', ')
: columnsUpdated
.map(colName => sanitizeValue(formData[colName]))
.join(', ');
return `
START TRANSACTION;
INSERT INTO ${schema}.${table} (${columns}) VALUES (${values});
SELECT id, ${columnsUpdated.join(', ')} FROM ${schema}.${table} WHERE (id = LAST_INSERT_ID());
COMMIT;
`;
};

View File

@ -0,0 +1,208 @@
import { VnObject } from './object';
import { JsonException } from './json-exception';
/**
* Handler for JSON rest connections.
*/
export class JsonConnection extends VnObject {
_connected = false;
_requestsCount = 0;
token = null;
interceptors = [];
errorInterceptor = null;
use(fn) {
this.interceptors.push(fn);
}
useErrorInterceptor(fn) {
this.errorInterceptor = fn;
}
/**
* Executes the specified REST service with the given params and calls
* the callback when response is received.
*
* @param {String} url The service path
* @param {Object} params The params to pass to the service
* @return {Object} The parsed JSON response
*/
async send(url, params) {
if (!params) params = {};
params.srv = `json:${url}`;
return this.sendWithUrl('POST', '.', params);
}
async sendForm(form) {
const params = {};
const elements = form.elements;
for (let i = 0; i < elements.length; i++) {
if (elements[i].name) {
params[elements[i].name] = elements[i].value;
}
}
return this.sendWithUrl('POST', form.action, params);
}
async sendFormMultipart(form) {
return this.request({
method: 'POST',
url: form.action,
data: new FormData(form)
});
}
async sendFormData(formData) {
return this.request({
method: 'POST',
url: '',
data: formData
});
}
/*
* Called when REST response is received.
*/
async sendWithUrl(method, url, params) {
const urlParams = new URLSearchParams();
for (const key in params) {
if (params[key] != null) {
urlParams.set(key, params[key]);
}
}
return this.request({
method,
url,
data: urlParams.toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}
async request(config) {
const request = new XMLHttpRequest();
request.open(config.method, config.url, true);
for (const fn of this.interceptors) {
config = fn(config);
}
const headers = config.headers;
if (headers) {
for (const header in headers) {
request.setRequestHeader(header, headers[header]);
}
}
const promise = new Promise((resolve, reject) => {
request.onreadystatechange = () =>
this._onStateChange(request, resolve, reject);
});
request.send(config.data);
this._requestsCount++;
if (this._requestsCount === 1) {
this.emit('loading-changed', true);
}
return promise;
}
_onStateChange(request, resolve, reject) {
if (request.readyState !== 4) {
return;
}
this._requestsCount--;
if (this._requestsCount === 0) {
this.emit('loading-changed', false);
}
let data = null;
let error = null;
try {
if (request.status === 0) {
const err = new JsonException();
err.message =
'The server does not respond, please check your Internet connection';
err.statusCode = request.status;
throw err;
}
let contentType = null;
try {
contentType = request
.getResponseHeader('Content-Type')
.split(';')[0]
.trim();
} catch (err) {
console.warn(err);
}
if (contentType !== 'application/json') {
const err = new JsonException();
err.message = request.statusText;
err.statusCode = request.status;
throw err;
}
let json;
let jsData;
if (request.responseText) {
json = JSON.parse(request.responseText);
}
if (json) {
jsData = json.data || json;
}
if (request.status >= 200 && request.status < 300) {
data = jsData;
} else {
let exception = jsData.exception;
const err = new JsonException();
err.statusCode = request.status;
if (exception) {
exception = exception
.replace(/\\/g, '.')
.replace(/Exception$/, '')
.replace(/^Vn\.Web\./, '');
err.exception = exception;
err.message = jsData.message;
err.code = jsData.code;
err.file = jsData.file;
err.line = jsData.line;
err.trace = jsData.trace;
} else {
err.message = request.statusText;
}
throw err;
}
} catch (e) {
data = null;
error = e;
}
if (error) {
if (this.errorInterceptor) {
this.errorInterceptor(error);
}
this.emit('error', error);
reject(error);
} else {
resolve(data);
}
}
}

View File

@ -0,0 +1,15 @@
/**
* This class stores the database errors.
*/
export class JsonException {
constructor (exception, message, code, file, line, trace, statucCode) {
this.name = 'JsonException'
this.exception = exception
this.message = message
this.code = code
this.file = file
this.line = line
this.trace = trace
this.statusCode = statucCode
}
}

289
src/js/vn/object.js Normal file
View File

@ -0,0 +1,289 @@
/**
* The main base class. Manages the signal system. Objects based on this class
* can be instantiated declaratively using XML.
*/
export class VnObject {
/**
* Tag to be used when the class instance is defined via XML. All classes
* must define this attribute, even if it is not used.
*/
static Tag = 'vn-object'
/**
* Class public properties.
*/
static Properties = {}
/*
* Reference count.
*/
_refCount = 1
/*
* Signal handlers data.
*/
_thisArg = null
/**
* Initializes the object and sets all properties passed to the class
* constructor.
*
* @param {Object} props The properties passed to the contructor
*/
constructor (props) {
this.setProperties(props)
}
initialize (props) {
this.setProperties(props)
}
/**
* Sets a group of object properties.
*
* @param {Object} props Properties
*/
setProperties (props) {
for (const prop in props) {
this[prop] = props[prop]
}
}
/**
* Increases the object reference count.
*/
ref () {
this._refCount++
return this
}
/**
* Decreases the object reference count.
*/
unref () {
this._refCount--
if (this._refCount === 0) {
this._destroy()
}
}
/**
* Called from @Vn.Builder when it finds a custom tag as a child of the
* element.
*
* @param {Vn.Scope} scope The scope instance
* @param {Node} node The custom tag child nodes
*/
loadXml () {}
/**
* Called from @Vn.Builder when it finds a a child tag that isn't
* associated to any property.
*
* @param {Object} child The child object instance
*/
appendChild () {}
/**
* Conects a signal with a function.
*
* @param {string} id The signal identifier
* @param {function} callback The callback
* @param {Object} instance The instance
*/
on (id, callback, instance) {
if (!(callback instanceof Function)) {
console.warn("Vn.Object: Invalid callback for signal '%s'", id)
return
}
this._signalInit()
let callbacks = this._thisArg.signals[id]
if (!callbacks) {
callbacks = this._thisArg.signals[id] = []
}
callbacks.push({
blocked: false,
callback,
instance
})
}
/**
* Locks/Unlocks a signal emission to the specified object.
*
* @param {string} id The signal identifier
* @param {function} callback The callback
* @param {boolean} block %true for lock the signal, %false for unlock
*/
blockSignal (id, callback, block, instance) {
if (!this._thisArg) {
return
}
const callbacks = this._thisArg.signals[id]
if (!callbacks) {
return
}
for (let i = 0; i < callbacks.length; i++) {
if (
callbacks[i].callback === callback &&
callbacks[i].instance === instance
) {
callbacks[i].blocked = block
}
}
}
/**
* Emits a signal in the object.
*
* @param {string} id The signal identifier
*/
emit (id) {
if (!this._thisArg) {
return
}
const callbacks = this._thisArg.signals[id]
if (!callbacks) {
return
}
const callbackArgs = []
callbackArgs.push(this)
for (let i = 1; i < arguments.length; i++) {
callbackArgs.push(arguments[i])
}
for (let i = 0; i < callbacks.length; i++) {
if (!callbacks[i].blocked) {
callbacks[i].callback.apply(callbacks[i].instance, callbackArgs)
}
}
}
/**
* Disconnects a signal from current object.
*
* @param {string} id The signal identifier
* @param {function} callback The connected callback
* @param {Object} instance The instance
*/
disconnect (id, callback, instance) {
if (!this._thisArg) {
return
}
const callbacks = this._thisArg.signals[id]
if (callbacks) {
for (let i = callbacks.length; i--;) {
if (
callbacks[i].callback === callback &&
callbacks[i].instance === instance
) {
callbacks.splice(i, 1)
}
}
}
}
/**
* Disconnects all signals for the given instance.
*
* @param {Object} instance The instance
*/
disconnectByInstance (instance) {
if (!this._thisArg) {
return
}
const signals = this._thisArg.signals
for (const signalId in signals) {
const callbacks = signals[signalId]
if (callbacks) {
for (let i = callbacks.length; i--;) {
if (callbacks[i].instance === instance) {
callbacks.splice(i, 1)
}
}
}
}
}
/**
* Destroys the object, this method should only be called before losing
* the last reference to the object. It can be overwritten by child classes
* but should always call the parent method.
*/
_destroy () {
if (!this._thisArg) {
return
}
const links = this._thisArg.links
for (const key in links) {
this._unlink(links[key])
}
this._thisArg = null
}
/**
* Links the object with another object.
*
* @param {Object} prop The linked property
* @param {Object} handlers The object events to listen with
*/
link (prop, handlers) {
this._signalInit()
const links = this._thisArg.links
for (const key in prop) {
const newObject = prop[key]
const oldObject = this[key]
if (oldObject) {
this._unlink(oldObject)
}
this[key] = newObject
if (newObject) {
links[key] = newObject.ref()
for (const signal in handlers) {
newObject.on(signal, handlers[signal], this)
}
} else if (oldObject) {
links[key] = undefined
}
}
}
_unlink (object) {
if (!object) return
object.disconnectByInstance(this)
object.unref()
}
_signalInit () {
if (!this._thisArg) {
this._thisArg = {
signals: {},
links: {}
}
}
}
}

View File

@ -0,0 +1,34 @@
<template>
<QLayout
id="bg"
class="fullscreen row justify-center items-center layout-view scroll"
>
<div class="column q-pa-md row items-center justify-center">
<router-view v-slot="{ Component }">
<transition>
<component :is="Component" />
</transition>
</router-view>
</div>
</QLayout>
</template>
<style lang="scss" scoped>
#bg {
background: white;
}
.column {
width: 270px;
overflow: hidden;
& > * {
width: 100%;
}
}
</style>
<script>
export default {
name: 'LoginLayout'
};
</script>

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

@ -0,0 +1,215 @@
<script setup>
import { ref, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router';
import { useUserStore } from 'stores/user';
import { useAppStore } from 'stores/app';
const router = useRouter();
const userStore = useUserStore();
const appStore = useAppStore();
const { user, supplantedUser } = storeToRefs(userStore);
const { menuEssentialLinks, title, subtitle, useRightDrawer, rightDrawerOpen } =
storeToRefs(appStore);
const actions = ref(null);
const leftDrawerOpen = ref(false);
const toggleLeftDrawer = () => {
leftDrawerOpen.value = !leftDrawerOpen.value;
};
onMounted(async () => {
appStore.isHeaderMounted = true;
await userStore.fetchUser();
await appStore.loadConfig();
await userStore.supplantInit();
await appStore.getMenuLinks();
});
const logout = async () => {
await userStore.logout();
router.push('/login');
};
const logoutSupplantedUser = async () => {
await userStore.logoutSupplantedUser();
await appStore.getMenuLinks();
};
</script>
<template>
<QLayout view="lHh Lpr lFf">
<QHeader>
<QToolbar>
<QBtn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<QToolbarTitle>
{{ title }}
<div v-if="subtitle" class="subtitle text-caption">
{{ subtitle }}
</div>
</QToolbarTitle>
<div id="actions" ref="actions" class="flex items-center"></div>
<QBtn
v-if="useRightDrawer"
@click="rightDrawerOpen = !rightDrawerOpen"
aria-label="Menu"
flat
dense
round
>
<QIcon name="menu" />
</QBtn>
</QToolbar>
</QHeader>
<QDrawer v-model="leftDrawerOpen" :width="250" show-if-above>
<QToolbar class="logo">
<img src="statics/logo-dark.svg" />
</QToolbar>
<div class="user-info">
<div>
<span id="user-name">{{ user?.nickname }}</span>
<QBtn flat icon="logout" alt="_Exit" @click="logout()" />
</div>
<div v-if="supplantedUser" id="supplant" class="supplant">
<span id="supplanted">
{{ supplantedUser?.nickname }}
</span>
<QBtn
flat
icon="logout"
alt="_Exit"
@click="logoutSupplantedUser()"
/>
</div>
</div>
<QList v-for="item in menuEssentialLinks" :key="item.id">
<QItem v-if="!item.childs" :to="`/${item.path}`">
<QItemSection>
<QItemLabel>{{ $t(item.description) }}</QItemLabel>
</QItemSection>
</QItem>
<QExpansionItem
v-if="item.childs"
:label="$t(item.description)"
expand-separator
>
<QList>
<QItem
v-for="subitem in item.childs"
:key="subitem.id"
:to="`/${subitem.path}`"
class="q-pl-lg"
>
<QItemSection>
<QItemLabel>
{{ $t(subitem.description) }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QExpansionItem>
</QList>
</QDrawer>
<QPageContainer>
<router-view />
</QPageContainer>
</QLayout>
</template>
<style lang="scss" scoped>
.q-toolbar {
min-height: 64px;
}
.logo {
background-color: $primary;
justify-content: center;
& > img {
width: 160px;
}
}
.user-info {
margin: 25px;
& > div {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
border: 1px solid #eaeaea;
& > span {
padding: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
}
.q-btn {
display: block;
margin: 0;
padding: 9px;
border-radius: 0;
&:hover {
background-color: #1a1a1a;
color: white;
}
}
&.supplant {
border-top: none;
}
}
}
</style>
<style lang="scss">
@import 'src/css/responsive';
.q-drawer {
.q-item {
padding-left: 38px;
}
.q-list .q-list .q-item {
padding-left: 50px;
}
}
.q-page-container > * {
padding: 16px;
}
@include mobile {
#actions {
.q-btn {
border-radius: 50%;
padding: 10px;
&__content {
& > .q-icon {
margin-right: 0;
}
& > .block {
display: none !important;
}
}
}
}
}
</style>
<i18n lang="yaml">
en-US:
visitor: Visitor
es-ES:
visitor: Visitante
</i18n>

109
src/lib/filters.js Normal file
View File

@ -0,0 +1,109 @@
import { i18n } from 'src/boot/i18n';
import { date as qdate, format } from 'quasar';
import { useAppStore } from 'stores/app';
const { pad } = format;
export function currency(val) {
return typeof val === 'number' ? val.toFixed(2) + '€' : val;
}
export function date(val, format = 'YYYY-MM-DD') {
if (val == null) return val;
if (!(val instanceof Date)) {
val = new Date(val);
}
return qdate.formatDate(val, format, i18n.global.tm('date'));
}
export const formatDate = (timeStamp, format = 'YYYY-MM-DD') => {
if (!timeStamp) return '';
const appStore = useAppStore();
return qdate.formatDate(timeStamp, format, appStore.localeDates);
};
/**
* @param {Date} timeStamp - La marca de tiempo que se va a formatear. Si no se proporciona, la función devolverá una cadena vacía.
* @param {Object} options - Un objeto que contiene las opciones de formato.
* @param {boolean} options.showTime - Indica si se debe mostrar la hora en el formato de la fecha.
* @param {boolean} options.showSeconds - Indica si se deben mostrar los segundos en el formato de la hora. Solo se aplica si showTime es true.
* @param {boolean} options.shortDay - Indica si se debe usar una versión corta del día (por ejemplo, "Mon" en lugar de "Monday").
* @returns {string} La fecha formateada como un título.
*/
export const formatDateTitle = (
timeStamp,
options = { showTime: false, showSeconds: false, shortDay: false }
) => {
if (!timeStamp) return '';
const { t } = i18n.global;
const timeFormat = options.showTime
? options.showSeconds
? ` [${t('at')}] HH:mm:ss`
: ` [${t('at')}] HH:mm`
: '';
const day = options.shortDay ? 'dd' : 'dddd';
const formatString = `${day}, D [${t('of')}] MMMM [${t('of')}] YYYY${timeFormat}`;
const formattedString = formatDate(timeStamp, formatString);
return formattedString;
};
export function relDate(val) {
if (val == null) return val;
if (!(val instanceof Date)) {
val = new Date(val);
}
const dif = qdate.getDateDiff(new Date(), val, 'days');
let day;
switch (dif) {
case 0:
day = 'today';
break;
case 1:
day = 'yesterday';
break;
case -1:
day = 'tomorrow';
break;
}
if (day) {
day = i18n.global.t(day);
} else {
if (dif > 0 && dif <= 7) {
day = qdate.formatDate(val, 'ddd', i18n.global.tm('date'));
} else {
day = qdate.formatDate(val, 'ddd, MMMM Do', i18n.global.tm('date'));
}
}
return day;
}
export function relTime(val) {
if (val == null) return val;
if (!(val instanceof Date)) {
val = new Date(val);
}
return relDate(val) + ' ' + qdate.formatDate(val, 'H:mm:ss');
}
export function elapsedTime(val) {
if (val == null) return val;
if (!(val instanceof Date)) {
val = new Date(val);
}
const now = new Date().getTime();
val = Math.floor((now - val.getTime()) / 1000);
const hours = Math.floor(val / 3600);
val -= hours * 3600;
const minutes = Math.floor(val / 60);
val -= minutes * 60;
const seconds = val;
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}`;
}

View File

@ -0,0 +1,189 @@
<script setup>
import { ref, inject, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnForm from 'src/components/common/VnForm.vue';
import ChangePasswordForm from 'src/components/ui/ChangePasswordForm.vue';
import { useUserStore } from 'stores/user';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { t } = useI18n();
const jApi = inject('jApi');
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const vnFormRef = ref(null);
const vnFormRef2 = ref(null);
const changePasswordFormDialog = ref(null);
const showChangePasswordForm = ref(false);
const langOptions = ref([]);
const pks = computed(() => ({ id: userStore?.user?.id }));
const fetchConfigDataSql = {
query: `
SELECT u.id, u.name, u.email, u.nickname,
u.lang, c.isToBeMailed, c.id clientFk
FROM account.myUser u
LEFT JOIN myClient c
ON u.id = c.id`,
params: {}
};
const fetchLanguagesSql = async () => {
try {
const data = await jApi.query(
'SELECT code, name FROM language WHERE isActive'
);
langOptions.value = data;
} catch (error) {
console.error(error);
}
};
onMounted(() => fetchLanguagesSql());
</script>
<template>
<QPage>
<QPage class="q-pa-md flex justify-center">
<Teleport v-if="isHeaderMounted" to="#actions">
<QBtn
:label="t('addresses')"
icon="location_on"
rounded
no-caps
:to="{ name: 'addressesList' }"
/>
<QBtn
:label="t('changePassword')"
icon="lock_reset"
rounded
no-caps
@click="showChangePasswordForm = true"
/>
</Teleport>
<VnForm
ref="vnFormRef"
:title="t('personalInformation')"
:fetch-form-data-sql="fetchConfigDataSql"
:pks="pks"
table="myUser"
schema="account"
:default-actions="false"
>
<template #form="{ data }">
<VnInput
v-model="data.name"
:label="t('name')"
disable
:clearable="false"
/>
<VnInput
v-model="data.email"
:label="t('email')"
@keyup.enter="vnFormRef.submit()"
@blur="vnFormRef.submit()"
/>
<VnInput
v-model="data.nickname"
:label="t('nickname')"
@keyup.enter="vnFormRef.submit()"
@blur="vnFormRef.submit()"
/>
<VnSelect
v-model="data.lang"
:label="t('lang')"
option-label="name"
option-value="code"
:options="langOptions"
@update:model-value="vnFormRef.submit()"
/>
</template>
<template #extraForm>
<VnForm
class="no-form-container"
ref="vnFormRef2"
:pks="pks"
table="myClient"
schema="hedera"
:fetch-form-data-sql="fetchConfigDataSql"
:default-actions="false"
>
<template #form="{ data }">
<QCheckbox
v-model="data.isToBeMailed"
:label="t('isToBeMailed')"
@update:model-value="vnFormRef2.submit()"
dense
/>
</template>
</VnForm>
</template>
</VnForm>
</QPage>
<QDialog
ref="changePasswordFormDialog"
v-model="showChangePasswordForm"
>
<ChangePasswordForm
@on-password-changed="showChangePasswordForm = false"
/>
</QDialog>
</QPage>
</template>
<i18n lang="yaml">
en-US:
personalInformation: Personal Information
isToBeMailed: Receive invoices by email
name: Name
email: Email
nickname: Display name
lang: Language
receiveInvoicesByMail: Receive invoices by mail
addresses: Addresses
changePassword: Change password
es-ES:
personalInformation: Datos personales
isToBeMailed: Recibir facturas por correo electrónico
name: Nombre
email: Correo electrónico
nickname: Nombre a mostrar
lang: Idioma
receiveInvoicesByMail: Recibir facturas por correo
addresses: Direcciones
changePassword: Cambiar contraseña
ca-ES:
personalInformation: Dades personals
isToBeMailed: Rebre factures per correu electrònic
name: Nom
email: Correu electrònic
nickname: Nom a mostrar
lang: Idioma
receiveInvoicesByMail: Rebre factures per correu
addresses: Adreces
changePassword: Canviar contrasenya
fr-FR:
personalInformation: Informations personnelles
isToBeMailed: Recevoir des factures par e-mail
name: Nom
email: E-mail
nickname: Nom à afficher
lang: Langue
receiveInvoicesByMail: Recevoir des factures par courrier
addresses: Adresses
changePassword: Changer le mot de passe
pt-PT:
personalInformation: Dados pessoais
isToBeMailed: Receber facturas por e-mail
name: Nome
email: E-mail
nickname: Nom à afficher
lang: Língua
receiveInvoicesByMail: Receber faturas por correio
addresses: Endereços
changePassword: Alterar palavra-passe
</i18n>

View File

@ -0,0 +1,190 @@
<script setup>
import { ref, inject, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnForm from 'src/components/common/VnForm.vue';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const router = useRouter();
const route = useRoute();
const { t } = useI18n();
const jApi = inject('jApi');
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const vnFormRef = ref(null);
const countriesOptions = ref([]);
const provincesOptions = ref([]);
const pks = { id: route.params.id };
const isEditMode = route.params.id !== '0';
const fetchAddressDataSql = {
query: `
SELECT a.id, a.street, a.nickname, a.city, a.postalCode, a.provinceFk, p.countryFk
FROM myAddress a
LEFT JOIN vn.province p ON p.id = a.provinceFk
WHERE a.id = #address
`,
params: { address: route.params.id }
};
watch(
() => vnFormRef?.value?.formData?.countryFk,
async val => await getProvinces(val)
);
const goBack = () => router.push({ name: 'addressesList' });
const getCountries = async () => {
countriesOptions.value = await jApi.query(
`SELECT id, name FROM vn.country
ORDER BY name`
);
};
const getProvinces = async countryFk => {
if (!countryFk) return;
provincesOptions.value = await jApi.query(
`SELECT id, name FROM vn.province
WHERE countryFk = #id
ORDER BY name`,
{ id: countryFk }
);
};
onMounted(() => getCountries());
</script>
<template>
<QPage class="q-pa-md flex justify-center">
<Teleport v-if="isHeaderMounted" to="#actions">
<QBtn
:label="t('back')"
icon="close"
rounded
no-caps
@click="goBack()"
>
<QTooltip>
{{ t('back') }}
</QTooltip>
</QBtn>
</Teleport>
<VnForm
ref="vnFormRef"
:fetch-form-data-sql="fetchAddressDataSql"
:columns-to-ignore-update="['countryFk']"
:create-model-default="{
field: 'clientFk',
value: 'account.myUser_getId()'
}"
:pks="pks"
:is-edit-mode="isEditMode"
:title="t(isEditMode ? 'editAddress' : 'addAddress')"
table="myAddress"
schema="hedera"
@on-data-saved="goBack()"
>
<template #form="{ data }">
<VnInput
v-model="data.nickname"
:label="t('name')"
/>
<VnInput
v-model="data.street"
:label="t('address')"
/>
<VnInput
v-model="data.city"
:label="t('city')"
/>
<VnInput
v-model="data.postalCode"
:label="t('postalCode')"
/>
<VnSelect
v-model="data.countryFk"
:label="t('country')"
:options="countriesOptions"
@update:model-value="data.provinceFk = null"
/>
<VnSelect
v-model="data.provinceFk"
:label="t('province')"
:options="provincesOptions"
/>
</template>
</VnForm>
</QPage>
</template>
<style lang="scss" scoped>
.form-container {
width: 100%;
height: max-content;
max-width: 544px;
padding: 32px;
}
</style>
<i18n lang="yaml">
en-US:
accept: Accept
name: Consignee
address: Address
city: City
postalCode: Zip code
country: Country
province: Province
addressChangedSuccessfully: Address changed successfully
addAddress: Add address
editAddress: Edit address
es-ES:
accept: Aceptar
name: Consignatario
address: Dirección
city: Ciudad
postalCode: Código postal
country: País
province: Provincia
addressChangedSuccessfully: Dirección modificada correctamente
addAddress: Añadir dirección
editAddress: Modificar dirección
ca-ES:
accept: Acceptar
name: Consignatari
address: Direcció
city: Ciutat
postalCode: Codi postal
country: País
province: Província
addressChangedSuccessfully: Adreça modificada correctament
addAddress: Afegir adreça
editAddress: Modificar adreça
fr-FR:
accept: Accepter
name: Destinataire
address: Numéro Rue
city: Ville
postalCode: Code postal
country: Pays
province: Province
addressChangedSuccessfully: Adresse modifié avec succès
addAddress: Ajouter adresse
editAddress: Modifier adresse
pt-PT:
accept: Aceitar
name: Consignatario
address: Morada
city: Concelho
postalCode: Código postal
country: País
province: Distrito
addressChangedSuccessfully: Morada modificada corretamente
addAddress: Adicionar morada
editAddress: Modificar morada
</i18n>

View File

@ -0,0 +1,199 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref, onMounted, inject } from 'vue';
import { useRouter } from 'vue-router';
import CardList from 'src/components/ui/CardList.vue';
import useNotify from 'src/composables/useNotify.js';
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const router = useRouter();
const jApi = inject('jApi');
const { notify } = useNotify();
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const addresses = ref([]);
const defaultAddress = ref(null);
const clientId = ref(null);
const goToAddressDetails = (id = 0) =>
router.push({ name: 'addressDetails', params: { id } });
const getDefaultAddress = async () => {
try {
const [address] = await jApi.query(
'SELECT id, defaultAddressFk FROM myClient c'
);
defaultAddress.value = address.defaultAddressFk;
clientId.value = address.id;
} catch (error) {
console.error('Error getting default address:', error);
}
};
const getActiveAddresses = async () => {
try {
addresses.value = await jApi.query(
`SELECT a.id, a.nickname, p.name province, a.postalCode, a.city, a.street, a.isActive
FROM myAddress a
LEFT JOIN vn.province p ON p.id = a.provinceFk
WHERE a.isActive`
);
} catch (error) {
console.error('Error getting active addresses:', error);
}
};
const changeDefaultAddress = async () => {
if (!clientId.value) return;
await jApi.execQuery(
`UPDATE myClient
SET defaultAddressFk = #defaultAddress
WHERE id = #id;`,
{
defaultAddress: defaultAddress.value,
id: clientId.value
}
);
notify(t('defaultAddressModified'), 'positive');
};
const removeAddress = async id => {
try {
await jApi.execQuery(
`START TRANSACTION;
UPDATE hedera.myAddress SET isActive = FALSE
WHERE ((id = #id));
SELECT isActive FROM hedera.myAddress WHERE ((id = #id));
COMMIT`,
{
id
}
);
getActiveAddresses();
notify(t('dataSaved'), 'positive');
} catch (error) {
console.error('Error removing address:', error);
}
};
onMounted(async () => {
getDefaultAddress();
getActiveAddresses();
});
</script>
<template>
<Teleport v-if="isHeaderMounted" to="#actions">
<QBtn
:label="t('addAddress')"
icon="add"
@click="goToAddressDetails()"
rounded
no-caps
>
<QTooltip>
{{ t('addAddress') }}
</QTooltip>
</QBtn>
</Teleport>
<QPage class="vn-w-sm">
<QList
class="rounded-borders shadow-1 shadow-transition"
separator
>
<CardList
v-for="(address, index) in addresses"
:key="index"
:rounded="false"
tag="label"
>
<template #prepend>
<QRadio
v-model="defaultAddress"
:val="address.id"
class="q-mr-sm"
@update:model-value="changeDefaultAddress"
/>
</template>
<template #content>
<span class="text-bold q-mb-sm">
{{ address.nickname }}
</span>
<span>{{ address.street }}</span>
<span>
{{ address.postalCode }},
{{ address.city }}
</span>
</template>
<template #actions>
<QBtn
icon="delete"
flat
rounded
@click.stop="
openConfirmationModal(
null,
t('confirmDeleteAddress'),
() => removeAddress(address.id)
)
"
>
<QTooltip>
{{ t('deleteAddress') }}
</QTooltip>
</QBtn>
<QBtn
icon="edit"
flat
rounded
@click.stop="goToAddressDetails(address.id)"
>
<QTooltip>
{{ t('editAddress') }}
</QTooltip>
</QBtn>
</template>
</CardList>
</QList>
</QPage>
</template>
<i18n lang="yaml">
en-US:
addAddress: Add address
defaultAddressModified: Default address modified
confirmDeleteAddress: Are you sure you want to delete the address?
editAddress: Edit address
deleteAddress: Delete address
es-ES:
addAddress: Añadir dirección
defaultAddressModified: Dirección por defecto modificada
confirmDeleteAddress: ¿Estás seguro de que quieres borrar la dirección?
editAddress: Modificar dirección
deleteAddress: Borrar dirección
ca-ES:
addAddress: Afegir adreça
defaultAddressModified: Adreça per defecte modificada
confirmDeleteAddress: Estàs segur que vols esborrar l'adreça?
editAddress: Modificar adreça
deleteAddress: Esborrar adreça
fr-FR:
addAddress: Ajouter une adresse
defaultAddressModified: Adresse par défaut modifiée
confirmDeleteAddress: Êtes-vous sûr de vouloir supprimer l'adresse?
editAddress: Modifier adresse
deleteAddress: Supprimer adresse
pt-PT:
addAddress: Adicionar Morada
defaultAddressModified: Endereço padrão modificado
confirmDeleteAddress: Tem a certeza de que deseja excluir o endereço?
editAddress: Modificar morada
deleteAddress: Eliminar morada
</i18n>

View File

@ -0,0 +1,160 @@
<script setup>
import { ref, onMounted, inject, onBeforeUnmount } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardList from 'src/components/ui/CardList.vue';
import { date as qdate } from 'quasar';
import { useUserStore } from 'stores/user';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const { t } = useI18n();
const jApi = inject('jApi');
const router = useRouter();
const userStore = useUserStore();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const connections = ref([]);
const loading = ref(false);
const intervalId = ref(null);
const getConnections = async () => {
try {
loading.value = true;
connections.value = await jApi.query(
`SELECT vu.userFk userId, vu.stamp, u.nickname, s.lastUpdate,
a.platform, a.browser, a.version, u.name user
FROM userSession s
JOIN visitUser vu ON vu.id = s.userVisitFk
JOIN visitAccess ac ON ac.id = vu.accessFk
JOIN visitAgent a ON a.id = ac.agentFk
JOIN visit v ON v.id = a.visitFk
JOIN account.user u ON u.id = vu.userFk
ORDER BY lastUpdate DESC`
);
loading.value = false;
} catch (error) {
console.error('Error getting connections:', error);
}
};
const supplantUser = async user => {
try {
await userStore.supplantUser(user);
await appStore.getMenuLinks();
router.push({ name: 'confirmedOrders' });
} catch (error) {
console.error('Error supplanting user:', error);
}
};
onMounted(async () => {
getConnections();
intervalId.value = setInterval(getConnections, 60000);
});
onBeforeUnmount(() => clearInterval(intervalId.value));
</script>
<template>
<Teleport v-if="isHeaderMounted" to="#actions">
<div class="flex">
<QBtn
:label="t('refresh')"
icon="refresh"
@click="getConnections()"
rounded
no-caps
class="q-mr-sm"
>
<QTooltip>
{{ t('refresh') }}
</QTooltip>
</QBtn>
<QBadge class="q-pa-sm" v-if="connections.length" color="blue">
{{ connections?.length }} {{ t('connections') }}
</QBadge>
</div>
</Teleport>
<QPage class="vn-w-xs">
<QList class="flex justify-center">
<QSpinner
v-if="loading"
color="primary"
size="3em"
:thickness="2"
/>
<CardList
v-else
v-for="(connection, index) in connections"
:key="index"
>
<template #content>
<span class="text-bold q-mb-sm">
{{ connection.nickname }}
</span>
<span>
{{
qdate.formatDate(connection.stamp, 'dd, hh:mm:ss A')
}}
-
{{
qdate.formatDate(
connection.lastUpdate,
'hh:mm:ss A'
)
}}</span
>
<span
v-if="
connection.platform &&
connection.browser &&
connection.version
"
>
{{ connection.platform }} - {{ connection.browser }} -
{{ connection.version }}
</span>
</template>
<template #actions>
<QBtn
icon="people"
flat
rounded
@click="supplantUser(connection.user)"
>
<QTooltip>
{{ t('supplantUser') }}
</QTooltip>
</QBtn>
</template>
</CardList>
</QList>
</QPage>
</template>
<i18n lang="yaml">
en-US:
refresh: Refresh
connections: Connections
supplantUser: Supplant user
es-ES:
refresh: Actualizar
connections: Conexiones
supplantUser: Suplantar usuario
ca-ES:
refresh: Actualitzar
connections: Connexions
supplantUser: Suplantar usuari
fr-FR:
refresh: Rafraîchir
connections: Connexions
supplantUser: Supplanter l'utilisateur
pt-PT:
refresh: Atualizar
connections: Conexões
supplantUser: Suplantar usuário
</i18n>

View File

@ -0,0 +1,100 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import CardList from 'src/components/ui/CardList.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import VnSearchBar from 'src/components/ui/VnSearchBar.vue';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const { t } = useI18n();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const loading = ref(false);
const items = ref([]);
const query = `SELECT i.id, i.longName, i.size, i.category,
i.value5, i.value6, i.value7,
i.image, im.updated
FROM vn.item i
LEFT JOIN image im
ON im.collectionFk = 'catalog'
AND im.name = i.image
WHERE i.longName LIKE CONCAT('%', #search, '%')
OR i.id = #search
ORDER BY i.longName LIMIT 50`;
const onSearch = data => (items.value = data || []);
</script>
<template>
<Teleport v-if="isHeaderMounted" to="#actions">
<VnSearchBar
:sqlQuery="query"
@onSearch="onSearch"
@onSearchError="items = []"
/>
</Teleport>
<QPage class="vn-w-xs">
<QList class="flex justify-center">
<span v-if="!loading && !items.length" class="flex items-center">
<QIcon name="refresh" size="sm" class="q-mr-sm" />
{{ t('introduceSearchTerm') }}
</span>
<QSpinner
v-if="loading"
color="primary"
size="3em"
:thickness="2"
/>
<CardList
v-else
v-for="(item, index) in items"
:key="index"
:clickable="false"
>
<template #prepend>
<VnImg
storage="catalog"
size="200x200"
:id="item.id"
width="80px"
height="80px"
class="q-mr-md"
rounded
editable
editSchema="catalog"
:editImageName="item.image"
/>
</template>
<template #content>
<span class="text-bold q-mb-sm">
{{ item.longName }}
</span>
<span>
{{ item.value5 }} {{ item.value6 }}
{{ item.value7 }}
</span>
<span>{{ item.id }}</span>
<span>{{ item.image }}</span>
</template>
</CardList>
</QList>
</QPage>
</template>
<i18n lang="yaml">
en-US:
introduceSearchTerm: Enter a search term
es-ES:
introduceSearchTerm: Introduce un término de búsqueda
ca-ES:
introduceSearchTerm: Introdueix un terme de cerca
fr-FR:
introduceSearchTerm: Entrez un terme de recherche
pt-PT:
introduceSearchTerm: Digite um termo de pesquisa
</i18n>

View File

@ -0,0 +1,66 @@
<script setup>
import { ref, onMounted, inject } from 'vue';
const jApi = inject('jApi');
const links = ref([]);
const getLinks = async () => {
try {
links.value = await jApi.query(
`SELECT image, name, description, link FROM link
ORDER BY name`
);
} catch (error) {
console.error('Error getting links:', error);
}
};
onMounted(async () => getLinks());
</script>
<template>
<QPage>
<QList class="flex justify-center q-gutter-md">
<QItem
v-for="(link, index) in links"
:key="index"
:href="link.link"
target="_blank"
class="flex no-padding"
>
<QCard class="card-container">
<QImg
:src="`http://cdn.verdnatura.es/image/link/full/${link.image}`"
width="60px"
height="60px"
/>
<span class="card-title q-mt-md">{{ link.name }}</span>
<p class="card-description">{{ link.description }}</p>
</QCard>
</QItem>
</QList>
</QPage>
</template>
<style lang="scss" scoped>
.card-container {
width: 140px;
height: 170px;
padding: 15px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.card-title {
font-size: 0.7rem;
font-weight: bold;
}
.card-description {
font-size: 0.65rem;
text-align: center;
}
</style>

View File

@ -0,0 +1,250 @@
<script setup>
import { onMounted, ref, inject, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import VnImg from 'src/components/ui/VnImg.vue';
import VnForm from 'src/components/common/VnForm.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const jApi = inject('jApi');
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const newsTags = ref([]);
const pks = computed(() => ({ id: route.params.id }));
const isEditMode = !!route.params.id;
const formData = ref(
!route.params.id
? {
title: '',
tag: '',
priority: '',
text: ''
}
: undefined
);
const fetchNewDataSql = computed(() => {
if (!route.params.id) return undefined;
return {
query: `
SELECT id, title, text, tag, priority, image
FROM news WHERE id = #id`,
params: { id: route.params.id }
};
});
const getNewsTag = async () => {
try {
newsTags.value = await jApi.query(
`SELECT name, description FROM newsTag
ORDER BY description`
);
} catch (error) {
console.error('Error getting newsTag:', error);
}
};
const goBack = () => router.push({ name: 'adminNews' });
onMounted(async () => {
getNewsTag();
});
</script>
<template>
<QPage class="vn-w-sm">
<Teleport v-if="isHeaderMounted" to="#actions">
<QBtn
:label="t('back')"
icon="close"
rounded
no-caps
@click="goBack()"
>
<QTooltip>{{ t('back') }}</QTooltip>
</QBtn>
</Teleport>
<VnForm
ref="vnFormRef"
:fetch-form-data-sql="fetchNewDataSql"
:form-initial-data="formData"
:create-model-default="{
field: 'userFk',
value: 'account.myUser_getId()'
}"
:pks="pks"
:is-edit-mode="isEditMode"
table="news"
schema="hedera"
separation-between-inputs="lg"
@on-data-saved="goBack()"
>
<template #form="{ data }">
<VnImg
:id="data.image"
:edit-image-name="data.image"
storage="news"
edit-schema="news"
size="200x200"
width="80px"
height="80px"
class="full-width"
rounded
editable
always-show-edit-button
/>
<VnInput
v-model="data.title"
:label="t('title')"
:clearable="false"
/>
<div class="row justify-between q-gutter-x-md">
<VnSelect
v-model="data.tag"
:label="t('tag')"
option-label="description"
option-value="name"
:options="newsTags"
class="col"
/>
<VnInput
v-model="data.priority"
:label="t('priority')"
:clearable="false"
class="col"
/>
</div>
<QEditor
v-model="data.text"
:toolbar="[
[
{
label: $q.lang.editor.align,
icon: $q.iconSet.editor.align,
fixedLabel: true,
options: ['left', 'center', 'right', 'justify']
}
],
[
'bold',
'italic',
'strike',
'underline',
'subscript',
'superscript'
],
['token', 'hr', 'link', 'custom_btn'],
['print', 'fullscreen'],
[
{
label: $q.lang.editor.formatting,
icon: $q.iconSet.editor.formatting,
list: 'no-icons',
options: [
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'code'
]
},
{
label: $q.lang.editor.fontSize,
icon: $q.iconSet.editor.fontSize,
fixedLabel: true,
fixedIcon: true,
list: 'no-icons',
options: [
'size-1',
'size-2',
'size-3',
'size-4',
'size-5',
'size-6',
'size-7'
]
},
{
label: $q.lang.editor.defaultFont,
icon: $q.iconSet.editor.font,
fixedIcon: true,
list: 'no-icons',
options: [
'default_font',
'arial',
'arial_black',
'comic_sans',
'courier_new',
'impact',
'lucida_grande',
'times_new_roman',
'verdana'
]
},
'removeFormat'
],
['quote', 'unordered', 'ordered', 'outdent', 'indent'],
['undo', 'redo'],
['viewsource']
]"
:fonts="{
arial: 'Arial',
arial_black: 'Arial Black',
comic_sans: 'Comic Sans MS',
courier_new: 'Courier New',
impact: 'Impact',
lucida_grande: 'Lucida Grande',
times_new_roman: 'Times New Roman',
verdana: 'Verdana'
}"
/>
</template>
</VnForm>
</QPage>
</template>
<i18n lang="yaml">
en-US:
addNew: Add new
confirmDeleteAddress: Are you sure you want to delete this new?
title: Title
tag: Tag
priority: Priority
es-ES:
addNew: Añadir noticia
confirmDeleteAddress: ¿Estás seguro de que quieres eliminar esta noticia?
title: Título
tag: Etiqueta
priority: Prioridad
ca-ES:
addNew: Afegir noticia
confirmDeleteAddress: Estàs segur que vols eliminar aquesta notícia?
title: Títol
tag: Etiqueta
priority: Prioritat
fr-FR:
addNew: Ajouter nouvelles
confirmDeleteAddress: Êtes-vous sûr de vouloir supprimer cette nouvelle?
title: Titre
tag: Tag
priority: Priorité
pt-PT:
addNew: Adicionar noticia
confirmDeleteAddress: Tem a certeza que deseja eliminar esta notícia?
title: Título
tag: Etiqueta
priority: Prioridade
</i18n>

View File

@ -0,0 +1,138 @@
<script setup>
import { onMounted, ref, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import CardList from 'src/components/ui/CardList.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
import useNotify from 'src/composables/useNotify.js';
const jApi = inject('jApi');
const { t } = useI18n();
const appStore = useAppStore();
const { openConfirmationModal } = useVnConfirm();
const { isHeaderMounted } = storeToRefs(appStore);
const { notify } = useNotify();
const loading = ref(false);
const news = ref([]);
const getNews = async () => {
try {
news.value = await jApi.query(
`SELECT n.id, u.nickname, n.priority, n.image, n.title
FROM news n
JOIN account.user u ON u.id = n.userFk
ORDER BY priority, n.created DESC`
);
} catch (error) {
console.error('Error getting news:', error);
}
};
const deleteNew = async (id, index) => {
try {
await jApi.execQuery(
`START TRANSACTION;
DELETE FROM hedera.news WHERE ((id = #id));
COMMIT`,
{
id
}
);
news.value.splice(index, 1);
notify(t('dataSaved'), 'positive');
} catch (error) {
console.error('Error deleting news:', error);
}
};
onMounted(async () => getNews());
</script>
<template>
<Teleport v-if="isHeaderMounted" to="#actions">
<QBtn
:label="t('addNew')"
icon="add"
:to="{ name: 'adminNewsDetails' }"
rounded
no-caps
>
<QTooltip>{{ t('addNew') }}</QTooltip>
</QBtn>
</Teleport>
<QPage class="vn-w-xs">
<QList class="flex justify-center">
<QSpinner
v-if="loading"
color="primary"
size="3em"
:thickness="2"
/>
<CardList
v-else
v-for="(newsItem, index) in news"
:key="index"
:to="{ name: 'adminNewsDetails', params: { id: newsItem.id } }"
>
<template #prepend>
<VnImg
:id="newsItem.image"
:edit-image-name="newsItem.image"
storage="news"
edit-schema="news"
size="200x200"
width="80px"
height="80px"
class="q-mr-md"
rounded
editable
/>
</template>
<template #content>
<span class="text-bold q-mb-sm">{{ newsItem.title }} </span>
<span>{{ newsItem.nickname }} </span>
<span>{{ newsItem.priority }}</span>
</template>
<template #actions>
<QBtn
icon="delete"
flat
rounded
@click.stop.prevent="
openConfirmationModal(
null,
t('confirmDeleteAddress'),
() => deleteNew(newsItem.id, index)
)
"
>
<QTooltip>{{ t('remove') }}</QTooltip>
</QBtn>
</template>
</CardList>
</QList>
</QPage>
</template>
<i18n lang="yaml">
en-US:
addNew: Add new
confirmDeleteAddress: Are you sure you want to delete this new?
es-ES:
addNew: Añadir noticia
confirmDeleteAddress: ¿Estás seguro de que quieres eliminar esta noticia?
ca-ES:
addNew: Afegir noticia
confirmDeleteAddress: Estàs segur que vols eliminar aquesta notícia?
fr-FR:
addNew: Ajouter nouvelles
confirmDeleteAddress: Êtes-vous sûr de vouloir supprimer cette nouvelle?
pt-PT:
addNew: Adicionar noticia
confirmDeleteAddress: Tem a certeza que deseja eliminar esta notícia?
</i18n>

View File

@ -0,0 +1,278 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref, onMounted, inject, reactive, computed } from 'vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnForm from 'src/components/common/VnForm.vue';
import VnInput from 'src/components/common/VnInput.vue';
import useNotify from 'src/composables/useNotify.js';
const jApi = inject('jApi');
const api = inject('api');
const { t } = useI18n();
const { notify } = useNotify();
const fileUploaderRef = ref(null);
const statusIcons = {
uploading: 'cloud_upload',
fulfilled: 'cloud_done',
rejected: 'error',
pending: 'add'
};
const formInitialData = reactive({
schema: 'catalog',
updateMatching: true
});
const imageCollections = ref([]);
const addedFiles = ref([]);
const isSubmitable = computed(() =>
addedFiles.value.some(file => file.uploadStatus === 'pending')
);
const getImageCollections = async () => {
try {
imageCollections.value = await jApi.query(
'SELECT name, `desc` FROM imageCollection ORDER BY `desc`'
);
} catch (error) {
console.error('Error getting image collections:', error);
}
};
const onSubmit = async data => {
if (!addedFiles.value.length) {
notify(t('noFilesToUpload'), 'warning');
return;
}
const filteredFiles = addedFiles.value.filter(
file => file.uploadStatus === 'pending'
);
const promises = filteredFiles.map((file, index) => {
const fileIndex = filteredFiles[index].index;
addedFiles.value[fileIndex].uploadStatus = 'uploading';
const formData = new FormData();
formData.append('updateMatching', data.updateMatching);
formData.append('image', file.file);
formData.append('name', file.name);
formData.append('schema', data.schema);
formData.append('srv', 'json:image/upload');
return api({
method: 'post',
url: location.origin,
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
});
});
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
const fileIndex = filteredFiles[index].index;
addedFiles.value[fileIndex].uploadStatus = result.status;
});
const allSuccessful = results.every(
result => result.status === 'fulfilled'
);
if (allSuccessful) {
notify(t('uploadSuccess'), 'positive');
} else {
notify(t('uploadError'), 'negative');
}
};
const onFilesAdded = files => {
const initialFilesLength = addedFiles.value.length;
files.forEach((file, index) => {
const [name] = file.name.split('.');
const fileData = {
name,
file,
index: initialFilesLength + index,
uploadStatus: 'pending'
};
addedFiles.value.push(fileData);
});
};
const recalculateFilesIndexes = () => {
addedFiles.value.forEach((_, index) => {
addedFiles.value[index].index = index;
});
};
const removeFile = (file, index) => {
fileUploaderRef.value.removeFile(file);
addedFiles.value.splice(index, 1);
recalculateFilesIndexes();
};
const clearFiles = () => {
fileUploaderRef.value.reset();
addedFiles.value = [];
};
onMounted(async () => getImageCollections());
</script>
<template>
<QPage class="vn-w-sm">
<VnForm
ref="vnFormRef"
:defaultActions="false"
:formInitialData="formInitialData"
separationBetweenInputs="md"
showBottomActions
>
<template #form="{ data }">
<VnSelect
v-model="data.schema"
:label="t('collection')"
option-label="desc"
option-value="name"
:options="imageCollections"
/>
<QUploader
ref="fileUploaderRef"
:label="t('dropYourFiles')"
class="full-width"
square
flat
multiple
bordered
hide-upload-btn
@added="onFilesAdded"
>
<template v-slot:list="scope">
<QList v-if="addedFiles.length" separator>
<QItem
v-for="(file, index) in scope.files"
:key="file.__key"
class="flex full-width row items-center justify-center"
>
<img
:src="file.__img.src"
style="width: 28px; height: 21px"
class="q-mr-md"
/>
<VnInput
v-model="addedFiles[index].name"
:clearable="false"
dense
class="full-width"
/>
<QSpinner
v-if="
addedFiles[index].uploadStatus ===
'uploading'
"
color="primary"
size="2em"
:thickness="1"
/>
<QIcon
v-else-if="
addedFiles[index].uploadStatus &&
addedFiles[index].uploadStatus !==
'uploading'
"
:name="
statusIcons[
addedFiles[index].uploadStatus
]
"
size="sm"
/>
<QBtn
v-if="
addedFiles[index].uploadStatus !==
'uploading'
"
class="gt-xs"
size="md"
flat
dense
round
icon="delete"
@click="removeFile(file, index)"
/>
</QItem>
</QList>
</template>
</QUploader>
<QCheckbox
v-model="data.updateMatching"
:label="t('updateMatching')"
/>
</template>
<template #actions="{ data }">
<QBtn
:label="t('clearAll')"
rounded
no-caps
flat
@click="clearFiles()"
/>
<QBtn
:label="t('uploadFiles')"
rounded
no-caps
flat
:disable="!isSubmitable"
@click="onSubmit(data)"
/>
</template>
</VnForm>
</QPage>
</template>
<i18n lang="yaml">
en-US:
collection: Collection
updateMatching: Update items with matching id
dropYourFiles: Click or drop files here
clearAll: Clear all
uploadFiles: Upload files
uploadSuccess: Upload finished successfully
uploadError: Some errors happened on upload
noFilesToUpload: There are no files to upload
es-ES:
collection: Colección
updateMatching: Actualizar artículos con id coincidente
dropYourFiles: Pulsa o suelta los archivos aquí
clearAll: Limpiar todo
uploadFiles: Subir archivos
uploadSuccess: Imágenes subidas correctamente
uploadError: Ocurrieron errores al subir alguna de las imágenes
noFilesToUpload: No se han seleccionado archivos para subir
ca-ES:
collection: Col·lecció
updateMatching: Actualitzar els elements amb id coincident
dropYourFiles: Prem o deixa anar els arxius aquí
clearAll: Netejar tot
uploadFiles: Pujar arxius
uploadSuccess: Imatges pujades correctament
uploadError: Van ocórrer errors en pujar alguna de les imatges
noFilesToUpload: No s'ha seleccionat arxius per pujar
fr-FR:
collection: Collection
updateMatching: Mettre à jour les éléments avec l'identifiant correspondant
dropYourFiles: Cliquez ici ou déposer des fichiers
clearAll: Tout effacer
uploadFiles: Upload Files
uploadSuccess: Les images téléchargées correctement
uploadError: Des erreurs sont survenues lors du téléchargement des images
noFilesToUpload: Aucun fichier sélectionné pour télécharger
pt-PT:
collection: Coleção
updateMatching: Atualizar itens com id correspondente
dropYourFiles: Clique ou solte arquivos aqui
clearAll: Limpar tudo
uploadFiles: Fazer upload de arquivos
uploadSuccess: Upload concluído com sucesso
uploadError: Ocorreram erros ao subir alguma das imagens
noFilesToUpload: Não arquivos selecionados para upload
</i18n>

View File

@ -0,0 +1,120 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import CardList from 'src/components/ui/CardList.vue';
import VnSearchBar from 'src/components/ui/VnSearchBar.vue';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
import { useUserStore } from 'stores/user';
import useNotify from 'src/composables/useNotify.js';
const { t } = useI18n();
const router = useRouter();
const userStore = useUserStore();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const { notify } = useNotify();
const loading = ref(false);
const users = ref([]);
const query = `SELECT u.id, u.name, u.nickname, u.active
FROM account.user u
WHERE u.name LIKE CONCAT('%', #user, '%')
OR u.nickname LIKE CONCAT('%', #user, '%')
OR u.id = #user
ORDER BY u.name LIMIT 200`;
const onSearch = data => (users.value = data || []);
const supplantUser = async user => {
try {
await userStore.supplantUser(user);
await appStore.getMenuLinks();
router.push({ name: 'confirmedOrders' });
} catch (error) {
console.error('Error supplanting user:', error);
notify(error.message, 'negative');
}
};
</script>
<template>
<Teleport v-if="isHeaderMounted" to="#actions">
<VnSearchBar
:sql-query="query"
search-field="user"
@on-search="onSearch"
@on-search-error="users = []"
/>
</Teleport>
<QPage class="vn-w-xs">
<QList class="flex justify-center">
<span v-if="!loading && !users.length" class="flex items-center">
<QIcon name="refresh" size="sm" class="q-mr-sm" />
{{ t('noData') }}
</span>
<QSpinner
v-if="loading"
color="primary"
size="3em"
:thickness="2"
/>
<CardList
v-else
v-for="(user, index) in users"
:key="index"
:clickable="false"
>
<template #content>
<span class="text-bold q-mb-sm">
{{ user.nickname }}
</span>
<span>#{{ user.id }} - {{ user.name }} </span>
</template>
<template #actions>
<QBtn
icon="people"
flat
rounded
@click="supplantUser(user.name)"
><QTooltip>
{{ t('Impersonate user') }}
</QTooltip></QBtn
>
</template>
</CardList>
</QList>
</QPage>
</template>
<i18n lang="yaml">
en-US:
User management: User management
Disabled: Disabled
Impersonate user: Impersonate user
Access log: Access log
es-ES:
User management: Gestión de usuarios
Disabled: Desactivado
Impersonate user: Suplantar usuario
Access log: Registro de accesos
ca-ES:
User management: Gestió d'usuaris
Disabled: Deshabilitat
Impersonate user: Suplantar usuari
Access log: Registre d'accessos
fr-FR:
User management: Gestion des utilisateurs
Disabled: Désactivé
Impersonate user: Accès utilisateur
Access log: Journal des accès
pt-PT:
User management: Gestão de usuarios
Disabled: Desativado
Impersonate user: Suplantar usuario
Access log: Registro de acessos
</i18n>

View File

@ -0,0 +1,176 @@
<script setup>
import { ref, inject, watch, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import { formatDateTitle, date } from 'src/lib/filters.js';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const { t } = useI18n();
const jApi = inject('jApi');
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const loading = ref(false);
const from = ref(Date.vnNew(route.query.from) || Date.vnNew());
const to = ref(Date.vnNew(route.query.to) || Date.vnNew());
const visitsData = ref(null);
const getVisits = async () => {
try {
loading.value = true;
const [visitsResponse] = await jApi.query(
`SELECT browser,
MIN(CAST(version AS DECIMAL(4,1))) minVersion,
MAX(CAST(version AS DECIMAL(4,1))) maxVersion,
MAX(c.stamp) lastVisit,
COUNT(DISTINCT c.id) visits,
SUM(a.firstAccessFk = c.id AND v.firstAgentFk = a.id) newVisits
FROM visitUser e
JOIN visitAccess c ON c.id = e.accessFk
JOIN visitAgent a ON a.id = c.agentFk
JOIN visit v ON v.id = a.visitFk
WHERE c.stamp BETWEEN TIMESTAMP(#from,'00:00:00') AND TIMESTAMP(#to,'23:59:59')
GROUP BY browser ORDER BY visits DESC`,
{
from: date(from.value),
to: date(to.value)
}
);
visitsData.value = visitsResponse;
loading.value = false;
} catch (error) {
console.error('Error getting visits:', error);
}
};
const visitsCardText = computed(
() =>
`${visitsData?.value?.visits || 0} ${t('visits')}, ${visitsData?.value?.newVisits || 0} ${t('news')}`
);
watch(
[() => from.value, () => to.value],
async () => {
await router.replace({
query: {
from: date(from.value),
to: date(to.value)
}
});
await getVisits();
},
{ immediate: true }
);
</script>
<template>
<Teleport v-if="isHeaderMounted" to="#actions">
<QBtn
:label="t('refresh')"
icon="refresh"
@click="getVisits()"
rounded
no-caps
class="q-mr-sm"
>
<QTooltip>
{{ t('refresh') }}
</QTooltip>
</QBtn>
<QBtn
:label="t('connections')"
icon="visibility"
rounded
no-caps
:to="{ name: 'adminConnections' }"
>
<QTooltip>
{{ t('connections') }}
</QTooltip>
</QBtn>
</Teleport>
<QPage class="vn-w-xs column">
<QCard class="column q-pa-lg q-mb-md">
<VnInputDate :label="t('from')" v-model="from" class="q-mb-sm" />
<VnInputDate :label="t('to')" v-model="to" />
</QCard>
<QCard v-if="!loading" class="q-pa-lg flex q-mb-md">
<span class="full-width text-right text-h6">
{{ visitsCardText }}
</span>
</QCard>
<QCard v-if="!loading" class="q-pa-lg column">
<span
v-if="
visitsData?.browser &&
visitsData?.minVersion &&
visitsData?.maxVersion
"
>
{{ visitsData?.browser }} - {{ visitsData?.minVersion }} -
{{ visitsData?.maxVersion }}
</span>
<span>{{ visitsCardText }}</span>
<span v-if="visitsData">
{{
formatDateTitle(visitsData.lastVisit, {
showTime: true,
showSeconds: true,
shortDay: true
})
}}
</span>
</QCard>
<QSpinner
v-else
color="primary"
size="3em"
:thickness="2"
style="margin: 0 auto"
/>
</QPage>
</template>
<i18n lang="yaml">
en-US:
from: From
to: To
visits: Visits
news: New
connections: Connections
refresh: Refresh
es-ES:
from: Desde
to: Hasta
visits: Visitas
news: Nuevas
connections: Conexiones
refresh: Actualizar
ca-ES:
from: Desde
to: Fins
visits: Visites
news: Noves
connections: Connexions
refresh: Actualitzar
fr-FR:
from: À partir de
to: Jusqu'à
visits: Visites
news: Nouveau
connections: Connexions
refresh: Rafraîchir
pt-PT:
from: Desde
to: Até
visits: Visitas
news: Novo
connections: Conexões
refresh: Atualizar
</i18n>

View File

@ -0,0 +1,88 @@
<script setup>
import { ref, inject, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import VnTable from 'src/components/ui/VnTable.vue';
const jApi = inject('jApi');
const { t } = useI18n();
const packages = ref([]);
const columns = computed(() => [
{
label: t('agency'),
name: 'agency',
field: 'Agencia',
align: 'left',
sortable: true
},
{
label: t('expeditions'),
name: 'expeditions',
field: 'expediciones',
align: 'right',
sortable: true
},
{
label: t('bundles'),
name: 'bundles',
field: 'Bultos',
align: 'right',
sortable: true
},
{
label: t('prevision'),
name: 'prevision',
field: 'Faltan',
align: 'right',
sortable: true
}
]);
const getPackages = async () => {
try {
const data = await jApi.query('CALL vn.agencyVolume()');
packages.value = data;
} catch (error) {
console.error(error);
}
};
onMounted(() => getPackages());
</script>
<template>
<QPage class="flex justify-center q-pa-md">
<VnTable
:columns="columns"
:rows="packages"
:loading="loading"
style="height: max-content; max-width: 100%"
/>
</QPage>
</template>
<i18n lang="yaml">
en-US:
bundles: Bundles
expeditions: Exps.
prevision: Prev.
es-ES:
bundles: Bultos
expeditions: Exps.
prevision: Prev.
ca-ES:
bundles: Paquets
expeditions: Exps.
prevision: Prev.
fr-FR:
bundles: Cartons
expeditions: Exps.
prevision: Prev.
pt-PT:
bundles: Bultos
expeditions: Exps.
prevision: Prev.
</i18n>

77
src/pages/Cms/Home.vue Normal file
View File

@ -0,0 +1,77 @@
<template>
<div style="padding: 0">
<div class="q-pa-sm row items-start">
<div class="new-card q-pa-sm" v-for="myNew in news" :key="myNew.id">
<QCard>
<QImg :src="`${$app.imageUrl}/news/full/${myNew.image}`">
</QImg>
<QCardSection>
<div class="text-h5">{{ myNew.title }}</div>
</QCardSection>
<QCardSection class="new-body">
<div v-html="myNew.text" />
</QCardSection>
</QCard>
</div>
</div>
<QPageSticky>
<QBtn
fab
icon="add_shopping_cart"
color="accent"
to="/ecomerce/catalog"
:title="$t('startOrder')"
/>
</QPageSticky>
</div>
</template>
<style lang="scss" scoped>
.new-card {
width: 100%;
@media screen and (min-width: 800px) and (max-width: 1400px) {
width: 50%;
}
@media screen and (min-width: 1401px) and (max-width: 1920px) {
width: 33.33%;
}
@media screen and (min-width: 19021) {
width: 25%;
}
}
.new-body {
font-family: 'Open Sans';
}
</style>
<script>
export default {
name: 'PageIndex',
data() {
return {
news: []
};
},
async mounted() {
this.news = await this.$jApi.query(
`SELECT title, text, image, id
FROM news
ORDER BY priority, created DESC`
);
}
};
</script>
<i18n lang="yaml">
en-US:
startOrder: Start order
es-ES:
startOrder: Empezar pedido
ca-ES:
startOrder: Començar comanda
fr-FR:
startOrder: Lancer commande
pt-PT:
startOrder: Comece uma encomenda
</i18n>

102
src/pages/Cms/HomeView.vue Normal file
View File

@ -0,0 +1,102 @@
<script setup>
import { ref, onMounted, inject } from 'vue';
import VnImg from 'src/components/ui/VnImg.vue';
const jApi = inject('jApi');
const news = ref([]);
const showPreview = ref(false);
const selectedImageSrc = ref('');
const fetchData = async () => {
news.value = await jApi.query(
`SELECT title, text, image, id
FROM news
ORDER BY priority, created DESC`
);
};
onMounted(async () => await fetchData());
</script>
<template>
<div style="padding: 0">
<div class="q-pa-sm row items-start">
<div
class="new-card q-pa-sm"
v-for="myNew in news"
:key="myNew.id"
>
<QCard>
<VnImg
:id="myNew.image"
storage="news"
/>
<QCardSection>
<div class="text-h5">
{{ myNew.title }}
</div>
</QCardSection>
<QCardSection class="new-body">
<div
v-html="myNew.text"
class="card-text"
/>
</QCardSection>
</QCard>
</div>
</div>
<QPageSticky>
<QBtn
fab
icon="add_shopping_cart"
color="accent"
to="/ecomerce/catalog"
:title="$t('startOrder')"
/>
</QPageSticky>
</div>
<QDialog
v-model="showPreview"
@hide="selectedImageSrc = ''"
>
<QImg :src="selectedImageSrc" />
</QDialog>
</template>
<style lang="scss" scoped>
.new-card {
width: 100%;
@media screen and (min-width: 800px) and (max-width: 1400px) {
width: 50%;
}
@media screen and (min-width: 1401px) and (max-width: 1920px) {
width: 33.33%;
}
@media screen and (min-width: 19021) {
width: 25%;
}
}
.new-body {
font-family: 'Open Sans';
}
.card-text {
:deep(a) {
color: $accent;
}
}
</style>
<i18n lang="yaml">
en-US:
startOrder: Start order
es-ES:
startOrder: Empezar pedido
ca-ES:
startOrder: Començar comanda
fr-FR:
startOrder: Lancer commande
pt-PT:
startOrder: Comece uma encomenda
</i18n>

Some files were not shown because too many files have changed in this diff Show More