Compare commits

...

44 Commits

Author SHA1 Message Date
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
98 changed files with 33457 additions and 17446 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

82
.eslintrc.js Normal file
View File

@ -0,0 +1,82 @@
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: [
// 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)
'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: [
{
extends: ['plugin:vue/vue3-essential'],
files: ['src/**/*.{js,vue,scss}'], // Aplica ESLint solo a archivos .js, .vue y .scss dentro de src (Proyecto de quasar)
rules: {
semi: 'off',
indent: ['error', 4, { SwitchCase: 1 }],
'space-before-function-paren': 'off'
}
}
]
};

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/
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
}
]
}

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

@ -0,0 +1,10 @@
{
"files.eol": "\n",
"eslint.autoFixOnSave": true,
"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"]
}

98
Jenkinsfile vendored
View File

@ -1,34 +1,25 @@
#!/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 {
agent any
environment {
PROJECT_NAME = 'hedera-web'
STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}"
}
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') {
when {
anyOf {
@ -38,31 +29,28 @@ pipeline {
}
agent {
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/'
registryCredentialsId 'docker-registry'
args '-v /mnt/appdata/reprepro:/reprepro'
}
}
steps {
sh 'debuild -us -uc -b'
sh 'vn-includedeb stretch'
sh 'mkdir -p debuild'
sh 'mv ../hedera-web_* debuild'
script {
def files = findFiles(glob: 'debuild/*.changes')
files.each { file -> env.CHANGES_FILE = file.name }
}
configFileProvider([
configFile(fileId: "dput.cf", variable: 'DPUT_CONFIG')
]) {
sshagent(credentials: ['jenkins-agent']) {
sh 'dput --config "$DPUT_CONFIG" verdnatura "debuild/$CHANGES_FILE"'
}
}
stage('Container') {
when {
anyOf {
branch 'master'
branch 'test'
}
}
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') {
@ -73,15 +61,41 @@ pipeline {
}
}
environment {
DOCKER_HOST = "${env.SWARM_HOST}"
CREDS = credentials('docker-registry')
IMAGE = "$REGISTRY/verdnatura/hedera-web"
}
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 {
unsuccessful {
setEnv()
sendEmail()
}
}

View File

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

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"
]
}

9320
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,18 +3,31 @@
"version": "22.48.2",
"description": "Verdnatura web page",
"license": "GPL-3.0",
"productName": "Salix",
"author": "Verdnatura",
"repository": {
"type": "git",
"url": "https://git.verdnatura.es/hedera-web"
},
"devDependencies": {
"@babel/eslint-parser": "^7.13.14",
"@babel/preset-env": "^7.20.2",
"@intlify/vue-i18n-loader": "^4.2.0",
"@quasar/app-webpack": "^3.0.0",
"archiver": "^5.3.1",
"assets-webpack-plugin": "^7.1.1",
"babel-loader": "^9.1.0",
"bundle-loader": "^0.5.6",
"css-loader": "^6.7.2",
"eslint": "^8.15.0",
"css-loader": "^5.2.7",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-n": "^15.0.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^9.27.0",
"eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0",
"fs-extra": "^10.1.0",
"glob": "^8.0.3",
@ -22,6 +35,10 @@
"json-loader": "^0.5.7",
"mini-css-extract-plugin": "^2.7.0",
"node-sass": "^7.0.1",
"postcss": "^8.4.39",
"postcss-import": "^13.0.0",
"postcss-loader": "^4.3.0",
"postcss-url": "^10.1.3",
"raw-loader": "^4.0.2",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
@ -33,17 +50,41 @@
"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"
"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 ../salix && gulp backOnly",
"db": "cd ../vn-database && myvc run",
"back": "cd ../vn-database && myvc start && cd ../salix && gulp backOnly",
"build": "rm -rf build/ ; webpack",
"clean": "rm -rf build/"
"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', '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 @@
<template>
<router-view />
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App'
});
</script>

View File

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

After

Width:  |  Height:  |  Size: 4.4 KiB

0
src/boot/.gitkeep Normal file
View File

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

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

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

@ -0,0 +1,66 @@
import { boot } from 'quasar/wrappers';
import { Connection } from '../js/db/connection';
import { userStore } 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 = '';
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 user = userStore();
function addToken(config) {
if (user.token) {
config.headers.Authorization = user.token;
}
return config;
}
api.interceptors.request.use(addToken, onRequestError);
api.interceptors.response.use(response => response, onResponseError);
jApi.use(addToken);
// 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 };

View File

@ -0,0 +1,234 @@
<script setup>
import { ref, inject, onMounted, computed, Teleport } from 'vue';
import { useI18n } from 'vue-i18n';
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
}
});
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const jApi = inject('jApi');
const { notify } = useNotify();
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 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 && props.autoLoad) {
fetchFormData();
}
});
defineExpose({
formData,
submit
});
</script>
<template>
<QCard class="form-container" v-bind="$attrs">
<QForm
v-if="!loading"
ref="addressFormRef"
class="column full-width q-gutter-y-xs"
@submit="submit()"
>
<span class="text-h6 text-bold">
{{ title }}
</span>
<slot name="form" :data="formData" />
<component
:is="showBottomActions ? 'div' : Teleport"
:to="$actions"
class="flex row justify-end q-gutter-x-sm"
:class="{ 'q-mt-md': showBottomActions }"
>
<QBtn
v-if="defaultActions"
:label="t('cancel')"
:icon="showBottomActions ? undefined : 'check'"
rounded
no-caps
flat
v-close-popup
/>
<QBtn
v-if="defaultActions"
:label="t('save')"
type="submit"
:icon="showBottomActions ? undefined : 'check'"
rounded
no-caps
flat
:disabled="!showBottomActions && !updatedColumns.length"
/>
<slot name="actions" />
</component>
</QForm>
<QSpinner v-else color="primary" size="3em" :thickness="2" />
</QCard>
</template>
<style lang="scss" scoped>
.form-container {
width: 100%;
height: max-content;
max-width: 544px;
padding: 32px;
display: flex;
justify-content: center;
}
</style>

View File

@ -0,0 +1,124 @@
<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 } = vnInputRef.value.$attrs;
if (min >= 0) {
if (Math.floor(val) < min) return t('inputMin', { value: min });
}
}
];
</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}
es-ES:
inputMin: Must be more than {value}
ca-ES:
inputMin: Ha de ser més gran que {value}
fr-FR:
inputMin: Doit être supérieur à {value}
pt-PT:
inputMin: Deve ser maior que {value}
</i18n>

View File

@ -0,0 +1,188 @@
<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,273 @@
<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 { userStore as useUserStore } from 'stores/user';
import useNotify from 'src/composables/useNotify.js';
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 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.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.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')"
:formInitialData="formData"
:saveFn="changePassword"
showBottomActions
:defaultActions="false"
style="max-width: 300px"
@onDataSaved="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 #actions>
<QBtn
:label="t('requirements')"
rounded
no-caps
flat
@click="passwordRequirementsDialogRef.show()"
/>
<QBtn :label="t('modify')" type="submit" rounded no-caps flat />
</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,127 @@
<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"></span>
<slot name="customHTML"></slot>
</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:
confirm: Confirm
wantToContinue: Are you sure you want to continue?
cancel: Cancel
es-ES:
confirm: Confirmar
wantToContinue: ¿Seguro que quieres continuar?
cancel: Cancelar
ca-ES:
confirm: Confirmar
wantToContinue: Segur que vols continuar?
cancel: Cancel·lar
fr-FR:
confirm: Confirmer
wantToContinue: Êtes-vous sûr de vouloir continuer?
cancel: Annuler
pt-PT:
confirm: Confirme
wantToContinue: Tem a certeza de que deseja continuar?
cancel: Cancelar
</i18n>

View File

@ -0,0 +1,66 @@
<script setup>
import { ref, computed } from 'vue';
import { appStore } from 'stores/app';
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
}
});
const app = appStore();
const show = ref(false);
const url = computed(() => {
return `${$props.baseURL ?? app.imageUrl}/${$props.storage}/${$props.size}/${$props.id}`;
});
</script>
<template>
<QImg
:class="{ zoomIn: $props.zoomSize }"
:src="url"
v-bind="$attrs"
@click="show = !show"
spinner-color="primary"
/>
<QDialog v-model="show" v-if="$props.zoomSize">
<QImg
:src="url"
size="full"
class="img_zoom"
v-bind="$attrs"
spinner-color="primary"
/>
</QDialog>
</template>
<style lang="scss" scoped>
.q-img {
&.zoomIn {
cursor: zoom-in;
}
min-width: 50px;
}
.rounded {
border-radius: 50%;
}
.img_zoom {
border-radius: 0%;
}
</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,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 };
}

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

@ -0,0 +1,38 @@
// 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;
}
}
.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;
}

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;
$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;
}

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

@ -0,0 +1,80 @@
// 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'
],
shortMonths: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
},
// menu
home: 'Home',
catalog: 'Catalog',
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'
};

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

@ -0,0 +1,98 @@
// This is just an example,
// so you can safely delete all default props below
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'
],
shortMonths: [
'Ene',
'Feb',
'Mar',
'Abr',
'May',
'Jun',
'Jul',
'Ago',
'Sep',
'Oct',
'Nov',
'Dic'
]
},
// Menu
home: 'Inicio',
catalog: 'Catálogo',
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'
};

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

@ -0,0 +1,7 @@
import enUS from './en-US'
import esES from './es-ES'
export default {
'en-US': enUS,
'es-ES': esES
}

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>

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

@ -0,0 +1,184 @@
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
}

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

@ -0,0 +1,130 @@
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
}
}

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

@ -0,0 +1,52 @@
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, ...columnsUpdated].join(', ');
const values = [
createModelDefault.value,
...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,200 @@
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 = []
use (fn) {
this.interceptors.push(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) {
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>

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

@ -0,0 +1,239 @@
<template>
<QLayout view="lHh Lpr lFf">
<QHeader>
<QToolbar>
<QBtn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<QToolbarTitle>
{{ $app.title }}
<div v-if="$app.subtitle" class="subtitle text-caption">
{{ $app.subtitle }}
</div>
</QToolbarTitle>
<div id="actions" ref="actions"></div>
<QBtn
v-if="$app.useRightDrawer"
@click="$app.rightDrawerOpen = !$app.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 id="supplant" class="supplant">
<span id="supplanted">{{ supplantedUser }}</span>
<QBtn flat icon="logout" alt="_Exit" />
</div>
</div>
<QList v-for="item in essentialLinks" :key="item.id">
<QItem v-if="!item.childs" :to="`/${item.path}`">
<QItemSection>
<QItemLabel>{{ item.description }}</QItemLabel>
</QItemSection>
</QItem>
<QExpansionItem
v-if="item.childs"
:label="item.description"
expand-separator
>
<QList>
<QItem
v-for="subitem in item.childs"
:key="subitem.id"
:to="`/${subitem.path}`"
class="q-pl-lg"
>
<QItemSection>
<QItemLabel>
{{ 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 {
display: none;
border-top: none;
&.show {
display: flex;
}
}
}
}
</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;
}
#actions > div {
display: flex;
align-items: center;
}
@include mobile {
#actions > div {
.q-btn {
border-radius: 50%;
padding: 10px;
&__content {
& > .q-icon {
margin-right: 0;
}
& > .block {
display: none !important;
}
}
}
}
}
</style>
<script>
import { defineComponent, ref } from 'vue';
import { userStore } from 'stores/user';
export default defineComponent({
name: 'MainLayout',
props: {},
setup() {
const leftDrawerOpen = ref(false);
return {
user: userStore(),
supplantedUser: ref(''),
essentialLinks: ref(null),
leftDrawerOpen,
toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
};
},
async mounted() {
this.$refs.actions.appendChild(this.$actions);
await this.user.loadData();
await this.$app.loadConfig();
await this.fetchData();
},
methods: {
async fetchData() {
const sections = await this.$jApi.query('SELECT * FROM myMenu');
const sectionMap = new Map();
for (const section of sections) {
sectionMap.set(section.id, section);
}
const sectionTree = [];
for (const section of sections) {
const parent = section.parentFk;
if (parent) {
const parentSection = sectionMap.get(parent);
if (!parentSection) continue;
let childs = parentSection.childs;
if (!childs) {
childs = parentSection.childs = [];
}
childs.push(section);
} else {
sectionTree.push(section);
}
}
this.essentialLinks = sectionTree;
},
async logout() {
this.user.logout();
this.$router.push('/login');
}
}
});
</script>
<i18n lang="yaml">
en-US:
visitor: Visitor
es-ES:
visitor: Visitante
</i18n>

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

@ -0,0 +1,73 @@
import { date as qdate, format } from 'quasar'
const { pad } = format
export function currency (val) {
return typeof val === 'number' ? val.toFixed(2) + '€' : val
}
export function date (val, format) {
if (val == null) return val
if (!(val instanceof Date)) {
val = new Date(val)
}
return qdate.formatDate(val, format, window.i18n.tm('date'))
}
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 = window.i18n.t(day)
} else {
if (dif > 0 && dif <= 7) {
day = qdate.formatDate(val, 'ddd', window.i18n.tm('date'))
} else {
day = qdate.formatDate(val, 'ddd, MMMM Do', window.i18n.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,159 @@
<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 { userStore as useUserStore } from 'stores/user';
const userStore = useUserStore();
const { t } = useI18n();
const jApi = inject('jApi');
const vnFormRef = ref(null);
const changePasswordFormDialog = ref(null);
const showChangePasswordForm = ref(false);
const langOptions = ref([]);
const pks = computed(() => ({ id: userStore.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 :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')"
:fetchFormDataSql="fetchConfigDataSql"
:pks="pks"
table="myUser"
schema="account"
:defaultActions="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:modelValue="vnFormRef.submit()"
/>
</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
name: Name
email: Email
nickname: Display name
lang: Language
receiveInvoicesByMail: Receive invoices by mail
addresses: Addresses
changePassword: Change password
es-ES:
personalInformation: Datos personales
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
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
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
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,169 @@
<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';
const router = useRouter();
const route = useRoute();
const { t } = useI18n();
const jApi = inject('jApi');
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 :to="$actions">
<QBtn
:label="t('back')"
icon="close"
rounded
no-caps
@click="goBack()"
/>
</Teleport>
<VnForm
ref="vnFormRef"
:fetchFormDataSql="fetchAddressDataSql"
:columnsToIgnoreUpdate="['countryFk']"
:createModelDefault="{
field: 'clientFk',
value: 'account.myUser_getId()'
}"
:pks="pks"
:isEditMode="isEditMode"
:title="t('addEditAddress')"
table="myAddress"
schema="hedera"
@onDataSaved="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:modelValue="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:
back: Back
accept: Accept
addEditAddress: Add or edit address
name: Consignee
address: Address
city: City
postalCode: Zip code
country: Country
province: Province
addressChangedSuccessfully: Address changed successfully
es-ES:
back: Volver
accept: Aceptar
addEditAddress: Añadir o modificar dirección
name: Consignatario
address: Morada
city: Ciudad
postalCode: Código postal
country: País
province: Distrito
addressChangedSuccessfully: Dirección modificada correctamente
ca-ES:
back: Tornar
accept: Acceptar
addEditAddress: Afegir o modificar adreça
name: Consignatari
address: Direcció
city: Ciutat
postalCode: Codi postal
country: País
province: Província
addressChangedSuccessfully: Adreça modificada correctament
fr-FR:
back: Retour
accept: Accepter
addEditAddress: Ajouter ou modifier l'adresse
name: Destinataire
address: Numéro Rue
city: Ville
postalCode: Code postal
country: Pays
province: Province
addressChangedSuccessfully: Adresse modifié avec succès
pt-PT:
back: Voltar
accept: Aceitar
addEditAddress: Adicionar ou modificar morada
name: Consignatario
address: Morada
city: Concelho
postalCode: Código postal
country: País
province: Distrito
addressChangedSuccessfully: Morada modificada corretamente
</i18n>

View File

@ -0,0 +1,190 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref, onMounted, inject } from 'vue';
import { useRouter } from 'vue-router';
import useNotify from 'src/composables/useNotify.js';
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
const router = useRouter();
const jApi = inject('jApi');
const { notify } = useNotify();
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
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 :to="$actions">
<QBtn
:label="t('addAddress')"
icon="add"
@click="goToAddressDetails()"
rounded
no-caps
/>
</Teleport>
<QPage class="column items-center">
<QList
class="full-width rounded-borders shadow-1 shadow-transition"
style="max-width: 544px"
separator
>
<QItem
v-for="(address, index) in addresses"
:key="index"
clickable
v-ripple
tag="label"
class="full-width row items-center justify-between address-item"
style="padding: 20px"
>
<QItemSection>
<div class="row">
<QRadio
v-model="defaultAddress"
:val="address.id"
class="q-mr-sm"
@update:model-value="changeDefaultAddress"
/>
<div>
<QItemLabel class="text-bold q-mb-sm">
{{ address.nickname }}
</QItemLabel>
<QItemLabel>{{ address.street }}</QItemLabel>
<QItemLabel>
{{ address.postalCode }},
{{ address.city }}
</QItemLabel>
</div>
</div>
</QItemSection>
<QItemSection class="actions-wrapper" side>
<QBtn
icon="delete"
flat
rounded
@click.stop="
openConfirmationModal(
null,
t('confirmDeleteAddress'),
() => removeAddress(address.id)
)
"
/>
<QBtn
icon="edit"
flat
rounded
@click.stop="goToAddressDetails(address.id)"
/>
</QItemSection>
</QItem>
</QList>
</QPage>
</template>
<style lang="scss" scoped>
.address-item {
.actions-wrapper {
visibility: hidden;
}
&:hover {
.actions-wrapper {
visibility: visible;
}
}
}
</style>
<i18n lang="yaml">
en-US:
addAddress: Add address
defaultAddressModified: Default address modified
confirmDeleteAddress: Are you sure you want to delete the 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?
ca-ES:
addAddress: Afegir adreça
defaultAddressModified: Adreça per defecte modificada
confirmDeleteAddress: Estàs segur que vols esborrar l'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?
pt-PT:
addAddress: Adicionar Morada
defaultAddressModified: Endereço padrão modificado
confirmDeleteAddress: Tem a certeza de que deseja excluir o endereço?
</i18n>

View File

@ -0,0 +1,108 @@
<script setup>
import { ref, inject, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
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">
<QTable
:columns="columns"
:rows="packages"
:loading="loading"
class="q-mt-lg"
style="max-width: 100%; height: max-content"
table-header-class="packages-table-header"
hide-bottom
>
<template #body-cell-id="{ row }">
<QTd auto-width @click.stop>
<QBtn flat color="blue">{{ row.id }}</QBtn>
<ItemDescriptorProxy :id="row.id" />
</QTd>
</template>
</QTable>
</QPage>
</template>
<style lang="scss">
.packages-table-header {
background-color: $accent !important;
color: white;
}
</style>
<i18n lang="yaml">
en-US:
agency: Agency
bundles: Bundles
expeditions: Exps.
prevision: Prev.
es-ES:
agency: Agencia
bundles: Bultos
expeditions: Exps.
prevision: Prev.
ca-ES:
agency: Agència
bundles: Paquets
expeditions: Exps.
prevision: Prev.
fr-FR:
agency: Agence
bundles: Cartons
expeditions: Exps.
prevision: Prev.
pt-PT:
agency: Agência
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>

View File

@ -0,0 +1,87 @@
<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>

View File

@ -0,0 +1,685 @@
<template>
<Teleport :to="$actions">
<QInput
:placeholder="$t('search')"
v-model="search"
debounce="500"
class="search q-mr-sm"
rounded
dark
dense
standout
>
<template v-slot:prepend>
<QIcon v-if="search === ''" name="search" />
<QIcon
v-else
name="clear"
class="cursor-pointer"
@click="search = ''"
/>
</template>
</QInput>
<QBtn
:icon="$t(viewMode == 'list' ? 'view_list' : 'grid_on')"
:label="$t(viewMode == 'list' ? 'listView' : 'gridView')"
@click="onViewModeClick()"
rounded
no-caps
/>
</Teleport>
<div style="padding-bottom: 5em">
<QDrawer v-model="$app.rightDrawerOpen" side="right" :width="250">
<div class="q-pa-md">
<div class="basket-info">
<p>{{ date(new Date()) }}</p>
<p>
{{ $t('warehouse') }}
{{ 'Algemesi' }}
</p>
<QBtn flat rounded no-caps>
{{ $t('modify') }}
</QBtn>
</div>
<div class="q-mt-md">
<div class="q-mb-xs text-grey-7">
{{ $t('category') }}
<QIcon
v-if="category"
style="font-size: 1.3em"
name="cancel"
class="cursor-pointer"
:title="$t('deleteFilter')"
@click="
$router.push({ params: { category: null } })
"
/>
</div>
<div class="categories">
<QBtn
flat
round
class="category q-pa-sm"
v-for="cat in categories"
:class="{ active: category == cat.id }"
:key="cat.id"
:title="cat.name"
:to="{ params: { category: cat.id, type: null } }"
>
<img :src="`statics/category/${cat.code}.svg`" />
</QBtn>
</div>
</div>
<div class="q-mt-md" v-if="category || search">
<div class="q-mb-xs text-grey-7">
{{ $t('filterBy') }}
</div>
<QSelect
v-model="type"
option-value="id"
option-label="name"
:options="types"
:disable="!category"
clearable
:label="$t('family')"
@filter="filterType"
@input="
$router.push({ params: { type: type && type.id } })
"
/>
<QSelect
v-model="order"
input-debounce="0"
:options="orderOptions"
:label="$t('orderBy')"
/>
</div>
</div>
<div class="q-pa-md" v-if="typeId || search">
<div class="q-mb-md" v-for="tag in tags" :key="tag.uid">
<div class="q-mb-xs text-caption text-grey-7">
{{ tag.name }}
<QIcon
v-if="tag.hasFilter"
style="font-size: 1.3em"
name="cancel"
:title="$t('deleteFilter')"
class="cursor-pointer"
@click="onResetTagFilterClick(tag)"
/>
</div>
<div v-if="!tag.useRange">
<div
v-for="value in tag.values.slice(0, tag.showCount)"
:key="value"
>
<QCheckbox
v-model="tag.filter"
:dense="true"
:val="value"
:label="value"
@input="onCheck(tag)"
/>
</div>
<div v-if="tag.values.length > tag.showCount">
<span
class="cursor-pointer text-blue"
@click="tag.showCount = Infinity"
>
<QIcon name="keyboard_arrow_down" />
{{ $t('viewMore') }}
</span>
</div>
<div v-if="tag.showCount == Infinity">
<span
class="cursor-pointer text-blue"
@click="tag.showCount = tag.initialCount"
>
<QIcon name="keyboard_arrow_up" />
{{ $t('viewLess') }}
</span>
</div>
</div>
<div class="q-mx-md">
<QRange
class="q-mt-lg"
v-if="tag.useRange"
v-model="tag.filter"
:min="tag.min"
:max="tag.max"
:step="tag.step"
:color="tag.hasFilter ? 'primary' : 'grey-6'"
@input="onRangeChange(tag, true)"
@change="onRangeChange(tag)"
label-always
markers
snap
/>
</div>
</div>
</div>
</QDrawer>
<QInfiniteScroll
@load="onLoad"
scroll-taget="html"
:offset="800"
:disable="disableScroll"
>
<div class="q-pa-md row justify-center q-gutter-md">
<QSpinner v-if="isLoading" color="primary" size="50px">
</QSpinner>
<div
v-if="items && !items.length"
class="text-subtitle1 text-grey-7 q-pa-md"
>
{{ $t('noItemsFound') }}
</div>
<div
v-if="!items && !isLoading"
class="text-subtitle1 text-grey-7 q-pa-md"
>
{{ $t('pleaseSetFilter') }}
</div>
<QCard class="my-card" v-for="item in items" :key="item.id">
<img :src="`${$imageBase}/catalog/200x200/${item.image}`" />
<QCardSection>
<div class="name text-subtitle1">
{{ item.longName }}
</div>
<div
class="sub-name text-uppercase text-subtitle1 text-grey-7 ellipsize q-pt-xs"
>
{{ item.subName }}
</div>
<div class="tags q-pt-xs">
<div v-for="tag in item.tags" :key="tag.tagFk">
<span class="text-grey-7">{{
tag.tag.name
}}</span>
{{ tag.value }}
</div>
</div>
</QCardSection>
<QCardActions class="actions justify-between">
<div class="q-pl-sm">
<span class="available bg-green text-white">{{
item.available
}}</span>
{{ $t('from') }}
<span class="price">{{
currency(item.buy?.price3)
}}</span>
</div>
<QBtn
icon="add_shopping_cart"
:title="$t('buy')"
@click="showItem(item)"
flat
>
</QBtn>
</QCardActions>
</QCard>
</div>
<template v-slot:loading>
<div class="row justify-center q-my-md">
<QSpinner color="primary" name="dots" size="40px" />
</div>
</template>
</QInfiniteScroll>
<QDialog v-model="showItemDialog">
<QCard style="width: 25em">
<QImg
:src="`${$imageBase}/catalog/200x200/${item.image}`"
:ratio="5 / 3"
>
<div class="absolute-bottom text-center q-pa-xs">
<div class="text-subtitle1">
{{ item.longName }}
</div>
</div>
</QImg>
<QCardSection>
<div
class="text-uppercase text-subtitle1 text-grey-7 ellipsize"
>
{{ item.subName }}
</div>
<div class="text-grey-7">#{{ item.id }}</div>
</QCardSection>
<QCardSection>
<div v-for="tag in item.tags" :key="tag.tagFk">
<span class="text-grey-7">{{ tag.tag.name }}</span>
{{ tag.value }}
</div>
</QCardSection>
<QCardActions align="right">
<QBtn @click="showItemDialog = false" flat>
{{ $t('cancel') }}
</QBtn>
<QBtn @click="showItemDialog = false" flat>
{{ $t('accept') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
<QPageSticky>
<QBtn
fab
to="/ecomerce/basket"
icon="shopping_cart"
color="accent"
:title="$t('shoppingCart')"
/>
</QPageSticky>
</div>
</template>
<style lang="scss" scoped>
.search {
max-width: 250px;
}
.basket-info {
background-color: #8cc63f;
color: white;
padding: 17px 28px;
border-radius: 7px;
text-align: center;
& > p {
margin: 0;
padding: 0.4em 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.categories {
margin: 0 auto;
width: 220px;
.category {
width: 55px;
&.active {
background: rgba(0, 0, 0, 0.08);
}
& > img {
height: 40px;
width: 40px;
}
}
}
.tags {
max-height: 4.6em;
overflow: hidden;
}
.available {
padding: 0.15em;
border-radius: 0.2em;
font-size: 1.3em;
}
.price {
font-size: 1.3em;
}
.my-card {
width: 100%;
max-width: 17.5em;
height: 32.5em;
overflow: hidden;
.name,
.sub-name {
line-height: 1.3em;
}
.ellipsize {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.description {
height: 40px;
overflow: hidden;
}
}
</style>
<script>
import { date, currency } from 'src/lib/filters.js';
import { date as qdate } from 'quasar';
import axios from 'axios';
const CancelToken = axios.CancelToken;
export default {
name: 'HederaCatalog',
data() {
return {
uid: 0,
search: '',
orderDate: qdate.formatDate(new Date(), 'YYYY/MM/DD'),
category: null,
categories: [],
type: null,
typeId: null,
types: [],
orgTypes: [],
item: {},
showItemDialog: false,
tags: [],
isLoading: false,
items: null,
limit: null,
pageSize: 30,
maxTags: 5,
disableScroll: true,
viewMode: 'list',
order: {
label: this.$t('relevancy'),
value: 'relevancy DESC, longName'
},
orderOptions: [
{
label: this.$t('relevancy'),
value: 'relevancy DESC, longName'
},
{
label: this.$t('name'),
value: 'longName'
},
{
label: this.$t('siceAsc'),
value: 'size ASC'
},
{
label: this.$t('sizeDesc'),
value: 'size DESC'
},
{
label: this.$t('priceAsc'),
value: 'price ASC'
},
{
label: this.$t('priceDesc'),
value: 'price DESC'
},
{
label: this.$t('available'),
value: 'available'
}
]
};
},
created() {
this.$app.useRightDrawer = true;
},
async mounted() {
this.categories = await this.$jApi.query(
`SELECT c.id, l.name, c.color, c.code
FROM vn.itemCategory c
JOIN vn.itemCategoryL10n l ON l.id = c.id
WHERE c.display
ORDER BY display`
);
this.onRouteChange(this.$route);
},
beforeUnmount() {
this.clearTimeoutAndRequest();
},
beforeRouteUpdate(to, from, next) {
this.onRouteChange(to);
next();
},
watch: {
categories() {
this.refreshTitle();
},
orgTypes() {
this.refreshTitle();
},
order() {
this.loadItems();
},
date() {
this.loadItems();
},
async category(value) {
this.orgTypes = [];
if (!value) return;
const res = await this.$jApi.execQuery(
`CALL myBasket_getAvailable;
SELECT DISTINCT t.id, l.name
FROM vn.item i
JOIN vn.itemType t ON t.id = i.typeFk
JOIN tmp.itemAvailable a ON a.id = i.id
JOIN vn.itemTypeL10n l ON l.id = t.id
WHERE t.\`order\` >= 0
AND t.categoryFk = #category
ORDER BY t.\`order\`, l.name;
DROP TEMPORARY TABLE tmp.itemAvailable;`,
{ category: value }
);
res.fetch();
this.orgTypes = res.fetchData();
},
search(value) {
const location = { params: this.$route.params };
if (value) location.query = { search: value };
this.$router.push(location);
}
},
methods: {
date,
currency,
onViewModeClick() {
this.viewMode = this.viewMode === 'list' ? 'grid' : 'list';
},
onRouteChange(route) {
let { category, type } = route.params;
category = parseInt(category) || null;
type = parseInt(type) || null;
this.category = category;
this.typeId = category ? type : null;
this.search = route.query.search || '';
this.tags = [];
this.refreshTitle();
this.loadItems();
},
refreshTitle() {
let title = this.$t(this.$router.currentRoute.value.name);
let subtitle;
if (this.category) {
const category =
this.categories.find(i => i.id === this.category) || {};
title = category.name;
}
if (this.typeId) {
this.type = this.orgTypes.find(i => i.id === this.typeId);
subtitle = title;
title = this.type && this.type.name;
} else {
this.type = null;
}
this.$app.$patch({ title, subtitle });
},
clearTimeoutAndRequest() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (this.source) {
this.source.cancel();
this.source = null;
}
},
loadItemsDelayed() {
this.clearTimeoutAndRequest();
this.timeout = setTimeout(() => this.loadItems(), 500);
},
loadItems() {
this.items = null;
this.isLoading = true;
this.limit = this.pageSize;
this.disableScroll = false;
this.isLoading = false;
// this.loadItemsBase().finally(() => (this.isLoading = false))
},
onLoad(index, done) {
if (this.isLoading) return done();
this.limit += this.pageSize;
done();
// this.loadItemsBase().finally(done)
},
loadItemsBase() {
this.clearTimeoutAndRequest();
if (!(this.category || this.typeId || this.search)) {
this.tags = [];
return Promise.resolve(true);
}
const tagFilter = [];
for (const tag of this.tags) {
if (tag.hasFilter) {
tagFilter.push({
tagFk: tag.id,
values: tag.filter
});
}
}
this.source = CancelToken.source();
const params = {
dated: this.orderDate,
typeFk: this.typeId,
categoryFk: this.category,
search: this.search,
order: this.order.value,
limit: this.limit,
tagFilter
};
const config = {
params,
cancelToken: this.source.token
};
return this.$axios
.get('Items/catalog', config)
.then(res => this.onItemsGet(res))
.catch(err => this.onItemsError(err))
.finally(() => (this.cancel = null));
},
onItemsError(err) {
if (err.__CANCEL__) return;
this.disableScroll = true;
throw err;
},
onItemsGet(res) {
for (const tag of res.data.tags) {
tag.uid = this.uid++;
if (tag.filter) {
tag.hasFilter = true;
tag.useRange = tag.filter.max || tag.filter.min;
} else {
tag.useRange =
tag.isQuantitative && tag.values.length > this.maxTags;
this.resetTagFilter(tag);
}
if (tag.values) {
tag.initialCount = this.maxTags;
if (Array.isArray(tag.filter)) {
tag.initialCount = Math.max(
tag.initialCount,
tag.filter.length
);
}
tag.showCount = tag.initialCount;
}
}
this.items = res.data.items;
this.tags = res.data.tags;
this.disableScroll = this.items.length < this.limit;
},
onRangeChange(tag, delay) {
tag.hasFilter = true;
if (!delay) this.loadItems();
else this.loadItemsDelayed();
},
onCheck(tag) {
tag.hasFilter = tag.filter.length > 0;
this.loadItems();
},
resetTagFilter(tag) {
tag.hasFilter = false;
if (tag.useRange) {
tag.filter = {
min: tag.min,
max: tag.max
};
} else {
tag.filter = [];
}
},
onResetTagFilterClick(tag) {
this.resetTagFilter(tag);
this.loadItems();
},
filterType(val, update) {
if (val === '') {
update(() => {
this.types = this.orgTypes;
});
} else {
update(() => {
const needle = val.toLowerCase();
this.types = this.orgTypes.filter(
type => type.name.toLowerCase().indexOf(needle) > -1
);
});
}
},
showItem(item) {
this.item = item;
this.showItemDialog = true;
const conf = this.$state.catalogConfig;
const params = {
dated: this.orderDate,
addressFk: conf.addressFk,
agencyModeFk: conf.agencyModeFk
};
this.$axios
.get(`Items/${item.id}/calcCatalog`, { params })
.then(res => (this.lots = res.data));
}
}
};
</script>
<i18n lang="yaml">
es-ES:
gridView: Vista de rejilla
listView: Vista de lista
shoppingCart: Cesta de la compra
warehouse: Almacén
agency: Agencia
modify: Modificar
category: Categoría
deleteFilter: Quitar filtro
filterBy: Filtrar por
family: Familia
orderBy: Ordernar por
pleaseSetFilter: Elige un filtro en el menú de la derecha
search: Buscar
</i18n>

View File

@ -0,0 +1,183 @@
<template>
<Teleport :to="$actions">
<QSelect
v-model="year"
:options="years"
color="white"
dark
standout
dense
rounded
/>
</Teleport>
<div class="vn-w-sm">
<div
v-if="!invoices?.length"
class="text-subtitle1 text-center text-grey-7 q-pa-md"
>
{{ $t('noInvoicesFound') }}
</div>
<QCard v-if="invoices?.length">
<QTable
:columns="columns"
:pagination="pagination"
:rows="invoices"
row-key="id"
hide-header
hide-bottom
>
<template v-slot:body="props">
<QTr :props="props">
<QTd key="ref" :props="props">
{{ props.row.ref }}
</QTd>
<QTd key="issued" :props="props">
{{ date(props.row.issued, 'ddd, MMMM Do') }}
</QTd>
<QTd key="amount" :props="props">
{{ currency(props.row.amount) }}
</QTd>
<QTd key="hasPdf" :props="props">
<QBtn
v-if="props.row.hasPdf"
icon="download"
:title="$t('downloadInvoicePdf')"
:href="invoiceUrl(props.row.id)"
target="_blank"
flat
round
/>
<QIcon
v-else
name="warning"
:title="$t('notDownloadable')"
color="warning"
size="24px"
/>
</QTd>
</QTr>
</template>
</QTable>
</QCard>
</div>
</template>
<script>
import { date, currency } from 'src/lib/filters.js';
export default {
name: 'OrdersPendingIndex',
data() {
const curYear = new Date().getFullYear();
const years = [];
for (let year = curYear - 5; year <= curYear; year++) {
years.push(year);
}
return {
columns: [
{ name: 'ref', label: 'serial', field: 'ref', align: 'left' },
{
name: 'issued',
label: 'issued',
field: 'issued',
align: 'left'
},
{ name: 'amount', label: 'amount', field: 'amount' },
{
name: 'hasPdf',
label: 'download',
field: 'hasPdf',
align: 'center'
}
],
pagination: {
rowsPerPage: 0
},
year: curYear,
years,
invoices: null
};
},
async mounted() {
await this.loadData();
},
watch: {
async year() {
await this.loadData();
}
},
methods: {
date,
currency,
async loadData() {
const params = {
from: new Date(this.year, 0),
to: new Date(this.year, 11, 31, 23, 59, 59)
};
this._invoices = await this.$jApi.query(
`SELECT id, ref, issued, amount, hasPdf
FROM myInvoice
WHERE issued BETWEEN #from AND #to
ORDER BY issued DESC
LIMIT 500`,
params
);
},
invoiceUrl(id) {
return (
'?' +
new URLSearchParams({
srv: 'rest:dms/invoice',
invoice: id,
access_token: this.$user.token
}).toString()
);
}
}
};
</script>
<i18n lang="yaml">
en-US:
noInvoicesFound: No invoices found
serial: Serial
issued: Date
amount: Import
downloadInvoicePdf: Download invoice PDF
notDownloadable: Not available for download, request the invoice to your salesperson
es-ES:
noInvoicesFound: No se han encontrado facturas
serial: Serie
issued: Fecha
amount: Importe
downloadInvoicePdf: Descargar factura en PDF
notDownloadable: No disponible para descarga, solicita la factura a tu comercial
ca-ES:
noInvoicesFound: No s'han trobat factures
serial: Sèrie
issued: Data
amount: Import
downloadInvoicePdf: Descarregar PDF
notDownloadable: No disponible per cescarrega, sol·licita la factura al teu comercial
fr-FR:
noInvoicesFound: Aucune facture trouvée
serial: Série
issued: Date
amount: Montant
downloadInvoicePdf: Télécharger le PDF
notDownloadable: Non disponible en téléchargement, demander la facture à votre commercial
pt-PT:
noInvoicesFound: Nenhuma fatura encontrada
serial: Serie
issued: Data
amount: Importe
downloadInvoicePdf: Baixar PDF
notDownloadable: Não disponível para download, solicite a fatura ao seu comercial
</i18n>

View File

@ -0,0 +1,199 @@
<template>
<Teleport :to="$actions">
<div class="balance">
<span class="label">{{ $t('balance') }}</span>
<span class="amount" :class="{ negative: debt < 0 }">
{{ currency(debt || 0) }}
</span>
<QIcon
name="info"
:title="$t('paymentInfo')"
class="info"
size="24px"
/>
</div>
<QBtn
icon="payments"
:label="$t('makePayment')"
@click="onPayClick()"
rounded
no-caps
/>
<QBtn
to="/ecomerce/basket"
icon="shopping_cart"
:label="$t('shoppingCart')"
rounded
no-caps
/>
</Teleport>
<div class="vn-w-sm">
<div
v-if="!orders?.length"
class="text-subtitle1 text-center text-grey-7 q-pa-md"
>
{{ $t('noOrdersFound') }}
</div>
<QCard v-if="orders?.length">
<QList bordered separator padding>
<QItem
v-for="order in orders"
:key="order.id"
:to="`ticket/${order.id}`"
clickable
v-ripple
>
<QItemSection>
<QItemLabel>
{{ date(order.landed, 'ddd, MMMM Do') }}
</QItemLabel>
<QItemLabel caption>#{{ order.id }}</QItemLabel>
<QItemLabel caption>{{ order.nickname }}</QItemLabel>
<QItemLabel caption>{{ order.agency }}</QItemLabel>
</QItemSection>
<QItemSection side top> {{ order.total }} </QItemSection>
</QItem>
</QList>
</QCard>
<QPageSticky>
<QBtn
fab
icon="add_shopping_cart"
color="accent"
to="/ecomerce/catalog"
:title="$t('startOrder')"
/>
</QPageSticky>
</div>
</template>
<style lang="scss" scoped>
.balance {
margin-right: 8px;
white-space: nowrap;
display: inline-block;
& > * {
vertical-align: middle;
}
& > .amount {
padding: 4px;
margin: 0 4px;
&.negative {
background-color: #e55;
border-radius: 2px;
box-shadow: 0 0 5px #333;
}
}
& > .info {
cursor: pointer;
}
}
</style>
<script>
import { date, currency } from 'src/lib/filters.js';
import { tpvStore } from 'stores/tpv';
export default {
name: 'OrdersPendingIndex',
data() {
return {
orders: null,
debt: 0,
tpv: tpvStore()
};
},
async mounted() {
await this.tpv.check(this.$route);
this.orders = await this.$jApi.query('CALL myTicket_list(NULL, NULL)');
this.debt = await this.$jApi.getValue('SELECT -myClient_getDebt(NULL)');
},
methods: {
date,
currency,
async onPayClick() {
let amount = -this.debt;
amount = amount <= 0 ? null : amount;
let defaultAmountStr = '';
if (amount !== null) {
defaultAmountStr = amount;
}
amount = prompt(this.$t('amountToPay'), defaultAmountStr);
if (amount != null) {
amount = parseFloat(amount.replace(',', '.'));
await this.tpv.pay(amount);
}
}
}
};
</script>
<i18n lang="yaml">
en-US:
startOrder: Start order
noOrdersFound: No orders found
makePayment: Make payment
shoppingCart: Shopping cart
balance: 'Balance:'
paymentInfo: >-
The amount shown is your slope (negative) or favorable balance today, it
disregards future orders. For get your order shipped, this amount must be
equal to or greater than 0. If you want to make a down payment, click the
payment button, delete the suggested amount and enter the amount you want.
es-ES:
startOrder: Empezar pedido
noOrdersFound: No se encontrado pedidos
makePayment: Realizar pago
shoppingCart: Cesta de la compra
balance: 'Saldo:'
paymentInfo: >-
La cantidad mostrada es tu saldo pendiente (negativa) o favorable a día de
hoy, no tiene en cuenta pedidos del futuro. Para que tu pedido sea enviado,
esta cantidad debe ser igual o mayor que 0. Si quieres realizar una entrega a
cuenta, pulsa el botón de pago, borra la cantidad sugerida e introduce la
cantidad que desees.
ca-ES:
startOrder: Començar encàrrec
noOrdersFound: No s'han trobat comandes
makePayment: Realitzar pagament
shoppingCart: Cistella de la compra
balance: 'Saldo:'
paymentInfo: >-
La quantitat mostrada és el teu saldo pendent (negatiu) o favorable a dia
d'avui, no en compte comandes del futur. Perquè la teva comanda sigui
enviat, aquesta quantitat ha de ser igual o més gran que 0. Si vols fer un
lliurament a compte, prem el botó de pagament, esborra la quantitat suggerida
e introdueix la quantitat que vulguis.
fr-FR:
startOrder: Acheter
noOrdersFound: Aucune commande trouvée
makePayment: Effectuer un paiement
shoppingCart: Panier
balance: 'Balance:'
paymentInfo: >-
Le montant indiqué est votre pente (négative) ou balance favorable
aujourd'hui, ne tient pas compte pour les commandes futures. Obtenir votre
commande est expédiée, ce montant doit être égal ou supérieur à 0. Si vous
voulez faire un versement, le montant suggéré effacé et entrez le montant que
vous souhaitez.
pt-PT:
startOrder: Iniciar encomenda
noOrdersFound: Nenhum pedido encontrado
makePayment: Realizar pagamento
shoppingCart: Cesta da compra
balance: 'Saldo:'
paymentInfo: >-
A quantidade mostrada é seu saldo pendente (negativo) ou favorável a dia de
hoje, não se vincula a pedidos futuros. Para que seu pedido seja enviado, esta
quantidade deve ser igual ou superior a 0. Se queres realizar um depósito à
conta, clique no botão de pagamento, apague a quantidade sugerida e introduza
a quantidade que deseje.
</i18n>

View File

@ -0,0 +1,145 @@
<template>
<Teleport :to="$actions">
<QBtn
icon="print"
:label="$t('printDeliveryNote')"
@click="onPrintClick()"
rounded
no-caps
/>
</Teleport>
<div>
<QCard class="vn-w-sm">
<QCardSection>
<div class="text-h6">#{{ ticket.id }}</div>
</QCardSection>
<QCardSection>
<div class="text-h6">{{ $t('shippingInformation') }}</div>
<div>
{{ $t('preparation') }}
{{ date(ticket.shipped, 'ddd, MMMM Do') }}
</div>
<div>
{{ $t('delivery') }}
{{ date(ticket.shipped, 'ddd, MMMM Do') }}
</div>
<div>
{{ $t(ticket.method != 'PICKUP' ? 'agency' : 'warehouse') }}
{{ ticket.agency }}
</div>
</QCardSection>
<QCardSection>
<div class="text-h6">{{ $t('deliveryAddress') }}</div>
<div>{{ ticket.nickname }}</div>
<div>{{ ticket.street }}</div>
<div>
{{ ticket.postalCode }} {{ ticket.city }} ({{
ticket.province
}})
</div>
</QCardSection>
<QSeparator inset />
<QList v-for="row in rows" :key="row.itemFk">
<QItem>
<QItemSection avatar>
<QAvatar size="68px">
<img
:src="`${$app.imageUrl}/catalog/200x200/${row.image}`"
/>
</QAvatar>
</QItemSection>
<QItemSection>
<QItemLabel lines="1">
{{ row.concept }}
</QItemLabel>
<QItemLabel lines="1" caption>
{{ row.value5 }} {{ row.value6 }} {{ row.value7 }}
</QItemLabel>
<QItemLabel lines="1">
{{ row.quantity }} x {{ currency(row.price) }}
</QItemLabel>
</QItemSection>
<QItemSection side class="total">
<QItemLabel>
<span class="discount" v-if="row.discount">
{{ currency(discountSubtotal(row)) }} -
{{ currency(row.discount) }} =
</span>
{{ currency(subtotal(row)) }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QCard>
</div>
</template>
<style lang="scss" scoped>
.total {
justify-content: flex-end;
}
</style>
<script>
import { date, currency } from 'src/lib/filters.js';
export default {
name: 'OrdersConfirmedView',
data() {
return {
ticket: {},
rows: null,
services: null,
packages: null
};
},
async mounted() {
const params = {
ticket: parseInt(this.$route.params.id)
};
this.ticket = await this.$jApi.getObject(
'CALL myTicket_get(#ticket)',
params
);
this.rows = await this.$jApi.query(
'CALL myTicket_getRows(#ticket)',
params
);
this.services = await this.$jApi.query(
'CALL myTicket_getServices(#ticket)',
params
);
this.packages = await this.$jApi.query(
'CALL myTicket_getPackages(#ticket)',
params
);
},
methods: {
date,
currency,
discountSubtotal(line) {
return line.quantity * line.price;
},
subtotal(line) {
const discount = line.discount;
return this.discountSubtotal(line) * ((100 - discount) / 100);
},
onPrintClick() {
const params = new URLSearchParams({
access_token: this.$user.token,
recipientId: this.$user.id,
type: 'deliveryNote'
});
window.open(
`/api/Tickets/${this.ticket.id}/delivery-note-pdf?${params.toString()}`
);
}
}
};
</script>

View File

@ -0,0 +1,31 @@
<template>
<div
class="fullscreen bg-accent text-white text-center q-pa-md flex flex-center"
>
<div>
<div style="font-size: 30vh">404</div>
<div class="text-h2" style="opacity: 0.4">
Oops. Nothing here...
</div>
<QBtn
class="q-mt-xl"
color="white"
text-color="accent"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'ErrorNotFound'
});
</script>

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

@ -0,0 +1,17 @@
<template>
<QPage class="flex flex-center">
<img
alt="Quasar logo"
src="~assets/quasar-logo-vertical.svg"
style="width: 200px; height: 200px"
/>
</QPage>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'IndexPage'
});
</script>

View File

@ -0,0 +1,205 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { userStore } from 'stores/user';
import { onMounted, ref } from 'vue';
import useNotify from 'src/composables/useNotify.js';
import { useRouter, useRoute } from 'vue-router';
const { notify } = useNotify();
const t = useI18n();
const user = userStore();
const route = useRoute();
const router = useRouter();
const email = ref(null);
const password = ref(null);
const remember = ref(false);
const showPwd = ref(false);
const langs = ['en', 'es', 'ca', 'fr', 'mn', 'pt'];
onMounted(() => {
if (route.query.emailConfirmed !== undefined) {
notify({
message: t('emailConfirmedSuccessfully'),
type: 'positive'
});
}
if (route.params.email) {
email.value = route.params.email;
password.value.focus();
}
});
async function onLogin() {
await user.login(email.value, password.value, remember.value);
router.push('/');
}
</script>
<template>
<div class="main">
<div class="header">
<router-link to="/" class="block">
<img src="statics/logo.svg" alt="Verdnatura" class="block" />
</router-link>
</div>
<QForm @submit="onLogin" class="q-gutter-y-md">
<div class="q-gutter-y-sm">
<QInput v-model="email" :label="$t('user')" autofocus />
<QInput
v-model="password"
:label="$t('password')"
:type="!showPwd ? 'password' : 'text'"
>
<template v-slot:append>
<QIcon
:name="showPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showPwd = !showPwd"
/>
</template>
</QInput>
<div class=" text-center"> <QCheckbox
v-model="remember"
:label="$t('remindMe')"
dense
/> <QBtn
id="switchLanguage"
:label="$t('language')"
icon="translate"
color="primary"
size="sm"
flat
rounded
>
<QMenu auto-close>
<QList dense v-for="lang in langs" :key="lang">
<QItem
disabled
v-ripple
clickable
>
{{ $t(`langs.${lang}`) }}
</QItem>
</QList>
</QMenu>
</QBtn></div>
</div>
<div class="justify-center">
<QBtn
type="submit"
:label="$t('logIn')"
class="full-width"
color="primary"
rounded
no-caps
unelevated
/>
</div>
<div class="justify-center">
<QBtn
to="/"
:label="$t('logInAsGuest')"
class="full-width"
color="primary"
rounded
no-caps
outline
/>
</div>
<p class="password-forgotten text-center q-mt-lg">
<router-link to="/remember-password" class="link">
{{ $t('haveForgottenPassword') }}
</router-link>
</p>
</QForm>
<div class="footer text-center">
<p>
{{ $t('notACustomerYet') }}
<a
href="//verdnatura.es/register/"
target="_blank"
class="link"
>
{{ $t('signUp') }}
</a>
</p>
<p class="contact">
<a :href="`tel:${$t('loginPhone')}`">
{{ $t('loginPhone') }}
</a>
·
<a :href="`mailto:${$t('loginMail')}`">{{ $t('loginMail') }}</a>
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
$login-margin-top: 50px;
$login-margin-between: 55px;
.main {
max-width: 280px;
}
a {
color: inherit;
}
.header {
margin-top: $login-margin-top;
margin-bottom: $login-margin-between;
img {
display: block;
margin: 0 auto;
width: 90%;
}
}
.remember {
margin-top: 20px;
margin-bottom: 40px;
}
.q-btn {
height: 50px;
}
.password-forgotten {
font-size: 0.8rem;
}
.footer {
margin-bottom: $login-margin-top;
margin-top: $login-margin-between;
text-align: center;
font-size: 0.8rem;
.contact {
margin-top: 15px;
color: grey;
}
a {
font-weight: bold;
}
}
</style>
<i18n lang="yaml">
en-US:
user: User
password: Password
remindMe: Remind me
logInAsGuest: Log in as guest
logIn: Log in
loginMail: infoverdnatura.es
loginPhone: +34 607 562 391
haveForgottenPassword: Have you forgotten your password?
notACustomerYet: Not a customer yet?
signUp: Register
es-ES:
user: Usuario
password: Contraseña
remindMe: Recuérdame
logInAsGuest: Entrar como invitado
logIn: Iniciar sesión
loginMail: infoverdnatura.es
loginPhone: +34 963 242 100
haveForgottenPassword: ¿Has olvidado tu contraseña?
notACustomerYet: ¿Todavía no eres cliente?
signUp: Registrarme
</i18n>

View File

@ -0,0 +1,108 @@
<template>
<div class="text-center">
<div>
<QIcon
name="contact_support"
class="block q-mx-auto text-accent"
style="font-size: 120px"
/>
</div>
<div>
<QForm @submit="onSend" class="q-gutter-y-md text-grey-8">
<div class="text-h5">
<div>
{{ $t('dontWorry') }}
</div>
<div>
{{ $t('fillData') }}
</div>
</div>
<QInput
v-model="email"
:label="$t('user')"
:rules="[val => !!val || $t('inputEmail')]"
autofocus
/>
<div class="q-mt-lg">
{{ $t('weSendEmail') }}
</div>
<div>
<QBtn
type="submit"
:label="$t('send')"
class="full-width q-mt-md"
color="primary"
rounded
no-caps
unelevated
/>
<div class="text-center q-mt-md">
<router-link to="/login" class="link">
{{ $t('return') }}
</router-link>
</div>
</div>
</QForm>
</div>
</div>
</template>
<style lang="scss" scoped>
#image {
height: 190px;
}
.q-btn {
height: 50px;
}
a {
color: inherit;
font-size: 0.8rem;
}
</style>
<script>
export default {
name: 'VnRememberPasword',
data() {
return {
email: ''
};
},
methods: {
async onSend() {
const params = {
email: this.email
};
await this.$axios.post('Users/reset', params);
this.$q.notify({
message: this.$t('weHaveSentEmailToRecover'),
type: 'positive'
});
this.$router.push('/login');
}
}
};
</script>
<i18n lang="yaml">
en-US:
user: User
inputEmail: Input email
rememberPassword: Rememeber password
dontWorry: Don't worry!
fillData: Fill the data
weSendEmail: We will sent you an email to recover your password
weHaveSentEmailToRecover: We've sent you an email where you can recover your password
send: Send
return: Return
es-ES:
user: Usuario
inputEmail: Introduce el correo electrónico
rememberPassword: Recordar contraseña
dontWorry: ¡No te preocupes!
fillData: Rellena los datos
weSendEmail: Te enviaremos un correo para restablecer tu contraseña
weHaveSentEmailToRecover: Te hemos enviado un correo donde podrás recuperar tu contraseña
send: Enviar
return: Volver
</i18n>

View File

@ -0,0 +1,106 @@
<template>
<div>
<QCard-section>
<QIcon
name="check"
class="block q-mx-auto text-accent"
style="font-size: 120px"
/>
</QCard-section>
<QCard-section>
<QForm @submit="onRegister" ref="form" class="q-gutter-y-md">
<div class="text-grey-8 text-h5 text-center">
{{ $t('fillData') }}
</div>
<div class="q-gutter-y-sm">
<QInput
v-model="password"
:label="$t('password')"
:type="showPwd ? 'password' : 'text'"
autofocus
hint=""
filled
>
<template v-slot:append>
<QIcon
:name="
showPwd ? 'visibility_off' : 'visibility'
"
class="cursor-pointer"
@click="showPwd = !showPwd"
/>
</template>
</QInput>
<QInput
v-model="repeatPassword"
:label="$t('repeatPassword')"
:type="showRpPwd ? 'password' : 'text'"
:rules="[
value =>
value == password || $t('repeatPasswordError')
]"
hint=""
filled
>
<template v-slot:append>
<QIcon
:name="
showRpPwd ? 'visibility_off' : 'visibility'
"
class="cursor-pointer"
@click="showRpPwd = !showRpPwd"
/>
</template>
</QInput>
</div>
<div>
<QBtn
type="submit"
:label="$t('resetPassword')"
class="full-width"
color="primary"
/>
<div class="text-center q-mt-xs">
<router-link to="/login" class="link">
{{ $t('return') }}
</router-link>
</div>
</div>
</QForm>
</QCard-section>
</div>
</template>
<script>
export default {
name: 'VnRegister',
data() {
return {
password: '',
repeatPassword: '',
showPwd: true,
showRpPwd: true
};
},
methods: {
async onRegister() {
const headers = {
Authorization: this.$route.query.access_token
};
await this.$axios.post(
'users/reset-password',
{
newPassword: this.password
},
{ headers }
);
this.$q.notify({
message: this.$t('passwordResetSuccessfully'),
type: 'positive'
});
this.$router.push('/login');
}
}
};
</script>

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

@ -0,0 +1,51 @@
import { route } from 'quasar/wrappers'
import { appStore } from 'stores/app'
import {
createRouter,
createMemoryHistory,
createWebHistory,
createWebHashHistory
} from 'vue-router'
import routes from './routes'
/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Router instance.
*/
export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(
process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE
)
})
Router.afterEach((to, from) => {
if (from.name === to.name) return
const app = appStore()
app.$patch({
title: window.i18n.t(to.name || 'home'),
subtitle: null,
useRightDrawer: false,
rightDrawerOpen: true
})
})
return Router
})

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

@ -0,0 +1,88 @@
const routes = [
{
path: '/login',
component: () => import('layouts/LoginLayout.vue'),
children: [
{
name: 'Login',
path: '/login/:email?',
component: () => import('pages/Login/LoginView.vue')
},
{
name: 'rememberPassword',
path: '/remember-password',
component: () => import('pages/Login/RememberPassword.vue')
},
{
name: 'resetPassword',
path: '/reset-password',
component: () => import('pages/Login/ResetPassword.vue')
}
]
},
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{
name: '',
path: '',
component: () => import('src/pages/Cms/HomeView.vue')
},
{
name: 'home',
path: '/cms/home',
component: () => import('src/pages/Cms/HomeView.vue')
},
{
name: 'orders',
path: '/ecomerce/orders',
component: () => import('pages/Ecomerce/Orders.vue')
},
{
name: 'ticket',
path: '/ecomerce/ticket/:id',
component: () => import('pages/Ecomerce/Ticket.vue')
},
{
name: 'invoices',
path: '/ecomerce/invoices',
component: () => import('pages/Ecomerce/Invoices.vue')
},
{
name: 'catalog',
path: '/ecomerce/catalog/:category?/:type?',
component: () => import('pages/Ecomerce/Catalog.vue')
},
{
name: 'packages',
path: '/agencies/packages',
component: () => import('src/pages/Agencies/PackagesView.vue')
},
{
name: 'Account',
path: '/account/conf',
component: () => import('pages/Account/AccountConfig.vue')
},
{
name: 'AddressesList',
path: '/account/address-list',
component: () => import('pages/Account/AddressList.vue')
},
{
name: 'AddressDetails',
path: '/account/address/:id?',
component: () => import('pages/Account/AddressDetails.vue')
}
]
},
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue')
}
];
export default routes;

19
src/stores/app.js Normal file
View File

@ -0,0 +1,19 @@
import { defineStore } from 'pinia'
import { jApi } from 'boot/axios'
export const appStore = defineStore('hedera', {
state: () => ({
title: null,
subtitle: null,
imageUrl: '',
useRightDrawer: false,
rightDrawerOpen: false
}),
actions: {
async loadConfig () {
const imageUrl = await jApi.getValue('SELECT url FROM imageConfig')
this.$patch({ imageUrl })
}
}
})

20
src/stores/index.js Normal file
View File

@ -0,0 +1,20 @@
import { store } from 'quasar/wrappers'
import { createPinia } from 'pinia'
/*
* If not building with SSR mode, you can
* directly export the Store instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Store instance.
*/
export default store((/* { ssrContext } */) => {
const pinia = createPinia()
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
return pinia
})

10
src/stores/store-flag.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import 'quasar/dist/types/feature-flag'
declare module 'quasar/dist/types/feature-flag' {
interface QuasarFeatureFlags {
store: true
}
}

93
src/stores/tpv.js Normal file
View File

@ -0,0 +1,93 @@
import { defineStore } from 'pinia'
import { jApi } from 'boot/axios'
export const tpvStore = defineStore('tpv', {
actions: {
async check (route) {
const order = route.query.tpvOrder
const status = route.query.tpvStatus
if (!(order && status)) return null
await jApi.execQuery('CALL myTpvTransaction_end(#order, #status)', {
order,
status
})
if (status === 'ko') {
const retry = confirm('retryPayQuestion')
if (retry) {
this.retryPay(order)
}
}
return status
},
async pay (amount, company) {
await this.realPay(amount * 100, company)
},
async retryPay (order) {
const payment = await jApi.getObject(
`SELECT t.amount, m.companyFk
FROM myTpvTransaction t
JOIN tpvMerchant m ON m.id = t.merchantFk
WHERE t.id = #order`,
{ order }
)
await this.realPay(payment.amount, payment.companyFk)
},
async realPay (amount, company) {
if (!isNumeric(amount) || amount <= 0) {
throw new Error('payAmountError')
}
const json = await jApi.send('tpv/transaction', {
amount: parseInt(amount),
urlOk: this.makeUrl('ok'),
urlKo: this.makeUrl('ko'),
company
})
const postValues = json.postValues
const form = document.createElement('form')
form.method = 'POST'
form.action = json.url
document.body.appendChild(form)
for (const field in postValues) {
const input = document.createElement('input')
input.type = 'hidden'
input.name = field
form.appendChild(input)
if (postValues[field]) {
input.value = postValues[field]
}
}
form.submit()
},
makeUrl (status) {
let path = location.protocol + '//' + location.hostname
path += location.port ? ':' + location.port : ''
path += location.pathname
path += location.search ? location.search : ''
path += '#/ecomerce/orders'
path +=
'?' +
new URLSearchParams({
tpvStatus: status,
tpvOrder: '_transactionId_'
}).toString()
return path
}
}
})
function isNumeric (n) {
return !isNaN(parseFloat(n)) && isFinite(n)
}

63
src/stores/user.js Normal file
View File

@ -0,0 +1,63 @@
import { defineStore } from 'pinia';
import { api, jApi } from 'boot/axios';
export const userStore = defineStore('user', {
state: () => {
const token =
sessionStorage.getItem('vnToken') ||
localStorage.getItem('vnToken');
return {
token,
id: null,
name: null,
nickname: null,
isGuest: false
};
},
getters: {
loggedIn: state => state.token != null
},
actions: {
async login(user, password, remember) {
const params = { user, password };
const res = await api.post('Accounts/login', params);
if (remember) {
localStorage.setItem('vnToken', res.data.token);
} else {
sessionStorage.setItem('vnToken', res.data.token);
}
this.$patch({
token: res.data.token,
name: user
});
},
async logout() {
if (this.token != null) {
try {
await api.post('Accounts/logout');
} catch (e) {}
localStorage.removeItem('vnToken');
sessionStorage.removeItem('vnToken');
}
this.$reset();
},
async loadData() {
const userData = await jApi.getObject(
'SELECT id, nickname, name FROM account.myUser'
);
this.$patch({
id: userData.id,
nickname: userData.nickname,
name: userData.name
});
}
}
});

View File

@ -28,27 +28,34 @@ const baseConfig = {
presets: ['@babel/preset-env']
}
}
}, {
},
{
test: /tinymce\/.*\/skin\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}, {
},
{
test: /tinymce\/.*\/content\.css$/i,
loader: 'css-loader',
options: { esModule: false }
}, {
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
exclude: [/node_modules/]
}, {
},
{
test: /\.yml$/,
use: ['json-loader', 'yaml-loader']
}, {
},
{
test: /\.xml$/,
use: 'raw-loader'
}, {
},
{
test: /\.ttf$/,
type: 'asset/resource'
}, {
},
{
test: /\.scss$/,
use: [
'style-loader',
@ -60,6 +67,18 @@ const baseConfig = {
}
}
]
},
{
test: /\.(woff|woff2)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/'
}
}
]
}
]
},
@ -89,7 +108,7 @@ const baseConfig = {
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: 'all',
chunks: 'all'
}
},
watchOptions: {
@ -127,7 +146,7 @@ const devConfig = {
'/api': 'http://localhost:3000',
'/': {
target: 'http://localhost/projects/hedera-web',
bypass: (req) => req.path !== '/' ? req.path : null
bypass: req => (req.path !== '/' ? req.path : null)
}
}
}