Merge pull request 'Creamos api en node para la conexión a base de datos y cambios que pidió pablo' (#4) from develop into master

Reviewed-on: #4
Reviewed-by: Pablo Natek <pablone@verdnatura.es>
This commit is contained in:
Pablo Natek 2024-01-18 06:13:47 +00:00
commit b769c14e0f
85 changed files with 6028 additions and 2112 deletions

4
.env Normal file
View File

@ -0,0 +1,4 @@
STRIPE_PUBLISHABLE_KEY=''
STRIPE_ACCOUNT=''
API_VERSION=''
LOCALE=''

2
.gitignore vendored
View File

@ -5,6 +5,7 @@ node_modules
# Quasar core related directories
.quasar
/dist
dist.rar
/quasar.config.*.temporary.compiled*
# Cordova related directories and files
@ -35,3 +36,4 @@ yarn-error.log*
# local .env files
.env.local*
api/.env

View File

@ -74,5 +74,8 @@
"workbench.tree.indent": 16,
"window.zoomLevel": 0,
"git.ignoreRebaseWarning": true,
"editor.largeFileOptimizations": false
"editor.largeFileOptimizations": false,
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}

5
api/.env.example Normal file
View File

@ -0,0 +1,5 @@
HOST="127.0.0.1"
DB_USER="root"
DB_PASSWORD="root"
PORT ="3306"
DATABASE="floranet"

View File

@ -0,0 +1,129 @@
const db = require("../db/db");
const productsJson = require("./products.json")
class ProductController {
async findAll(req, res) {
const _products = await db.getProducts();
const mapedProducts = await _products[0].map(function (item) {
return {
id: item.id,
name: item.name,
description: item.description,
type: item.type,
price: item.size,
specialPrice: item.specialPrice,
isNew: item.isNew,
slug: item.slug,
category: item.category,
postalCode: item.postalCode,
dateExpired: item.dateExpired,
images: [item.image],
featured: item.featured,
}
})
//Gerar produtos fake
/* const fakeProducts = generateFlowers(50)
console.log(fakeProducts); */
const params = req.query;
let productsFilter = mapedProducts
console.log(params);
if (Number(params.featured)) {
productsFilter = productsFilter.filter(item => item.featured === Number(params.featured))
}
if (params.category) {
productsFilter = productsFilter.filter(item => item.category === Number(params.category))
}
if (params.postalCode) {
productsFilter = productsFilter.filter(item => item.postalCode === params.postalCode)
}
if (params.minPrice && !params.maxPrice) {
productsFilter = productsFilter.filter(item => {
const price = Number(item.price.replace(/€/g, ''))
if (price >= Number(params.minPrice)) {
return item
}
})
}
if (params.maxPrice && !params.minPrice) {
productsFilter = productsFilter.filter(item => {
const price = Number(item.price.replace(/€/g, ''))
if (price <= Number(params.maxPrice)) {
return item
}
})
}
if (params.maxPrice && params.minPrice) {
productsFilter = productsFilter.filter(item => {
const price = Number(item.price.replace(/€/g, ''))
if (price >= Number(params.minPrice) && price <= Number(params.maxPrice)) {
console.log(price);
return item
}
})
}
if (params.dateExpired) {
const [day, month, year] = params.dateExpired.split("/");
const dateSearch = new Date(year, month - 1, day);
productsFilter = productsFilter.filter(item => {
const [day, month, year] = item.dateExpired.split("/");
const dateProduct = new Date(year, month - 1, day);
if (dateProduct >= dateSearch) {
return item
}
})
}
if (Number(params.bigPrice)) {
productsFilter.sort((a, b) => {
const itemA = Number(a.price.replace(/€/g, ''))
const itemB = Number(b.price.replace(/€/g, ''))
return itemB - itemA;
})
}
if (Number(params.lowPrice)) {
productsFilter.sort((a, b) => {
const itemA = Number(a.price.replace(/€/g, ''))
const itemB = Number(b.price.replace(/€/g, ''))
return itemA - itemB;
})
}
if (params.isNew) {
productsFilter = productsFilter.filter(item => item.isNew === true)
}
let productsFilterPages = []
const totalItens = params?.itens ? Number(params.itens) : 200
const page = params.page ? Number(params.page) : 1
const startIndex = (totalItens * page) - totalItens
const lastIndex = (totalItens * page)
const products = productsFilter.slice(startIndex, lastIndex)
productsFilterPages.push({
page: page,
productsPerPage: products.length,
products: products
})
return res.status(200).send({
data: productsFilterPages
})
}
findBySlug(req, res) {
const slug = req.params.slug
const products = productsJson
const filterSlug = products.filter(item => item.slug === slug)
return res.status(200).send({
data: filterSlug
})
}
}
module.exports = new ProductController();

1974
api/controller/products.json Normal file

File diff suppressed because it is too large Load Diff

28
api/db/db.js Normal file
View File

@ -0,0 +1,28 @@
async function connect() {
if (global.connection && global.connection.state !== 'disconnected')
return global.connection;
const host = process.env.HOST;
const port = process.env.PORT;
const database = process.env.DATABASE;
const user = process.env.DB_USER;
const password = process.env.DB_PASSWORD;
const mysql = require("mysql2/promise");
const connection = await mysql.createConnection("mysql://" + user + ":" + password + "@" + host + ":" + port + "/" + database + "");
console.log("Connected to MySQL!");
global.connection = connection;
return connection;
}
async function getProducts() {
const conn = await connect();
const [rows] = await conn.query('CALL catalogue_get("2024-01-30", "08001")');
return rows;
}
module.exports = { getProducts }

14
api/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>

27
api/index.js Normal file
View File

@ -0,0 +1,27 @@
const cors = require('cors');
const express = require('express');
const path = require('path');
const productController = require('./controller/product.controller');
const app = express();
const port = 9999;
const allowedOrigins = ['http://localhost:9100', 'https://floranet.onecommerce.dev/'];
const corsOptions = {
origin: allowedOrigins,
optionsSuccessStatus: 200,
};
app.use(cors(corsOptions));
app.get('/', (req, res) => {
const indexPath = path.join(__dirname, './', 'index.html');
res.sendFile(indexPath);
});
//Products
app.get('/api/products', productController.findAll);
app.get('/api/products/slug/:slug', productController.findBySlug);
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});

657
api/package-lock.json generated Normal file
View File

@ -0,0 +1,657 @@
{
"name": "backend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "backend",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"express": "^4.18.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"dependencies": {
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"dependencies": {
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"dependencies": {
"get-intrinsic": "^1.2.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"dependencies": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"engines": {
"node": ">= 0.8"
}
}
}
}

15
api/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2"
}
}

View File

@ -16,27 +16,27 @@
rel="icon"
type="image/png"
sizes="128x128"
href="icons/favicon-128x128.png"
href="icons/floranet-favicon.jpg"
/>
<link
rel="icon"
type="image/png"
sizes="96x96"
href="icons/favicon-96x96.png"
href="icons/floranet-favicon.jpg"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="icons/favicon-32x32.png"
href="icons/floranet-favicon.jpg"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="icons/favicon-16x16.png"
href="icons/floranet-favicon.jpg"
/>
<link rel="icon" type="image/ico" href="favicon.ico" />
<link rel="icon" type="image/ico" href="icons/floranet-favicon.jpg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
@ -51,6 +51,7 @@
src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-element-bundle.min.js"
defer
></script>
<script src="https://js.stripe.com/v3" defer></script>
</head>
<body>
<!-- quasar:entry-point -->

812
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,35 +9,42 @@
"lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"dev": "quasar dev -m ssr",
"api": "cd api && node --env-file .env index.js ",
"build": "quasar build -m ssr",
"build:api": "cd api && tsc",
"start:build": "npm run build && cd dist/ssr && npm i && npm run start",
"backend": "json-server -p 5000 -d 600 -w src/services/json-server/db.json"
"backend": "json-server -p 3000 -d 600 -w src/services/json-server/db.json --routes src/services/json-server/routes.json"
},
"dependencies": {
"@vee-validate/zod": "^4.12.2",
"@vueuse/core": "^10.7.0",
"vee-validate": "^4.12.2",
"vue-image-zoomer": "^2.2.3",
"zod": "^3.22.4",
"axios": "^1.2.1",
"vue-i18n": "^9.0.0",
"pinia": "^2.0.11",
"@quasar/extras": "^1.16.4",
"@vee-validate/zod": "^4.12.2",
"@vue-stripe/vue-stripe": "^4.5.0",
"@vueuse/core": "^10.7.0",
"axios": "^1.2.1",
"express": "^4.18.2",
"mysql2": "^3.7.0",
"pinia": "^2.0.11",
"quasar": "^2.6.0",
"vee-validate": "^4.12.2",
"vue": "^3.0.0",
"vue-router": "^4.0.0"
"vue-i18n": "^9.0.0",
"vue-image-zoomer": "^2.2.3",
"vue-router": "^4.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@faker-js/faker": "^8.3.1",
"json-server": "^0.17.4",
"eslint": "^8.10.0",
"eslint-plugin-vue": "^9.0.0",
"eslint-config-prettier": "^8.1.0",
"prettier": "^2.5.1",
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
"@quasar/app-vite": "^1.3.0",
"autoprefixer": "^10.4.2",
"postcss": "^8.4.14"
"eslint": "^8.10.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^9.0.0",
"json-server": "^0.17.4",
"postcss": "^8.4.14",
"prettier": "^2.5.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"engines": {
"node": "^18 || ^16 || ^14.19",

BIN
public/assets/empty-img.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -28,7 +28,7 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: ["i18n", "axios"],
boot: ["i18n", "axios" /* , "stripe" */],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ["app.scss"],
@ -119,7 +119,7 @@ module.exports = configure(function (/* ctx */) {
// directives: [],
// Quasar plugins
plugins: ["Meta", "Loading"],
plugins: ["Meta", "Loading", "Notify"],
},
// animations: 'all', // --- includes all animations

View File

@ -1,11 +1,11 @@
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "App",
});
</script>
<template>
<router-view />
</template>
<script lang="js">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App'
});
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -7,7 +7,8 @@ import { boot } from "quasar/wrappers";
// 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: "http://localhost:5000/" });
const api = axios.create({ baseURL: "http://localhost:3000/jsonServer/" });
const apiBack = axios.create({ baseURL: "http://localhost:9999/api/" });
export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
@ -17,8 +18,9 @@ export default boot(({ app }) => {
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api;
app.config.globalProperties.$apiBack = apiBack;
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
});
export { api };
export { api, apiBack };

View File

@ -1,6 +1,6 @@
import { boot } from "quasar/wrappers";
import { createI18n } from "vue-i18n";
import messages from "src/i18n";
import { createI18n } from "vue-i18n";
export default boot(({ app }) => {
const i18n = createI18n({

17
src/boot/stripe.js Normal file
View File

@ -0,0 +1,17 @@
import { StripePlugin } from "@vue-stripe/vue-stripe";
import { boot } from "quasar/wrappers";
// "async" is optional;
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
export default boot(async ({ app, router, store }) => {
if (typeof window === "undefined") return;
const options = {
pk: process.env.STRIPE_PUBLISHABLE_KEY,
stripeAccount: process.env.STRIPE_ACCOUNT,
apiVersion: process.env.API_VERSION,
locale: process.env.LOCALE,
};
app.use(StripePlugin, options);
});

View File

@ -1,3 +1,68 @@
<script>
import { toTypedSchema } from "@vee-validate/zod";
import { storeToRefs } from "pinia";
import { quasarNotify } from "src/functions/quasarNotify";
import { useFormStore } from "src/stores/forms";
import { availabilitySchema } from "src/utils/zod/schemas/availabilitySchema";
import { useForm } from "vee-validate";
import { defineComponent, ref, watch } from "vue";
import IconCalendar from "../icons/IconCalendar.vue";
export default defineComponent({
name: "calendar-input",
components: { IconCalendar },
setup() {
const getDate = new Date();
const currentDay = getDate.getDate().toString().padStart(2, "0");
const currentMonth = getDate.getMonth() + 1;
const currentYear = getDate.getFullYear();
const fullCurrentDate = `${currentYear}/${currentMonth}/${currentDay}`;
const formStore = useFormStore();
const { availability } = storeToRefs(formStore);
const proxyDate = ref(fullCurrentDate);
const validationSchema = toTypedSchema(
availabilitySchema.pick({ date: true })
);
const { errors, defineField, values } = useForm({
validationSchema,
});
const [calendar, calendarAttrs] = defineField("date");
const onBlur = () => {
availability.value.date = calendar.value;
};
availability.value.date = values.date;
watch(errors, (newErrors) => {
if (newErrors.date) {
quasarNotify({ message: newErrors.date, type: "erro" });
}
});
return {
availability,
proxyDate,
calendar,
calendarAttrs,
errors,
onBlur,
updateProxy() {
proxyDate.value = availability.value.date;
},
optionsValidDates(date) {
return date >= fullCurrentDate /* && date <= '2019/02/15' */;
},
save() {
availability.value.date = proxyDate.value;
calendar.value = proxyDate.value;
},
};
},
});
</script>
<template>
<div class="custom-input-el calendar">
<q-btn round size="sm" class="custom-date-btn">
@ -30,74 +95,17 @@
<div class="custom-block-content">
<p class="custom-head-paragraph">¿Cuándo?</p>
<q-input
<!-- <q-input
class="custom-date-input"
label="Elige una fecha"
placeholder="DD/MM/YYYY"
placeholder="Elige una fecha"
v-model="calendar"
mask="##/##/####"
:error="!!errors.date"
:error-message="errors.date"
@blur="onBlur"
borderless
dense
/>
/> -->
<slot></slot>
</div>
</div>
</template>
<script lang="js">
import { toTypedSchema } from '@vee-validate/zod';
import { storeToRefs } from 'pinia';
import { useFormStore } from 'src/stores/forms';
import { availabilitySchema } from 'src/utils/zod/schemas/availabilitySchema';
import { useForm } from 'vee-validate';
import { defineComponent, ref } from 'vue';
import IconCalendar from '../icons/IconCalendar.vue';
export default defineComponent({
name: 'calendar-input',
components: { IconCalendar },
setup() {
const getDate = new Date();
const currentDay = getDate.getDate().toString().padStart(2, '0');
const currentMonth = getDate.getMonth() + 1;
const currentYear = getDate.getFullYear();
const fullCurrentDate = `${currentYear}/${currentMonth}/${currentDay}`;
const formStore = useFormStore();
const { availability } = storeToRefs(formStore);
const proxyDate = ref(fullCurrentDate);
const validationSchema = toTypedSchema(
availabilitySchema.pick({ date: true })
);
const { errors, defineField } = useForm({
validationSchema,
});
const [calendar, calendarAttrs] = defineField('date');
const onBlur = () => {
availability.value.date = calendar.value;
};
return {
availability,
proxyDate,
calendar,
calendarAttrs,
errors,
onBlur,
updateProxy() {
proxyDate.value = availability.value.date;
},
optionsValidDates(date) {
return date >= fullCurrentDate /* && date <= '2019/02/15' */;
},
save() {
availability.value.date = proxyDate.value;
calendar.value = proxyDate.value;
},
};
},
});
</script>

View File

@ -0,0 +1,62 @@
<script>
import { toTypedSchema } from "@vee-validate/zod";
import { Field, useForm } from "vee-validate";
import { defineComponent, watch } from "vue";
import { quasarNotify } from "src/functions/quasarNotify";
import { useFormStore } from "src/stores/forms";
import { availabilitySchema } from "src/utils/zod/schemas/availabilitySchema";
import IconPostalCode from "../icons/IconPostalCode.vue";
export default defineComponent({
name: "PostalCodeEx",
components: { IconPostalCode, Field },
setup() {
const formStore = useFormStore();
const validationSchema = toTypedSchema(
availabilitySchema.pick({ date: true })
);
const { errors, values, handleSubmit } = useForm({
validationSchema,
initialValues: {
date: "",
},
});
watch(values, (newValues) => {
formStore.$patch({
availability: {
postalCode: newValues.date,
},
});
});
watch(errors, (newErrors) => {
if (newErrors.date) {
quasarNotify({ message: newErrors.date, type: "erro" });
}
});
const onSubmit = handleSubmit(formStore.registerAvailability);
return {
postalCode,
postalCodeAttrs,
errors,
onBlur,
};
},
});
</script>
<template>
<div class="custom-input-el postal-code">
<IconPostalCode />
<div class="custom-block-content">
<p class="custom-head-paragraph">¿Dónde?</p>
<!-- <p class="custom-main-paragraph">código postal</p> -->
<Field />
</div>
</div>
</template>

View File

@ -0,0 +1,62 @@
<script>
import { toTypedSchema } from "@vee-validate/zod";
import { Field, useForm } from "vee-validate";
import { defineComponent, watch } from "vue";
import { quasarNotify } from "src/functions/quasarNotify";
import { useFormStore } from "src/stores/forms";
import { availabilitySchema } from "src/utils/zod/schemas/availabilitySchema";
import IconPostalCode from "../icons/IconPostalCode.vue";
export default defineComponent({
name: "PostalCodeEx",
components: { IconPostalCode, Field },
setup() {
const formStore = useFormStore();
const validationSchema = toTypedSchema(
availabilitySchema.pick({ postalCode: true })
);
const { errors, values, handleSubmit } = useForm({
validationSchema,
initialValues: {
postalCode: "",
},
});
watch(values, (newValues) => {
formStore.$patch({
availability: {
postalCode: newValues.postalCode,
},
});
});
watch(errors, (newErrors) => {
if (newErrors.postalCode) {
quasarNotify({ message: newErrors.postalCode, type: "erro" });
}
});
const onSubmit = handleSubmit(formStore.registerAvailability);
return {
postalCode,
postalCodeAttrs,
errors,
onBlur,
};
},
});
</script>
<template>
<div class="custom-input-el postal-code">
<IconPostalCode />
<div class="custom-block-content">
<p class="custom-head-paragraph">¿Dónde?</p>
<!-- <p class="custom-main-paragraph">código postal</p> -->
<Field />
</div>
</div>
</template>

View File

@ -1,38 +1,16 @@
<template>
<div class="custom-input-el postal-code">
<IconPostalCode />
<script>
import { toTypedSchema } from "@vee-validate/zod";
import { storeToRefs } from "pinia";
import { useForm } from "vee-validate";
import { defineComponent, watch } from "vue";
<div class="custom-block-content">
<p class="custom-head-paragraph">¿Dónde?</p>
<!-- <p class="custom-main-paragraph">código postal</p> -->
<q-input
borderless
class="custom-main-paragraph"
v-model="postalCode"
v-bind="postalCodeAttrs"
:error="!!errors.postalCode"
:error-message="errors.postalCode"
label="código postal"
placeholder="00000-000"
mask="#####-###"
@blur="onBlur"
dense
/>
</div>
</div>
</template>
<script lang="js">
import { toTypedSchema } from '@vee-validate/zod';
import { storeToRefs } from 'pinia';
import { useFormStore } from 'src/stores/forms';
import { availabilitySchema } from 'src/utils/zod/schemas/availabilitySchema';
import { useForm } from 'vee-validate';
import { defineComponent } from 'vue';
import IconPostalCode from '../icons/IconPostalCode.vue';
import { quasarNotify } from "src/functions/quasarNotify";
import { useFormStore } from "src/stores/forms";
import { availabilitySchema } from "src/utils/zod/schemas/availabilitySchema";
import IconPostalCode from "../icons/IconPostalCode.vue";
export default defineComponent({
name: 'postal-code',
name: "postal-code",
components: { IconPostalCode },
setup() {
const formStore = useFormStore();
@ -40,16 +18,23 @@ export default defineComponent({
const validationSchema = toTypedSchema(
availabilitySchema.pick({ postalCode: true }).partial()
);
const { errors, defineField } = useForm({
const { errors, defineField, values } = useForm({
validationSchema,
initialValues: {
postalCode: availability.value.date,
postalCode: availability.value.postalCode,
},
});
const [postalCode, postalCodeAttrs] = defineField('postalCode');
const [postalCode, postalCodeAttrs] = defineField("postalCode");
const onBlur = () => {
availability.value.postalCode = postalCode.value;
};
availability.value.postalCode = values.postalCode;
watch(errors, (newErrors) => {
if (newErrors.postalCode) {
quasarNotify({ message: newErrors.postalCode, type: "erro" });
}
});
return {
postalCode,
@ -60,3 +45,26 @@ export default defineComponent({
},
});
</script>
<template>
<div class="custom-input-el postal-code">
<IconPostalCode />
<div class="custom-block-content">
<p class="custom-head-paragraph">¿Dónde?</p>
<slot></slot>
<!-- <q-input
borderless
class="custom-main-paragraph"
v-model="postalCode"
v-bind="postalCodeAttrs"
:error="!!errors.postalCode"
placeholder="código postal"
mask="#####"
@blur="onBlur"
dense
/> -->
</div>
</div>
</template>

View File

@ -1,36 +1,22 @@
<template>
<div class="range-container">
<p class="filter-item-paragraph">Precio</p>
<q-range
v-model="rangePriceStore.rangeValue"
@change="rangePriceStore.handlePriceRange"
:min="0"
:max="200"
color="primary"
/>
<div class="range-price-content">
<p class="filter-item-paragraph min-price">
Desde:
<span class="green-text">{{ rangePriceStore.rangeValue.min }}</span>
</p>
<p class="filter-item-paragraph max-price">
Hasta:
<span class="green-text">{{ rangePriceStore.rangeValue.max }}</span>
</p>
</div>
</div>
</template>
<script lang="js">
import { useRangePriceStore } from 'src/stores/rangePrice';
import { defineComponent } from 'vue';
<script>
import { useRangePriceStore } from "src/stores/rangePrice";
import { defineComponent } from "vue";
export default defineComponent({
name: 'range-component',
name: "range-component",
components: {},
props: {
min: {
type: Number,
default: 0,
required: true,
},
max: {
type: Number,
default: 200,
required: true,
},
},
setup() {
const rangePriceStore = useRangePriceStore();
@ -39,6 +25,26 @@ export default defineComponent({
});
</script>
<template>
<div class="range-container">
<p class="filter-item-paragraph">Precio</p>
<slot></slot>
<div class="range-price-content">
<p class="filter-item-paragraph min-price">
Desde:
<span class="green-text">{{ min }}</span>
</p>
<p class="filter-item-paragraph max-price">
Hasta:
<span class="green-text">{{ max }}</span>
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.range-container {
display: flex;

View File

@ -1,3 +1,25 @@
<script>
import { storeToRefs } from "pinia";
import { useFormStore } from "src/stores/forms";
import { defineComponent } from "vue";
export default defineComponent({
name: "SortSelect",
components: {},
setup() {
const formStore = useFormStore();
const { sortProductFilters } = storeToRefs(formStore);
async function handleOrder(order) {
sortProductFilters.value.order = order;
sortProductFilters.value.isOpenOrderFilter = false;
}
return { sortProductFilters, handleOrder };
},
});
</script>
<template>
<div class="order-values" role="select">
<div
@ -34,28 +56,6 @@
</div>
</template>
<script lang="js">
import { storeToRefs } from 'pinia';
import { useFormStore } from 'src/stores/forms';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'SortSelect',
components: {},
setup() {
const formStore = useFormStore();
const { sortProductFilters } = storeToRefs(formStore);
function handleOrder(order) {
sortProductFilters.value.order = order;
sortProductFilters.value.isOpenOrderFilter = false;
}
return { sortProductFilters, handleOrder };
},
});
</script>
<style lang="scss" scoped>
.order-values {
display: flex;

View File

@ -1,3 +1,23 @@
<script>
import { defineComponent } from "vue";
import { ref } from "vue";
import IconArrowCircleRight from "../icons/IconArrowCircleRight.vue";
import LogoWhite from "../logo/LogoWhite.vue";
import Container from "../ui/Container.vue";
export default defineComponent({
name: "footer-component",
components: { Container, LogoWhite, IconArrowCircleRight },
setup() {
const currentYear = new Date().getFullYear();
const year = ref(currentYear);
return { year };
},
});
</script>
<template>
<q-footer class="custom-q-footer" elevated>
<container class="footer-container">
@ -26,10 +46,10 @@
<ul class="footer-list footer-secondary">
<li class="footer-list-item list-links">
<RouterLink to="/">Ramos</RouterLink><br />
<RouterLink to="/">Plantas</RouterLink><br />
<RouterLink to="/">Nosotros</RouterLink><br />
<RouterLink to="/faq">FAQs</RouterLink><br />
<RouterLink to="/categoria/ramos">Ramos</RouterLink><br />
<RouterLink to="/categoria/plantas">Plantas</RouterLink><br />
<!-- <RouterLink to="/">Nosotros</RouterLink><br />
<RouterLink to="/faq">FAQs</RouterLink><br /> -->
<RouterLink to="/contacta">Contacta</RouterLink>
</li>
@ -39,11 +59,11 @@
envío, calcular un nuevo envío o solicitar recogida, estamos para
ayudarte. <br /><br />
<RouterLink to="/">
<!-- <RouterLink to="/">
<IconArrowCircleRight /> Preguntas frecuentes
</RouterLink>
</RouterLink> -->
<br />
<RouterLink to="/">
<RouterLink to="/example">
<IconArrowCircleRight /> Contacta con nosotros
</RouterLink>
</p>
@ -51,11 +71,13 @@
<li class="footer-list-item">
<p class="footer-list-content">
Floranet &copy;2023 <br /><br />
<RouterLink to="/">Aviso Legal</RouterLink> <br />
<RouterLink to="/">Condiciones de uso</RouterLink><br />
<RouterLink to="/">Política de cookies</RouterLink><br />
<RouterLink to="/"> Política de Redes Sociales </RouterLink>
Floranet &copy;{{ year }} <br /><br />
<RouterLink to="/example">Aviso Legal</RouterLink> <br />
<RouterLink to="/example">Condiciones de uso</RouterLink><br />
<RouterLink to="/example">Política de cookies</RouterLink><br />
<RouterLink to="/example">
Política de Redes Sociales
</RouterLink>
<br /><br />
Desarrollado por diligent
@ -67,19 +89,6 @@
</q-footer>
</template>
<script lang="js">
import { defineComponent } from 'vue';
import IconArrowCircleRight from '../icons/IconArrowCircleRight.vue';
import LogoWhite from '../logo/LogoWhite.vue';
import Container from '../ui/Container.vue';
export default defineComponent({
name: 'footer-component',
components: { Container, LogoWhite, IconArrowCircleRight },
});
</script>
<style lang="scss" scoped>
.custom-q-footer {
position: static;
@ -138,6 +147,16 @@ a:hover {
}
}
@media only screen and (max-width: $med-lg) {
&.footer-primary {
flex: 0 0 min(100%, 275px);
}
&.footer-secondary {
gap: 50px;
flex: 0 0 min(100%, 545px);
}
}
@media only screen and (max-width: $med-md) {
justify-content: space-evenly;
&.footer-primary {
@ -176,6 +195,9 @@ a:hover {
}
}
}
/* @media only screen and (max-width: $med-lg) and (max-width: 1200px) {
gap: initial;
} */
}
}
</style>

View File

@ -1,3 +1,28 @@
<script>
import { defineComponent } from "vue";
import { storeToRefs } from "pinia";
import { useMobileStore } from "src/stores/mobileNav";
import LogoGreenLight from "../logo/LogoGreenLight.vue";
import LogoWhite from "../logo/LogoWhite.vue";
import SendBanner from "../ui/SendBanner.vue";
import NavLinks from "./NavLinks.vue";
import UserArea from "./UserArea.vue";
export default defineComponent({
name: "header-primary",
components: { SendBanner, UserArea, NavLinks, LogoWhite, LogoGreenLight },
setup() {
const mobileStore = useMobileStore();
const { isOpenNav } = storeToRefs(mobileStore);
const { handleResize } = mobileStore;
window.addEventListener("resize", handleResize);
return { isOpenNav };
},
});
</script>
<template>
<q-header
class="header-container transparent"
@ -23,31 +48,6 @@
</q-header>
</template>
<script lang="js">
import { defineComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { useMobileStore } from 'src/stores/mobileNav';
import LogoGreenLight from '../logo/LogoGreenLight.vue';
import LogoWhite from '../logo/LogoWhite.vue';
import SendBanner from '../ui/SendBanner.vue';
import NavLinks from './NavLinks.vue';
import UserArea from './UserArea.vue';
export default defineComponent({
name: 'header-primary',
components: { SendBanner, UserArea, NavLinks, LogoWhite, LogoGreenLight },
setup() {
const mobileStore = useMobileStore();
const { isOpenNav } = storeToRefs(mobileStore);
const { handleResize } = mobileStore;
window.addEventListener('resize', handleResize);
return { isOpenNav };
},
});
</script>
<style lang="scss" scoped>
.header-container {
display: flex;

View File

@ -1,3 +1,26 @@
<script>
import { defineComponent } from "vue";
import { useMobileStore } from "src/stores/mobileNav";
import LogoWhite from "../logo/LogoWhite.vue";
import SendBanner from "../ui/SendBanner.vue";
import NavLinks from "./NavLinks.vue";
import UserArea from "./UserArea.vue";
export default defineComponent({
name: "header-secondary",
components: { SendBanner, UserArea, NavLinks, LogoWhite },
setup() {
const mobileStore = useMobileStore();
const { handleResize } = mobileStore;
window.addEventListener("resize", handleResize);
return;
},
});
</script>
<template>
<q-header class="header-container transparent">
<send-banner
@ -21,29 +44,6 @@
</q-header>
</template>
<script lang="js">
import { defineComponent } from 'vue';
import { useMobileStore } from 'src/stores/mobileNav';
import LogoWhite from '../logo/LogoWhite.vue';
import SendBanner from '../ui/SendBanner.vue';
import NavLinks from './NavLinks.vue';
import UserArea from './UserArea.vue';
export default defineComponent({
name: 'header-secondary',
components: { SendBanner, UserArea, NavLinks, LogoWhite },
setup() {
const mobileStore = useMobileStore();
const { handleResize } = mobileStore;
window.addEventListener('resize', handleResize);
return;
},
});
</script>
<style lang="scss" scoped>
.header-container {
display: flex;

View File

@ -1,3 +1,15 @@
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "NavLinks",
components: {},
setup() {
return {};
},
});
</script>
<template>
<nav class="menu-nav">
<ul class="links-list">
@ -24,18 +36,6 @@
</nav>
</template>
<script lang="js">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'NavLinks',
components: {},
setup() {
return {};
},
});
</script>
<style lang="scss" scoped>
.menu-nav .links-list {
display: flex;

View File

@ -1,13 +1,49 @@
<script>
import { defineComponent } from "vue";
import IconCart from "components/icons/IconCart.vue";
import IconHamburger from "components/icons/IconHamburger.vue";
/* import IconUser from "components/icons/IconUser.vue";
import DropdownGroup from "components/quasar-components/dropdown/DropdownGroup.vue";
import DropdownItem from "components/quasar-components/dropdown/DropdownItem.vue"; */
import { storeToRefs } from "pinia";
import { useCartStore } from "src/stores/cart";
import { useMobileStore } from "stores/mobileNav";
export default defineComponent({
name: "user-area",
components: {
IconCart,
// IconUser,
// DropdownGroup,
// DropdownItem,
IconHamburger,
},
setup() {
const mobileStore = useMobileStore();
const { handleOpenMobileNav } = mobileStore;
const cartStore = useCartStore();
const { cartLength } = storeToRefs(cartStore);
return { handleOpenMobileNav, cartLength };
},
});
</script>
<template>
<div class="user-area">
<dropdown-group class="user-area-lang" label="Idioma">
<!-- <dropdown-group class="user-area-lang" label="Idioma">
<dropdown-item current-lang="en"> EN </dropdown-item>
<dropdown-item current-lang="pt"> PT </dropdown-item>
<dropdown-item current-lang="es"> ES </dropdown-item>
</dropdown-group>
<RouterLink class="user-area-link user" to="/"><icon-user /></RouterLink> -->
<RouterLink class="user-area-link user" to="/"><icon-user /></RouterLink>
<RouterLink class="user-area-link cart" to="/checkout">
<RouterLink
class="user-area-link cart"
to="/checkout"
v-if="cartLength > 0"
>
<icon-cart />
<span class="cart-count" :class="cartLength > 0 && 'active'">
{{ cartLength }}
@ -27,37 +63,6 @@
</div>
</template>
<script>
import { defineComponent } from "vue";
import IconCart from "components/icons/IconCart.vue";
import IconHamburger from "components/icons/IconHamburger.vue";
import IconUser from "components/icons/IconUser.vue";
import DropdownGroup from "components/quasar-components/dropdown/DropdownGroup.vue";
import DropdownItem from "components/quasar-components/dropdown/DropdownItem.vue";
import { storeToRefs } from "pinia";
import { useCartStore } from "src/stores/cart";
import { useMobileStore } from "stores/mobileNav";
export default defineComponent({
name: "user-area",
components: {
IconCart,
IconUser,
DropdownGroup,
DropdownItem,
IconHamburger,
},
setup() {
const mobileStore = useMobileStore();
const { handleOpenMobileNav } = mobileStore;
const cartStore = useCartStore();
const { cartLength } = storeToRefs(cartStore);
return { handleOpenMobileNav, cartLength };
},
});
</script>
<style lang="scss" scoped>
.user-area {
display: flex;

View File

@ -1,3 +1,15 @@
<script>
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "product-carousel",
components: {},
setup() {
return { slide: ref(1), fullscreen: ref(false) };
},
});
</script>
<template>
<q-carousel
animated
@ -24,16 +36,4 @@
</q-carousel>
</template>
<script lang="js">
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'product-carousel',
components: {},
setup() {
return { slide: ref(1), fullscreen: ref(false) };
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,3 +1,59 @@
<script>
import { useIntersectionObserver } from "@vueuse/core";
import { storeToRefs } from "pinia";
import { defineComponent, ref } from "vue";
import Calendar from "src/components/@inputs/Calendar.vue";
import PostalCode from "src/components/@inputs/PostalCode.vue";
import IconSearch from "src/components/icons/IconSearch.vue";
import { usePostalCalendar } from "src/hooks/usePostalCalendar";
import { useMobileStore } from "src/stores/mobileNav";
export default defineComponent({
name: "vertical-carousel-imgs",
props: {
imgsArr: {
type: Array,
default: () => [""],
},
},
setup() {
const {
onSubmit,
fields: { calendar, calendarAttrs, postalCode, postalCodeAttrs },
errors,
} = usePostalCalendar({ type: "home" });
const mobileStore = useMobileStore();
const { screenWidth } = storeToRefs(mobileStore);
const { handleResize } = mobileStore;
const target = ref(null);
const navPos = ref("bottom");
useIntersectionObserver(target, ([{ isIntersecting }]) => {
mobileStore.isCarouselVisible = isIntersecting;
});
document.addEventListener("resize", handleResize);
return {
slide: ref("style"),
navPos,
target,
screenWidth,
errors,
onSubmit,
postalCode,
postalCodeAttrs,
calendar,
calendarAttrs,
};
},
components: { IconSearch, Calendar, PostalCode },
});
</script>
<template>
<div
ref="target"
@ -8,7 +64,7 @@
navigation-position="right"
style="min-height: 100dvh"
v-model="slide"
:navigation="screenWidth >= 768"
navigation
autoplay
infinite
animated
@ -35,65 +91,49 @@
</p>
</header>
<div class="carousel-content-body">
<form @submit="onSubmit" class="carousel-content-body">
<div class="carousel-content-item">
<Calendar />
<!-- <Calendar /> -->
<Calendar>
<q-input
borderless
class="custom-date-input"
v-model="calendar"
v-bind="calendarAttrs"
:error="!!errors.date"
placeholder="Elige una fecha"
mask="##/##/####"
dense
/>
</Calendar>
</div>
<div class="carousel-content-item">
<PostalCode />
<!-- <PostalCode /> -->
<PostalCode>
<q-input
borderless
class="custom-main-paragraph"
v-model="postalCode"
v-bind="postalCodeAttrs"
:error="!!errors.postalCode"
placeholder="código postal"
mask="#####"
dense
/>
</PostalCode>
</div>
<q-btn to="/categoria/ramos" class="btn carousel-content-item">
<q-btn type="submit" class="btn carousel-content-item">
<IconSearch /> ver disponibilidad
</q-btn>
</div>
</form>
<!-- <footer class="carousel-content-footer"></footer> -->
</div>
</div>
</template>
<script lang="js">
import { useIntersectionObserver } from '@vueuse/core';
import { defineComponent, ref } from 'vue';
import { storeToRefs } from 'pinia';
import Calendar from 'src/components/@inputs/Calendar.vue';
import PostalCode from 'src/components/@inputs/PostalCode.vue';
import IconSearch from 'src/components/icons/IconSearch.vue';
import { useMobileStore } from 'src/stores/mobileNav';
export default defineComponent({
name: 'vertical-carousel-imgs',
props: {
imgsArr: {
type: Array,
default: () => [''],
},
},
setup() {
const mobileStore = useMobileStore();
const { screenWidth } = storeToRefs(mobileStore);
const { handleResize } = mobileStore;
const target = ref(null);
const navPos = ref('bottom');
useIntersectionObserver(target, ([{ isIntersecting }]) => {
mobileStore.isCarouselVisible = isIntersecting;
});
document.addEventListener('resize', handleResize);
return {
slide: ref('style'),
navPos,
target,
screenWidth,
};
},
components: { IconSearch, Calendar, PostalCode },
});
</script>
<style lang="scss" scoped>
.vertical-carousel-container {
position: relative;
@ -106,7 +146,7 @@ export default defineComponent({
min-height: fit-content;
left: 0;
right: 0;
bottom: 70px;
bottom: 110px;
width: min(100%, 845px);
margin: 0 auto;
& .carousel-content-header {
@ -135,12 +175,11 @@ export default defineComponent({
flex-wrap: wrap;
border-radius: 10px 0 0 10px;
overflow: hidden;
/* margin-bottom: 82px; */
min-height: 150px;
min-height: 96px;
& .carousel-content-item {
display: flex;
align-items: center;
padding: 27px 53px 34px 48px;
padding-inline: 53px 34px;
flex: 1;
position: relative;
&:first-child::after {
@ -169,7 +208,7 @@ export default defineComponent({
&:last-child {
border-radius: 0 10px 10px 0;
padding: 27px 56px 34px;
padding-inline: 56px;
}
}
}

View File

@ -1,3 +1,18 @@
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "dropdown-group",
props: {
label: {
type: String,
default: "",
required: true,
},
},
});
</script>
<template>
<q-btn-dropdown :label="label">
<q-list>
@ -6,21 +21,6 @@
</q-btn-dropdown>
</template>
<script lang="js">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'dropdown-group',
props: {
label: {
type: String,
default: '',
required: true,
},
},
});
</script>
<style lang="scss" scoped>
.q-btn {
text-transform: initial;

View File

@ -1,24 +1,14 @@
<template>
<q-item clickable v-close-popup @click="onItemClick(currentLang)">
<q-item-section>
<q-item-label>
<slot></slot>
</q-item-label>
</q-item-section>
</q-item>
</template>
<script lang="js">
import { storeToRefs } from 'pinia';
import { useLanguageStore } from 'src/stores/language';
import { defineComponent } from 'vue';
<script>
import { storeToRefs } from "pinia";
import { useLanguageStore } from "src/stores/language";
import { defineComponent } from "vue";
export default defineComponent({
name: 'DropdownItem',
name: "DropdownItem",
props: {
currentLang: {
type: String,
default: '',
default: "",
},
},
setup() {
@ -33,3 +23,13 @@ export default defineComponent({
},
});
</script>
<template>
<q-item clickable v-close-popup @click="onItemClick(currentLang)">
<q-item-section>
<q-item-label>
<slot></slot>
</q-item-label>
</q-item-section>
</q-item>
</template>

View File

@ -1,3 +1,20 @@
<script>
import { defineComponent } from "vue";
import IconChatRoundedFill from "../icons/IconChatRoundedFill.vue";
import Container from "../ui/Container.vue";
export default defineComponent({
name: "dudas-section",
components: { IconChatRoundedFill, Container },
props: {
isWhite: {
type: Boolean,
defaul: false,
},
},
});
</script>
<template>
<section
class="questions-section"
@ -21,23 +38,6 @@
</section>
</template>
<script lang="js">
import { defineComponent } from 'vue';
import IconChatRoundedFill from '../icons/IconChatRoundedFill.vue';
import Container from '../ui/Container.vue';
export default defineComponent({
name: 'dudas-section',
components: { IconChatRoundedFill, Container },
props: {
isWhite: {
type: Boolean,
defaul: false,
},
},
});
</script>
<style lang="scss" scoped>
.questions-section {
background-color: $secondary-10;

View File

@ -1,3 +1,21 @@
<script>
import { defineComponent } from "vue";
import IconCarr from "../icons/IconCar.vue";
import IconChatRoundedFill from "../icons/IconChatRoundedFill.vue";
import IconLocation from "../icons/IconLocation.vue";
// import Container from '../ui/Container.vue';
export default defineComponent({
name: "info-section",
components: {
IconChatRoundedFill,
IconLocation,
IconCarr,
},
});
</script>
<template>
<section class="info-container">
<div class="info-content amplia">
@ -40,24 +58,6 @@
</section>
</template>
<script lang="js">
import { defineComponent } from 'vue';
import IconCarr from '../icons/IconCar.vue';
import IconChatRoundedFill from '../icons/IconChatRoundedFill.vue';
import IconLocation from '../icons/IconLocation.vue';
// import Container from '../ui/Container.vue';
export default defineComponent({
name: 'info-section',
components: {
IconChatRoundedFill,
IconLocation,
IconCarr,
},
});
</script>
<style lang="scss" scoped>
.info-container {
display: flex;

View File

@ -1,3 +1,20 @@
<script>
import { defineComponent } from "vue";
import IconQuestion from "../icons/IconQuestion.vue";
import LogoWhite from "../logo/LogoWhite.vue";
import Container from "../ui/Container.vue";
import QuestionForm from "../ui/QuestionForm.vue";
export default defineComponent({
name: "question-component",
components: { Container, LogoWhite, IconQuestion, QuestionForm },
setup() {
return {};
},
});
</script>
<template>
<section
class="question-container"
@ -32,27 +49,6 @@
</section>
</template>
<script lang="js">
import { defineComponent, ref } from 'vue';
import IconQuestion from '../icons/IconQuestion.vue';
import LogoWhite from '../logo/LogoWhite.vue';
import Container from '../ui/Container.vue';
import QuestionForm from '../ui/QuestionForm.vue';
export default defineComponent({
name: 'question-component',
components: { Container, LogoWhite, IconQuestion, QuestionForm },
setup() {
const handleSubmit = () => {
console.log('Foi submit');
};
return { handleSubmit, text: ref(''), checkbox: ref(false) };
},
});
</script>
<style lang="scss" scoped>
p {
color: $white;

View File

@ -1,3 +1,29 @@
<script>
import { defineComponent } from "vue";
import IconChatRoundedFill from "../icons/IconChatRoundedFill.vue";
import IconCheck from "../icons/IconCheck.vue";
import IconFlower from "../icons/IconFlower.vue";
import IconQuality from "../icons/IconQuality.vue";
import IconService from "../icons/IconService.vue";
import IconShop from "../icons/IconShop.vue";
import LogoWhite from "../logo/LogoWhite.vue";
import Container from "../ui/Container.vue";
export default defineComponent({
name: "reasons-section",
components: {
Container,
LogoWhite,
IconChatRoundedFill,
IconQuality,
IconFlower,
IconShop,
IconService,
IconCheck,
},
});
</script>
<template>
<section
class="reasons-container"
@ -84,32 +110,6 @@
</section>
</template>
<script lang="js">
import { defineComponent } from 'vue';
import IconChatRoundedFill from '../icons/IconChatRoundedFill.vue';
import IconCheck from '../icons/IconCheck.vue';
import IconFlower from '../icons/IconFlower.vue';
import IconQuality from '../icons/IconQuality.vue';
import IconService from '../icons/IconService.vue';
import IconShop from '../icons/IconShop.vue';
import LogoWhite from '../logo/LogoWhite.vue';
import Container from '../ui/Container.vue';
export default defineComponent({
name: 'reasons-section',
components: {
Container,
LogoWhite,
IconChatRoundedFill,
IconQuality,
IconFlower,
IconShop,
IconService,
IconCheck,
},
});
</script>
<style lang="scss" scoped>
p,
h5 {

View File

@ -1,3 +1,57 @@
<script>
import { storeToRefs } from "pinia";
import { defineComponent, onMounted, ref } from "vue";
import { useMobileStore } from "src/stores/mobileNav";
import IconArrowCircleFilledLeft from "../icons/IconArrowCircleFilledLeft.vue";
import IconArrowCircleFilledRight from "../icons/IconArrowCircleFilledRight.vue";
export default defineComponent({
name: "SwiperComponent",
components: { IconArrowCircleFilledLeft, IconArrowCircleFilledRight },
setup() {
const mobileStore = useMobileStore();
const { screenWidth } = storeToRefs(mobileStore);
const prevBtn = ref(null);
const nextBtn = ref(null);
const swiperContainer = ref(null);
const prevSwiperBtn = ref(null);
const nextSwiperBtn = ref(null);
onMounted(() => {
// console.log('Montado!');
swiperContainer.value =
document.querySelector("swiper-container").shadowRoot;
prevSwiperBtn.value = swiperContainer.value.querySelector(
".swiper-button-prev"
);
nextSwiperBtn.value = swiperContainer.value.querySelector(
".swiper-button-next"
);
const swiperDisplay = "none";
nextSwiperBtn.value.style.display = swiperDisplay;
prevSwiperBtn.value.style.display = swiperDisplay;
nextBtn.value = document.querySelector(".swiper-btn.next");
prevBtn.value = document.querySelector(".swiper-btn.prev");
});
return {
screenWidth,
handlePrev() {
// console.log('Prev click');
prevSwiperBtn.value.click();
},
handleNext() {
// console.log('Next click');
nextSwiperBtn.value.click();
},
};
},
});
</script>
<template>
<q-btn
title="previous button"
@ -15,10 +69,8 @@
class="swiper"
:space-between="screenWidth > 1024 ? 56 : 25"
:slides-per-view="'auto'"
:centered-slides="true"
:grabCursor="true"
:navigation="true"
:loop="true"
:pagination="{
dynamicBullets: true,
clickable: true,
@ -43,72 +95,6 @@
</q-btn>
</template>
<script lang="js">
import { storeToRefs } from 'pinia';
import { defineComponent, onMounted, ref } from 'vue';
import { useMobileStore } from 'src/stores/mobileNav';
import IconArrowCircleFilledLeft from '../icons/IconArrowCircleFilledLeft.vue';
import IconArrowCircleFilledRight from '../icons/IconArrowCircleFilledRight.vue';
export default defineComponent({
name: 'SwiperComponent',
components: { IconArrowCircleFilledLeft, IconArrowCircleFilledRight },
setup() {
const mobileStore = useMobileStore();
const { screenWidth } = storeToRefs(mobileStore);
const prevBtn = ref(null);
const nextBtn = ref(null);
const swiperContainer = ref(null);
const prevSwiperBtn = ref(null);
const nextSwiperBtn = ref(null);
onMounted(() => {
// console.log('Montado!');
swiperContainer.value =
document.querySelector('swiper-container').shadowRoot;
prevSwiperBtn.value = swiperContainer.value.querySelector(
'.swiper-button-prev'
);
nextSwiperBtn.value = swiperContainer.value.querySelector(
'.swiper-button-next'
);
const swiperDisplay = 'none';
nextSwiperBtn.value.style.display = swiperDisplay;
prevSwiperBtn.value.style.display = swiperDisplay;
nextBtn.value = document.querySelector('.swiper-btn.next');
prevBtn.value = document.querySelector('.swiper-btn.prev');
});
/* onUpdated(() => {
console.log('Atualizado!');
console.groupCollapsed('%c Custom', 'color: tomato;');
console.log({ prevBtn: prevBtn.value, nextBtn: nextBtn.value });
console.groupEnd();
console.groupCollapsed('%c Swiper', 'color: hotpink;');
console.log(prevSwiperBtn.value);
console.groupEnd();
}); */
return {
screenWidth,
handlePrev() {
// console.log('Prev click');
prevSwiperBtn.value.click();
},
handleNext() {
// console.log('Next click');
nextSwiperBtn.value.click();
},
};
},
});
</script>
<style lang="scss">
.swiper {
height: 100%;

View File

@ -1,15 +1,15 @@
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "button-component",
});
</script>
<template>
<button class="btn" type="button">
<slot></slot>
</button>
</template>
<script lang="js">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'button-component',
});
</script>
<style lang="scss"></style>

View File

@ -1,8 +1,76 @@
<script>
import { defineComponent, ref } from "vue";
import IconEyes from "../icons/IconEyes.vue";
export default defineComponent({
name: "card-component",
components: { IconEyes },
props: {
price: {
type: String,
default: "",
required: true,
},
title: {
type: String,
default: "",
required: true,
},
discount: {
type: String,
default: "",
},
imgSrc: {
type: String,
default: "",
required: true,
},
imgClass: {
type: String,
default: "",
},
headClass: {
type: String,
default: "",
},
isNew: {
type: Boolean,
default: false,
},
size: {
type: String,
default: "md-card",
},
alt: {
type: String,
default: "",
},
id: {
type: String,
required: true,
},
},
setup({ price, discount }) {
const isLoaded = ref(false);
const percent = +discount / 100;
const priceWithoutLetter = ~~price.replaceAll("€", "");
const finalValue = ~~(priceWithoutLetter - priceWithoutLetter * percent);
const onLoad = () => {
isLoaded.value = true;
};
return { onLoad, isLoaded, finalValue, priceWithoutLetter };
},
});
</script>
<template>
<div class="card-container" :class="size">
<RouterLink
:to="`/product/${id}`"
class="card-container-head"
:class="[headClass]"
role="heading"
>
<div v-if="isNew || discount" class="tags">
@ -12,9 +80,17 @@
<img
class="card-img"
:src="imgSrc ? imgSrc : '../../assets/empty-img.png'"
:class="[imgClass]"
:src="imgSrc ? imgSrc : '../../assets/empty-img.jpg'"
:alt="alt"
@load="handleLoad"
:key="imgSrc"
@load="onLoad"
/>
<q-skeleton
v-if="!isLoaded"
class="skeleton"
width="100%"
height="100%"
/>
<div class="head-hovered">
@ -24,113 +100,50 @@
</RouterLink>
<div class="card-container-body">
<p class="card-name" v-if="productName">{{ productName }}</p>
<p class="card-name" v-if="title">{{ title }}</p>
<div class="card-values">
<p class="price" v-if="productValue">{{ productValue }}</p>
<p class="price offer tachado" v-if="+valueWithDiscount > 0">
{{ valueWithDiscount() }}
<p class="price" v-if="finalValue">{{ finalValue }}</p>
<p class="price offer tachado" v-if="priceWithoutLetter !== finalValue">
{{ priceWithoutLetter }}
</p>
</div>
</div>
</div>
</template>
<script lang="js">
import { defineComponent, ref } from 'vue';
import IconEyes from '../icons/IconEyes.vue';
export default defineComponent({
name: 'card-component',
components: { IconEyes },
props: {
productValue: {
type: String,
default: '',
required: true,
},
productName: {
type: String,
default: '',
required: true,
},
discount: {
type: String,
default: '',
},
imgSrc: {
type: String,
default: '',
required: true,
},
isNew: {
type: Boolean,
default: false,
},
size: {
type: String,
default: '',
},
alt: {
type: String,
default: '',
},
id: {
type: Number,
required: true,
},
},
setup({ productValue, discount }) {
const loadingImg = ref(true);
const handleLoad = () => {
loadingImg.value = false;
};
const valueWithDiscount = () => {
const productWithouCaracters = ~~productValue.replaceAll('€', '');
if (discount) {
const finalValue = (+discount / 100) * productWithouCaracters;
return finalValue.toFixed(2);
}
};
return { handleLoad, valueWithDiscount };
},
});
</script>
<style lang="scss" scoped>
.card-container {
display: flex;
flex-direction: column;
gap: 12px;
user-select: none;
height: 100%;
&:focus {
outline: 2px solid $secondary;
}
&.sm-card {
width: min(100%, 166px);
& .card-container-head {
border-radius: 10px;
min-height: 188px;
}
}
&.md-card {
width: min(100%, 310px);
& .card-container-head {
min-height: 352px;
height: 300px;
@media only screen and (min-width: $med-hg) {
height: 352px;
}
@media only screen and (max-width: $med-md) {
border-radius: 10px;
height: 188px;
}
}
}
&.lg-card {
width: min(100%, 360px);
& .card-container-head {
min-height: 409px;
height: 409px;
@media only screen and (max-width: $med-lg) {
border-radius: 10px;
height: 188px;
}
}
}
@ -138,6 +151,15 @@ export default defineComponent({
border-radius: 15px;
overflow: hidden;
position: relative;
height: 100%;
max-height: 409px;
&.list-products-head {
min-height: 365px;
@media only screen and (max-width: $med-sm) {
min-height: 190px;
}
}
&:focus-visible {
outline: 2px solid $primary-light;
}
@ -193,8 +215,27 @@ export default defineComponent({
& .card-img {
width: 100%;
height: 100%;
object-fit: cover;
&.list-products {
@media only screen and (min-width: calc($med-md + 1px)) {
min-height: 352px;
}
@media only screen and (max-width: $med-md) {
min-height: 190px;
}
}
&.carousel {
max-height: 410px;
}
}
& .skeleton {
position: absolute;
inset: 0;
background-color: $primary;
opacity: 0.5;
}
& .head-hovered {

View File

@ -1,21 +1,21 @@
<template>
<q-btn flat @click="handleClick"><IconChat /></q-btn>
</template>
<script lang="js">
import { defineComponent } from 'vue';
import IconChat from '../icons/IconChat.vue';
<script>
import { defineComponent } from "vue";
import IconChat from "../icons/IconChat.vue";
export default defineComponent({
name: 'chat-component',
name: "chat-component",
components: { IconChat },
setup() {
const handleClick = () => {
console.log('click');
console.log("click");
};
return { handleClick };
},
});
</script>
<template>
<q-btn flat @click="handleClick"><IconChat /></q-btn>
</template>
<style lang="scss" scoped></style>

View File

@ -1,13 +1,3 @@
<template>
<component
:is="tag"
class="custom-container"
:class="cardContainer && 'cards-container'"
>
<slot></slot>
</component>
</template>
<script>
import { defineComponent } from "vue";
@ -29,3 +19,13 @@ export default defineComponent({
},
});
</script>
<template>
<component
:is="tag"
class="custom-container"
:class="cardContainer && 'cards-container'"
>
<slot></slot>
</component>
</template>

View File

@ -1,45 +1,12 @@
<template>
<div class="mobile-nav-container" :class="!isOpenNav && 'hide'">
<header class="mobile-nav-links">
<RouterLink @click="closeNav" class="mobile-link" to="/categoria/ramos"
>Ramos</RouterLink
>
<RouterLink @click="closeNav" class="mobile-link" to="/categoria/plantas">
Plantas
</RouterLink>
<RouterLink @click="closeNav" class="mobile-link" to="/"
>Floranet</RouterLink
>
<RouterLink @click="closeNav" class="mobile-link" to="/">FAQs</RouterLink>
<RouterLink @click="closeNav" class="mobile-link" to="/"
>Contacta</RouterLink
>
</header>
<script>
import { useMobileStore } from "src/stores/mobileNav";
<div class="mobile-nav-lang">
<p class="mobile-lang-paragraph">Idioma</p>
<select class="mobile-lang-select" name="lang">
<option value="">Español</option>
</select>
</div>
<footer class="mobile-nav-footer">
<RouterLink to="/" class="btn outlined rounded sm-btn">
¿Tienes dudas? hablemos <IconChatRoundedFill />
</RouterLink>
</footer>
</div>
</template>
<script lang="js">
import { useMobileStore } from 'src/stores/mobileNav';
import { storeToRefs } from 'pinia';
import { defineComponent, watch } from 'vue';
import IconChatRoundedFill from '../icons/IconChatRoundedFill.vue';
import { storeToRefs } from "pinia";
import { defineComponent, watch } from "vue";
import IconChatRoundedFill from "../icons/IconChatRoundedFill.vue";
export default defineComponent({
name: 'mobile-nav',
name: "mobile-nav",
components: { IconChatRoundedFill },
setup() {
const mobileStore = useMobileStore();
@ -51,16 +18,16 @@ export default defineComponent({
function closeNav() {
isOpenNav.value = false;
console.log('foi click');
console.log("foi click");
}
watch(isOpenNav, (newValue) => {
if (newValue) {
setBodyStyle('hidden');
setBodyStyle("hidden");
window.scrollTo(0, 0);
return;
}
setBodyStyle('visible');
setBodyStyle("visible");
});
return { isOpenNav, closeNav };
@ -68,6 +35,39 @@ export default defineComponent({
});
</script>
<template>
<div class="mobile-nav-container" :class="!isOpenNav && 'hide'">
<header class="mobile-nav-links">
<RouterLink @click="closeNav" class="mobile-link" to="/categoria/ramos">
Ramos
</RouterLink>
<RouterLink @click="closeNav" class="mobile-link" to="/categoria/plantas">
Plantas
</RouterLink>
<RouterLink @click="closeNav" class="mobile-link" to="/example">
Floranet
</RouterLink>
<!-- <RouterLink @click="closeNav" class="mobile-link" to="/">FAQs</RouterLink> -->
<RouterLink @click="closeNav" class="mobile-link" to="/example">
Contacta
</RouterLink>
</header>
<!-- <div class="mobile-nav-lang">
<p class="mobile-lang-paragraph">Idioma</p>
<select class="mobile-lang-select" name="lang">
<option value="">Español</option>
</select>
</div> -->
<footer class="mobile-nav-footer">
<RouterLink to="/" class="btn outlined rounded sm-btn">
¿Tienes dudas? hablemos <IconChatRoundedFill />
</RouterLink>
</footer>
</div>
</template>
<style lang="scss" scoped>
.mobile-nav-container {
position: absolute;

View File

@ -1,3 +1,78 @@
<script>
import { defineComponent } from "vue";
import { usePostalCalendar } from "src/hooks/usePostalCalendar";
import Calendar from "../@inputs/Calendar.vue";
import PostalCode from "../@inputs/PostalCode.vue";
import PriceRange from "../@inputs/PriceRange.vue";
import IconCloseModal from "../icons/IconCloseModal.vue";
import IconSearch from "../icons/IconSearch.vue";
export default defineComponent({
name: "modal-component",
components: {
IconSearch,
IconCloseModal,
PriceRange,
Calendar,
PostalCode,
},
props: {
modalItem: {
type: String,
default: "",
},
typeModal: {
type: String,
default: "",
},
},
setup({ modalItem, typeModal }) {
const {
onSubmit,
fields: {
calendar,
calendarAttrs,
postalCode,
postalCodeAttrs,
priceRange,
priceRangeAttrs,
},
errors,
modalStore,
} = usePostalCalendar({
modalItem,
type: typeModal,
});
const modalTextContent = {
isOpenAvailability: {
title: "Disponibilidad",
subtitle: "Elige a dónde y cuando quieres enviar el ramo",
},
isOpenFilters: {
title: "Filtros",
subtitle: "Personaliza tu búsqueda",
},
};
return {
errors,
onSubmit,
modalStore,
modalTextContent,
postalCode,
postalCodeAttrs,
calendar,
calendarAttrs,
priceRange,
priceRangeAttrs,
};
},
});
</script>
<template>
<q-dialog v-model="modalStore[modalItem]" class="modal-container">
<q-card class="modal-content">
@ -23,9 +98,22 @@
</p>
<div class="modal-body-content">
<div v-if="modalItem === 'isOpenFilters'" class="modal-body-filters">
<form @submit="onSubmit" id="filters-form">
<div
v-if="modalItem === 'isOpenFilters'"
class="modal-body-filters"
>
<div class="filter-field">
<PriceRange />
<PriceRange :min="priceRange.min" :max="priceRange.max">
<q-range
v-model="priceRange"
v-bind="priceRangeAttrs"
:min="0"
:max="200"
color="primary"
/>
<!-- @change="rangePriceStore.handlePriceRange" -->
</PriceRange>
</div>
</div>
@ -33,75 +121,63 @@
v-if="modalItem === 'isOpenAvailability'"
class="modal-body-availability"
>
<calendar />
<Calendar>
<q-input
borderless
class="custom-date-input"
v-model="calendar"
v-bind="calendarAttrs"
:error="!!errors.date"
placeholder="Elige una fecha"
mask="##/##/####"
dense
/>
</Calendar>
<postal-code />
<PostalCode>
<q-input
borderless
class="custom-main-paragraph"
v-model="postalCode"
v-bind="postalCodeAttrs"
:error="!!errors.postalCode"
placeholder="código postal"
mask="#####"
dense
/>
</PostalCode>
</div>
</form>
</div>
</q-card-section>
<q-btn
flat
type="submit"
align="center"
class="modal-footer"
form="filters-form"
>
<!-- v-close-popup -->
<IconSearch />
<p>ver disponibilidad</p>
</q-btn>
<!-- <q-btn
flat
type="button"
align="center"
class="modal-footer"
@click="modalStore.handleSubmit({ isModalContent: modalItem })"
form="filters-form"
@click="handleSubmit({ content: modalItem })"
v-close-popup
>
<IconSearch />
<p>ver disponibilidad</p>
</q-btn>
</q-btn> -->
</q-card>
</q-dialog>
</template>
<script lang="js">
import { defineComponent } from 'vue';
import { useModalStore } from 'src/stores/modalStore';
import Calendar from '../@inputs/Calendar.vue';
import PostalCode from '../@inputs/PostalCode.vue';
import PriceRange from '../@inputs/PriceRange.vue';
import IconCloseModal from '../icons/IconCloseModal.vue';
import IconSearch from '../icons/IconSearch.vue';
export default defineComponent({
name: 'modal-component',
components: {
IconSearch,
IconCloseModal,
PriceRange,
Calendar,
PostalCode,
},
props: {
modalItem: {
type: String,
default: '',
},
},
setup() {
const modalStore = useModalStore();
const modalTextContent = {
isOpenAvailability: {
title: 'Disponibilidad',
subtitle: 'Elige a dónde y cuando quieres enviar el ramo',
},
isOpenFilters: {
title: 'Filtros',
subtitle: 'Personaliza tu búsqueda',
},
};
return {
modalStore,
modalTextContent,
};
},
});
</script>
<style lang="scss">
.modal-container {
& .q-dialog__backdrop {

View File

@ -1,3 +1,70 @@
<script>
import { useQuasar } from "quasar";
import { useFormStore } from "src/stores/forms";
import { useForm } from "vee-validate";
import { defineComponent } from "vue";
import IconArrowRightOne from "src/components/icons/IconArrowRightOne.vue";
import { questionSchema } from "src/utils/zod/schemas/questionSchema";
export default defineComponent({
name: "QuestionForm",
components: { IconArrowRightOne },
setup() {
const $q = useQuasar();
const formStore = useFormStore();
const { handleQuestionData } = formStore;
const { errors, meta, defineField, handleSubmit, handleReset } = useForm({
validationSchema: questionSchema,
initialValues: {
terms: false,
},
});
const [firstName, firstNameAttrs] = defineField("name");
const [secondName, secondNameAttrs] = defineField("surname");
const [email, emailAttrs] = defineField("email");
const [phone, phoneAttrs] = defineField("phone");
const [query, queryAttrs] = defineField("query");
const [message, messageAttrs] = defineField("message");
const [terms, termsAttrs] = defineField("terms");
const onSubmit = handleSubmit((values) => {
console.log(values);
handleQuestionData(values);
handleReset();
if (!terms.value) {
$q.notify({
color: "negative",
message: "Primero tienes que aceptar la licencia y las condiciones",
});
return;
}
});
return {
errors,
firstName,
firstNameAttrs,
secondName,
secondNameAttrs,
email,
emailAttrs,
phone,
phoneAttrs,
query,
queryAttrs,
message,
messageAttrs,
terms,
termsAttrs,
onSubmit,
meta,
};
},
});
</script>
<template>
<form class="question-form-body" @submit.prevent="onSubmit">
<div class="fields">
@ -90,73 +157,6 @@
</form>
</template>
<script>
import { useQuasar } from "quasar";
import { useFormStore } from "src/stores/forms";
import { useForm } from "vee-validate";
import { defineComponent } from "vue";
import IconArrowRightOne from "src/components/icons/IconArrowRightOne.vue";
import { questionSchema } from "src/utils/zod/schemas/questionSchema";
export default defineComponent({
name: "QuestionForm",
components: { IconArrowRightOne },
setup() {
const $q = useQuasar();
const formStore = useFormStore();
const { handleQuestionData } = formStore;
const { errors, meta, defineField, handleSubmit, handleReset } = useForm({
validationSchema: questionSchema,
initialValues: {
terms: false,
},
});
const [firstName, firstNameAttrs] = defineField("name");
const [secondName, secondNameAttrs] = defineField("surname");
const [email, emailAttrs] = defineField("email");
const [phone, phoneAttrs] = defineField("phone");
const [query, queryAttrs] = defineField("query");
const [message, messageAttrs] = defineField("message");
const [terms, termsAttrs] = defineField("terms");
const onSubmit = handleSubmit((values) => {
console.log(values);
handleQuestionData(values);
handleReset();
if (!terms.value) {
$q.notify({
color: "negative",
message: "Primero tienes que aceptar la licencia y las condiciones",
});
return;
}
});
return {
errors,
firstName,
firstNameAttrs,
secondName,
secondNameAttrs,
email,
emailAttrs,
phone,
phoneAttrs,
query,
queryAttrs,
message,
messageAttrs,
terms,
termsAttrs,
onSubmit,
meta,
};
},
});
</script>
<style lang="scss" scoped>
.question-form-body {
display: flex;

View File

@ -1,3 +1,28 @@
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "send-banner",
props: {
leftText: {
type: String,
default: "",
required: true,
},
rightText: {
type: String,
default: "",
required: true,
},
mobileText: {
type: String,
default: "",
required: true,
},
},
});
</script>
<template>
<div class="send-banner-container">
<div class="send-banner-wrapper custom-container">
@ -8,31 +33,6 @@
</div>
</template>
<script lang="js">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'send-banner',
props: {
leftText: {
type: String,
default: '',
required: true,
},
rightText: {
type: String,
default: '',
required: true,
},
mobileText: {
type: String,
default: '',
required: true,
},
},
});
</script>
<style lang="scss" scoped>
.send-banner-container {
display: flex;

View File

@ -151,6 +151,9 @@ html {
grid-auto-rows: 1fr;
gap: 47px;
}
&.category-container {
width: min(100%, 1920px);
}
//! 1440px
@media only screen and (max-width: calc($med-xlg + 100px)) {
@ -164,7 +167,7 @@ html {
padding-inline: 16px;
&.cards-container {
gap: 25px;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 166px), 1fr));
grid-template-columns: repeat(auto-fill, minmax(min(100%, 235px), 1fr));
}
}
@ -291,7 +294,7 @@ p,
color: $title-default;
}
& .pege-subtitle {
& .page-subtitle {
font-family: $font-questrial;
font-size: 1rem;
text-align: center;
@ -313,3 +316,42 @@ body {
margin: 0 auto;
padding: 0 20px;
}
.carousel-content-item .custom-block-content .custom-head-paragraph {
margin-bottom: -14px;
}
//! QUASAR
.q-virtual-scroll__content .q-item .q-item__label {
font-family: $font-questrial;
font-size: $font-14;
color: $text-default;
line-height: 21px;
letter-spacing: 0.28px;
}
.checkout-fields {
& .q-placeholder::placeholder,
& .q-field__label {
font-family: $font-questrial;
font-size: $font-14;
color: $text-default;
line-height: 21px;
letter-spacing: 0.28px;
}
}
.q-carousel__navigation {
@media only screen and (max-width: $med-md) {
display: none;
}
}
.custom-input-el {
& .q-placeholder::placeholder {
font-family: $font-questrial;
color: $text-normal-100 !important;
opacity: 1;
}
}

View File

@ -0,0 +1,35 @@
import { Notify } from "quasar";
export function quasarNotify({ message = "", type, timeout = 1000 }) {
const obj = {
success: () =>
Notify.create({
message,
color: "positive",
position: "top",
icon: "check_circle",
timeout,
}),
warning: () =>
Notify.create({
message,
color: "warning",
position: "top",
icon: "report_problem",
timeout,
}),
erro: () =>
Notify.create({
message,
color: "negative",
position: "top",
icon: "report_problem",
timeout,
}),
};
if (type) {
return obj[type]() || console.error(`Type is invalid! TYPE: ${type}`);
}
console.error("Type is required, success, warning or erro");
}

View File

@ -0,0 +1,157 @@
import { toTypedSchema } from "@vee-validate/zod";
import { storeToRefs } from "pinia";
import { useForm } from "vee-validate";
import { watch } from "vue";
import { useRouter } from "vue-router";
import { apiBack } from "src/boot/axios";
import { quasarNotify } from "src/functions/quasarNotify";
import { useCartStore } from "src/stores/cart";
import { useFormStore } from "src/stores/forms";
import { useModalStore } from "src/stores/modalStore";
import { useRangePriceStore } from "src/stores/rangePrice";
import { availabilitySchema } from "src/utils/zod/schemas";
import { rangePriceSchema } from "src/utils/zod/schemas/rangePriceSchema";
export function usePostalCalendar({ modalItem = "", type = "home" }) {
const { push } = useRouter();
const rangePriceStore = useRangePriceStore();
const { rangeValue } = storeToRefs(rangePriceStore);
const modalStore = useModalStore();
const { isOpenAvailability } = storeToRefs(modalStore);
const formStore = useFormStore();
const { availability, sortProductFilters } = storeToRefs(formStore);
const cartStore = useCartStore();
const { addToCart, getProducts } = cartStore;
const { currentProduct, products } = storeToRefs(cartStore);
const min = 0;
const max = 200;
const { handleSubmit, handleReset, defineField, errors } = useForm({
validationSchema: toTypedSchema(
type !== "filter" ? availabilitySchema : rangePriceSchema
),
initialValues: {
range: {
min,
max,
},
postalCode: "",
date: "",
},
});
const [calendar, calendarAttrs] = defineField("date");
const [postalCode, postalCodeAttrs] = defineField("postalCode");
const [priceRange, priceRangeAttrs] = defineField("range");
const [dedication, dedicationAttrs] = defineField("dedication");
watch(errors, (newErrors) => {
const errorsObj = {
postalCode: () =>
quasarNotify({ message: newErrors.postalCode, type: "erro" }),
date: () => quasarNotify({ message: newErrors.date, type: "erro" }),
range: () => quasarNotify({ message: newErrors.range, type: "erro" }),
};
const keys = Object.keys(newErrors);
keys.forEach((key) => {
errorsObj[key]();
});
});
const onSubmit = handleSubmit((values) => {
const postalAndDateParams = {
postalCode: values.postalCode,
dateExpired: values.date,
};
const objVal = {
home: async () => {
console.log(type);
getProducts(
{
itens: 30,
category: 1,
postalCode: values.postalCode,
dateExpired: values.date,
},
() => push("/categoria/ramos")
);
},
product: async () => {
console.log(type);
try {
const {
data: { data },
} = await apiBack.get(`products/slug/${currentProduct.value.id}`, {
params: postalAndDateParams,
});
} catch (error) {
console.error(error);
push("/");
}
},
availability: async () => {
console.log(type);
getProducts({
itens: 20,
postalCode: values.postalCode,
dateExpired: values.date,
});
},
filter: async () => {
console.log(type);
rangeValue.value.max = values.range.max;
rangeValue.value.min = values.range.min;
const category = sortProductFilters.value.category;
const categoryObj = {
plantas: 1,
ramos: 2,
};
const params = {
itens: 20,
category: categoryObj[category],
minPrice: values.range.min,
maxPrice: values.range.max,
};
getProducts(params);
},
};
objVal[type]() ||
console.error(
`INVALID TYPE! TYPE: ${type}, ONLY HOME, PRODUCT AND FILTER ARE VALID!`
);
if (modalItem) {
modalStore[modalItem] = false;
}
handleReset();
});
return {
onSubmit,
modalStore,
fields: {
calendar,
calendarAttrs,
postalCode,
postalCodeAttrs,
priceRange,
priceRangeAttrs,
dedication,
dedicationAttrs,
},
errors,
};
}

View File

@ -1,6 +1,3 @@
// This is just an example,
// so you can safely delete all default props below
export default {
failed: "Action failed",
success: "Action was successful",

4
src/i18n/es/index.js Normal file
View File

@ -0,0 +1,4 @@
export default {
failed: "Acción fallida",
success: "La acción se ha realizado correctamente",
};

View File

@ -1,5 +1,9 @@
import enUS from "./en-US";
import es from "./es";
import pt from "./pt";
export default {
"en-US": enUS,
es,
pt,
};

4
src/i18n/pt/index.js Normal file
View File

@ -0,0 +1,4 @@
export default {
failed: "Ação falhou",
success: "Ação foi um sucesso",
};

View File

@ -1,22 +1,4 @@
<template>
<q-layout>
<q-no-ssr>
<header-secondary />
</q-no-ssr>
<mobile-nav />
<q-page-container class="no-padding padding-top more">
<router-view />
</q-page-container>
<reasons-section />
<question-section />
<footer-component />
</q-layout>
</template>
<script lang="js">
<script>
import { defineComponent } from "vue";
import FooterComponent from "src/components/footer/FooterComponent.vue";
@ -46,4 +28,22 @@ export default defineComponent({
});
</script>
<template>
<q-layout>
<q-no-ssr>
<header-secondary />
</q-no-ssr>
<mobile-nav />
<q-page-container class="no-padding padding-top more">
<router-view />
</q-page-container>
<reasons-section />
<question-section />
<footer-component />
</q-layout>
</template>
<style lang="scss" scoped></style>

View File

@ -1,3 +1,32 @@
<script>
import { defineComponent } from "vue";
import { storeToRefs } from "pinia";
import FooterComponent from "src/components/footer/FooterComponent.vue";
import HeaderSecondary from "src/components/header/HeaderSecondary.vue";
import DudasSection from "src/components/sections/DudasSection.vue";
import QuestionSection from "src/components/sections/QuestionSection.vue";
import MobileNav from "src/components/ui/MobileNav.vue";
import { useMobileStore } from "src/stores/mobileNav";
export default defineComponent({
name: "ProductLayout",
components: {
HeaderSecondary,
FooterComponent,
QuestionSection,
MobileNav,
DudasSection,
},
setup() {
const mobileStore = useMobileStore();
const { isOpenNav } = storeToRefs(mobileStore);
return { isOpenNav };
},
});
</script>
<template>
<q-layout>
<q-no-ssr>
@ -17,33 +46,4 @@
</q-layout>
</template>
<script lang="js">
import { defineComponent } from 'vue';
import { storeToRefs } from 'pinia';
import FooterComponent from 'src/components/footer/FooterComponent.vue';
import HeaderSecondary from 'src/components/header/HeaderSecondary.vue';
import DudasSection from 'src/components/sections/DudasSection.vue';
import QuestionSection from 'src/components/sections/QuestionSection.vue';
import MobileNav from 'src/components/ui/MobileNav.vue';
import { useMobileStore } from 'src/stores/mobileNav';
export default defineComponent({
name: 'ProductLayout',
components: {
HeaderSecondary,
FooterComponent,
QuestionSection,
MobileNav,
DudasSection,
},
setup() {
const mobileStore = useMobileStore();
const { isOpenNav } = storeToRefs(mobileStore);
return { isOpenNav };
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,3 +1,25 @@
<script>
import { defineComponent } from "vue";
import { storeToRefs } from "pinia";
import FooterComponent from "src/components/footer/FooterComponent.vue";
import HeaderSecondary from "src/components/header/HeaderSecondary.vue";
import ReasonsSection from "src/components/sections/ReasonsSection.vue";
import MobileNav from "src/components/ui/MobileNav.vue";
import { useMobileStore } from "src/stores/mobileNav";
export default defineComponent({
name: "DefaultLayout",
components: { FooterComponent, ReasonsSection, HeaderSecondary, MobileNav },
setup() {
const mobileStore = useMobileStore();
const { isOpenNav } = storeToRefs(mobileStore);
return { isOpenNav };
},
});
</script>
<template>
<q-layout>
<q-no-ssr>
@ -15,26 +37,4 @@
</q-layout>
</template>
<script lang="js">
import { defineComponent } from 'vue';
import { storeToRefs } from 'pinia';
import FooterComponent from 'src/components/footer/FooterComponent.vue';
import HeaderSecondary from 'src/components/header/HeaderSecondary.vue';
import ReasonsSection from 'src/components/sections/ReasonsSection.vue';
import MobileNav from 'src/components/ui/MobileNav.vue';
import { useMobileStore } from 'src/stores/mobileNav';
export default defineComponent({
name: 'DefaultLayout',
components: { FooterComponent, ReasonsSection, HeaderSecondary, MobileNav },
setup() {
const mobileStore = useMobileStore();
const { isOpenNav } = storeToRefs(mobileStore);
return { isOpenNav };
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,3 +1,34 @@
<script>
import { storeToRefs } from "pinia";
import { defineComponent } from "vue";
import FooterComponent from "src/components/footer/FooterComponent.vue";
import HeaderPrimary from "src/components/header/HeaderPrimary.vue";
import InfoSection from "src/components/sections/InfoSection.vue";
import QuestionSection from "src/components/sections/QuestionSection.vue";
import ReasonsSection from "src/components/sections/ReasonsSection.vue";
import MobileNav from "src/components/ui/MobileNav.vue";
import { useMobileStore } from "src/stores/mobileNav";
export default defineComponent({
name: "HomeLayout",
components: {
HeaderPrimary,
QuestionSection,
InfoSection,
ReasonsSection,
FooterComponent,
MobileNav,
},
setup() {
const mobileStore = useMobileStore();
const { isOpenNav } = storeToRefs(mobileStore);
return { isOpenNav };
},
});
</script>
<template>
<q-layout>
<q-no-ssr>
@ -17,35 +48,4 @@
</q-layout>
</template>
<script lang="js">
import { storeToRefs } from 'pinia';
import { defineComponent } from 'vue';
import FooterComponent from 'src/components/footer/FooterComponent.vue';
import HeaderPrimary from 'src/components/header/HeaderPrimary.vue';
import InfoSection from 'src/components/sections/InfoSection.vue';
import QuestionSection from 'src/components/sections/QuestionSection.vue';
import ReasonsSection from 'src/components/sections/ReasonsSection.vue';
import MobileNav from 'src/components/ui/MobileNav.vue';
import { useMobileStore } from 'src/stores/mobileNav';
export default defineComponent({
name: 'HomeLayout',
components: {
HeaderPrimary,
QuestionSection,
InfoSection,
ReasonsSection,
FooterComponent,
MobileNav,
},
setup() {
const mobileStore = useMobileStore();
const { isOpenNav } = storeToRefs(mobileStore);
return { isOpenNav };
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,22 +1,14 @@
import { fakerES } from "@faker-js/faker";
export function mockGenerator({ length }) {
const flowersMock = Array.from({ length }, (_, i) => {
const position = fakerES.datatype.boolean() && i + 1;
const discount =
fakerES.datatype.boolean() &&
fakerES.commerce.price({ min: 5, max: 15, dec: 0 });
export const cardMock = Array.from({ length: 8 }, (_, i) => ({
const flower = {
id: i + 1,
imgSrc: `assets/flowers/flower-${i + 1}.png`,
title: fakerES.commerce.productName(),
discount: fakerES.commerce.price({ min: 5, max: 15, dec: 0 }),
isNew: fakerES.datatype.boolean(),
value: fakerES.commerce.price({ min: 20, max: 150 }),
// title: 'Nombre del producto',
// discount: i % 2 === 0 ? '10' : '',
// isNew: i % 2 === 0,
// value: '25,90',
}));
export function generateFlowers({ length }) {
const flowersMock = Array.from({ length }, (_, i) => ({
id: i + 1,
title: fakerES.commerce.productName(),
name: fakerES.commerce.productName(),
description: fakerES.commerce.productDescription(),
price: fakerES.commerce.price({
symbol: "€",
@ -24,12 +16,29 @@ export function generateFlowers({ length }) {
max: 200,
dec: 0,
}),
sku: fakerES.commerce.isbn({ separator: "", variant: 13 }),
category: fakerES.commerce.productMaterial(),
images: Array.from({ length: fakerES.number.int({ min: 2, max: 6 }) }, () =>
fakerES.image.urlPicsumPhotos()
specialPrice: fakerES.commerce.price({
symbol: "€",
min: 20,
max: 60,
dec: 0,
}),
isNew: fakerES.datatype.boolean(),
slug: fakerES.commerce.isbn({ separator: "-", variant: 13 }),
category: fakerES.number.int({ min: 1, max: 2 }),
postalCode: "12345",
dateExpired: "30/01/2024",
images: Array.from(
{ length: fakerES.number.int({ min: 2, max: 6 }) },
() => fakerES.image.urlPicsumPhotos()
),
}));
featured: fakerES.number.int({ min: 0, max: 1 }),
};
if (position) flower.position = position;
if (discount) flower.discount = discount;
return flower;
});
return flowersMock;
}

View File

@ -1,3 +1,140 @@
<script>
import { storeToRefs } from "pinia";
import { defineComponent, onBeforeMount, onUpdated, ref, watch } from "vue";
import { useRoute } from "vue-router";
import SortSelect from "src/components/@inputs/SortSelect.vue";
import IconArrowCircleFilledRight from "src/components/icons/IconArrowCircleFilledRight.vue";
import IconArrowDownWhite from "src/components/icons/IconArrowDownWhite.vue";
import IconFilter from "src/components/icons/IconFilter.vue";
import IconPencil from "src/components/icons/IconPencil.vue";
import DudasSection from "src/components/sections/DudasSection.vue";
import Card from "src/components/ui/Card.vue";
import Container from "src/components/ui/Container.vue";
import Modal from "src/components/ui/Modal.vue";
import { useCartStore } from "src/stores/cart";
import { useFormStore } from "src/stores/forms";
import { useModalStore } from "src/stores/modalStore";
export default defineComponent({
name: "CategoryPage",
components: {
IconArrowCircleFilledRight,
IconArrowDownWhite,
IconPencil,
IconFilter,
Container,
DudasSection,
Modal,
Card,
SortSelect,
},
setup() {
const route = useRoute();
const modalStore = useModalStore();
const { openModal } = modalStore;
const formStore = useFormStore();
const { availability, sortProductFilters } = storeToRefs(formStore);
const cartStore = useCartStore();
const { products } = storeToRefs(cartStore);
const { getProducts } = cartStore;
const monthTest = ref("");
const isOpenOrder = ref(false);
const monthES = {
0: "Enero",
1: "Febrero",
2: "Marzo",
3: "Abril",
4: "Mayo",
5: "Junio",
6: "Julio",
7: "Agosto",
8: "Septiembre",
9: "Octubre",
10: "Noviembre",
11: "Diciembre",
};
const orderText = {
"lowest-price": "menor precio",
"highest-price": "mayor precio",
recommended: "recomendados",
latest: "más recientes",
};
const categoryObj = {
plantas: 1,
ramos: 2,
};
watch(availability, (newDate) => {
const [_day, month, _year] = newDate.date.split("/");
monthTest.value = monthES[+month - 1];
console.log(monthTest.value);
});
watch(
[() => route.path, () => sortProductFilters.value.order],
([newPath, newOrder]) => {
const categoryPath = newPath.split("/")[2];
sortProductFilters.value.category = categoryPath;
const params = {
category: categoryObj[categoryPath],
itens: window.screen.width <= 445 ? 16 : 20,
};
const paramsObj = {
"lowest-price": () => (params.lowPrice = 1),
"highest-price": () => (params.bigPrice = 1),
latest: () => (params.isNew = 1),
// recommended: () => params.featured = 1,
};
if (newOrder) {
paramsObj[newOrder]();
}
getProducts(params);
}
);
onBeforeMount(async () => {
const categoryPath = route.path.split("/")[2];
await getProducts({
category: categoryObj[categoryPath],
itens: window.screen.width <= 445 ? 16 : 20,
});
});
onUpdated(() => {
console.groupCollapsed("%c Updated!", "color: green;");
console.log(sortProductFilters.value);
console.log(availability.value);
console.groupEnd();
});
function openOrderFilter() {
sortProductFilters.value.isOpenOrderFilter =
!sortProductFilters.value.isOpenOrderFilter;
}
return {
sortProductFilters,
openOrderFilter,
openModal,
availability,
isOpenOrder,
modalStore,
orderText,
products,
};
},
});
</script>
<template>
<q-page class="category-container">
<section class="products-section">
@ -32,7 +169,7 @@
flat
class="btn filter-btn availability"
type="button"
@click="modalStore.openModal({ modal: 'availability' })"
@click="openModal({ modal: 'availability' })"
>
<IconPencil />
</q-btn>
@ -44,7 +181,7 @@
flat
class="btn filter-item filters filter-btn"
type="button"
@click="modalStore.openModal({ modal: 'filters' })"
@click="openModal({ modal: 'filters' })"
>
<p class="filter-paragraph remove-mob">Filtros</p>
<IconFilter />
@ -80,19 +217,28 @@
</header>
<div class="products-section-body">
<Container cardContainer>
<Container cardContainer class="category-container">
<template
v-for="{
images,
discount,
isNew,
name,
price,
slug,
id,
} in products.data.products"
:key="id"
>
<Card
v-for="(
{ imgSrc, discount, isNew, title, value, id }, i
) in cardsMock"
:productValue="value"
:productName="title"
:discount="discount.toString()"
:imgSrc="imgSrc"
:price="price"
:title="name"
:discount="discount"
:imgSrc="images[0]"
:isNew="isNew"
:key="i"
:id="id"
:id="slug"
/>
</template>
</Container>
</div>
@ -104,114 +250,11 @@
</section>
<dudas-section />
<modal modalItem="isOpenAvailability" />
<modal modalItem="isOpenFilters" />
<Modal modalItem="isOpenAvailability" typeModal="availability" />
<Modal modalItem="isOpenFilters" typeModal="filter" />
</q-page>
</template>
<script>
import { fakerES } from "@faker-js/faker";
import { storeToRefs } from "pinia";
import {
defineAsyncComponent,
defineComponent,
onUpdated,
reactive,
ref,
watch,
} from "vue";
import { useRoute } from "vue-router";
import SortSelect from "src/components/@inputs/SortSelect.vue";
import IconArrowCircleFilledRight from "src/components/icons/IconArrowCircleFilledRight.vue";
import IconArrowDownWhite from "src/components/icons/IconArrowDownWhite.vue";
import IconFilter from "src/components/icons/IconFilter.vue";
import IconPencil from "src/components/icons/IconPencil.vue";
import Container from "src/components/ui/Container.vue";
import { useFormStore } from "src/stores/forms";
import { useModalStore } from "src/stores/modalStore";
export default defineComponent({
name: "CategoryPage",
components: {
IconArrowCircleFilledRight,
IconArrowDownWhite,
IconPencil,
IconFilter,
Container,
DudasSection: defineAsyncComponent(() =>
import("src/components/sections/DudasSection.vue")
),
Modal: defineAsyncComponent(() => import("src/components/ui/Modal.vue")),
Card: defineAsyncComponent(() => import("src/components/ui/Card.vue")),
SortSelect,
},
setup() {
const modalStore = useModalStore();
const formStore = useFormStore();
const { availability, sortProductFilters } = storeToRefs(formStore);
const monthES = reactive({
0: "Enero",
1: "Febrero",
2: "Marzo",
3: "Abril",
4: "Mayo",
5: "Junio",
6: "Julio",
7: "Agosto",
8: "Septiembre",
9: "Octubre",
10: "Noviembre",
11: "Diciembre",
});
const isOpenOrder = ref(false);
const route = useRoute();
const cardsMock = Array.from({ length: 8 }, (_, i) => ({
id: i + 1,
imgSrc: `../assets/flowers/flower-${i + 1}.png`,
discount: fakerES.number.int({ min: 5, max: 15 }),
isNew: fakerES.datatype.boolean(),
title: fakerES.commerce.product(),
value: fakerES.commerce.price({ min: 30, max: 100 }),
}));
const orderText = {
"lowest-price": "menor precio",
"highest-price": "mayor precio",
recommended: "recomendados",
latest: "más recientes",
};
watch(
() => route.path,
(newPatch) => {
sortProductFilters.value.category = newPatch.split("/")[2];
}
);
onUpdated(() => {
console.groupCollapsed("%c Updated!", "color: green;");
console.log(sortProductFilters.value);
console.groupEnd();
});
function openOrderFilter() {
sortProductFilters.value.isOpenOrderFilter =
!sortProductFilters.value.isOpenOrderFilter;
}
return {
sortProductFilters,
openOrderFilter,
availability,
isOpenOrder,
modalStore,
orderText,
cardsMock,
};
},
});
</script>
<style lang="scss" scoped>
.products-section {
display: flex;

View File

@ -1,26 +1,225 @@
<script>
import { toTypedSchema } from "@vee-validate/zod";
import { storeToRefs } from "pinia";
import { useForm } from "vee-validate";
import { computed, defineComponent, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import Container from "src/components/ui/Container.vue";
import { useCartStore } from "src/stores/cart";
import { useFormStore } from "src/stores/forms";
import { checkoutSchema } from "src/utils/zod/schemas/checkoutSchema";
export default defineComponent({
name: "CheckoutPage",
components: {
Container,
},
setup() {
const { push } = useRouter();
const cartStore = useCartStore();
const { cart, cartList, totalPrice, cartLength } = storeToRefs(cartStore);
if (cartLength.value === 0) return push("/");
const formStore = useFormStore();
const { handleCheckoutData } = formStore;
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
validationSchema: toTypedSchema(checkoutSchema),
initialValues: {
paymentMethod: "stripe",
terms: false,
},
});
const [name, nameAttrs] = defineField("name");
const [surname, surnameAttrs] = defineField("surname");
const [address, addressAttrs] = defineField("address");
const [postalCode, postalCodeAttrs] = defineField("postalCode");
const [phone, phoneAttrs] = defineField("phone");
const [city, cityAttrs] = defineField("city");
const [province, provinceAttrs] = defineField("province");
const [senderName, senderNameAttrs] = defineField("senderName");
const [senderCifNif, senderCifNifAttrs] = defineField("senderCifNif");
const [senderEmail, senderEmailAttrs] = defineField("senderEmail");
const [senderPhone, senderPhoneAttrs] = defineField("senderPhone");
const [senderNotes, senderNotesAttrs] = defineField("senderNotes");
const [paymentMethod, paymentMethodAttrs] = defineField("paymentMethod");
const [terms, termsAttrs] = defineField("terms");
const stepActive = reactive({ data: 1 });
const stepList = reactive({
data: [
{
value: 1,
name: "Paso 1",
description: "Datos de facturación",
active: true,
},
{
value: 2,
name: "Paso 2",
description: "Confirmación",
active: false,
},
{
value: 3,
name: "Paso 3",
description: "Pago",
active: false,
},
],
});
const checkoutBlock = ref(true);
const onSubmit = handleSubmit((values) => {
handleCheckoutData(values);
stepList.data[2].active = true;
checkoutBlock.value = false;
resetForm();
});
const handleClickStep = (value) => {
stepActive["data"] = value;
};
const stepsFormated = computed(() => {
return stepList["data"].map((step) => {
if (step.value === stepActive["data"]) {
return { ...step, active: true };
}
return step;
});
});
const provinceOptions = ref([
{ code: "01", name: "Araba/Álava" },
{ code: "02", name: "Albacete" },
{ code: "03", name: "Alicante/Alacant" },
{ code: "04", name: "Almería" },
{ code: "05", name: "Ávila" },
{ code: "06", name: "Badajoz" },
{ code: "07", name: "Balears, Illes" },
{ code: "08", name: "Barcelona" },
{ code: "09", name: "Burgos" },
{ code: "10", name: "Cáceres" },
{ code: "11", name: "Cádiz" },
{ code: "12", name: "Castellón/Castelló" },
{ code: "13", name: "Ciudad Real" },
{ code: "14", name: "Córdoba" },
{ code: "15", name: "Coruña, A" },
{ code: "16", name: "Cuenca" },
{ code: "17", name: "Girona" },
{ code: "18", name: "Granada" },
{ code: "19", name: "Guadalajara" },
{ code: "20", name: "Gipuzkoa" },
{ code: "21", name: "Huelva" },
{ code: "22", name: "Huesca" },
{ code: "23", name: "Jaén" },
{ code: "24", name: "León" },
{ code: "25", name: "Lleida" },
{ code: "26", name: "Rioja, La" },
{ code: "27", name: "Lugo" },
{ code: "28", name: "Madrid" },
{ code: "29", name: "Málaga" },
{ code: "30", name: "Murcia" },
{ code: "31", name: "Navarra" },
{ code: "32", name: "Ourense" },
{ code: "33", name: "Asturias" },
{ code: "34", name: "Palencia" },
{ code: "35", name: "Palmas, Las" },
{ code: "36", name: "Pontevedra" },
{ code: "37", name: "Salamanca" },
{ code: "38", name: "Santa Cruz de Tenerife" },
{ code: "39", name: "Cantabria" },
{ code: "40", name: "Segovia" },
{ code: "41", name: "Sevilla" },
{ code: "42", name: "Soria" },
{ code: "43", name: "Tarragona" },
{ code: "44", name: "Teruel" },
{ code: "45", name: "Toledo" },
{ code: "46", name: "Valencia/València" },
{ code: "47", name: "Valladolid" },
{ code: "48", name: "Bizkaia" },
{ code: "49", name: "Zamora" },
{ code: "50", name: "Zaragoza" },
{ code: "51", name: "Ceuta" },
{ code: "52", name: "Melilla" },
]);
return {
handleClickStep,
stepsFormated,
onSubmit,
stepList,
provinceOptions,
totalPrice,
cartList,
step: ref(1),
cart,
checkoutBlock,
meta,
errors,
name,
nameAttrs,
surname,
surnameAttrs,
address,
addressAttrs,
postalCode,
postalCodeAttrs,
phone,
phoneAttrs,
city,
cityAttrs,
province,
provinceAttrs,
senderName,
senderNameAttrs,
senderCifNif,
senderCifNifAttrs,
senderEmail,
senderEmailAttrs,
senderPhone,
senderPhoneAttrs,
senderNotes,
senderNotesAttrs,
terms,
termsAttrs,
paymentMethod,
paymentMethodAttrs,
};
},
});
</script>
<template>
<q-page class="checkout-page">
<Container tag="section">
<header class="header-title" :class="!checkoutBlock && 'success'">
<h1 class="pege-title" v-if="checkoutBlock">
¿A quién y dónde lo entregamos?
{{ checkoutBlock && "¿A quién y dónde lo entregamos?" }}
{{ !checkoutBlock && "¡Muchas gracias Jerom!" }}
</h1>
<h1 class="pege-title" v-if="!checkoutBlock">¡Muchas gracias Jerom!</h1>
<p class="pege-subtitle checkout" v-if="checkoutBlock">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
<p class="pege-subtitle checkout" v-if="!checkoutBlock">
¡Tu pedido se ha realizado con éxito! Gracias por confiar en nosotros,
en breves recibirás un correo con la confirmación de tu pedido.
<p class="page-subtitle checkout" v-if="checkoutBlock">
{{
checkoutBlock &&
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
}}
{{
!checkoutBlock &&
"¡Tu pedido se ha realizado con éxito! Gracias por confiar en nosotros, en breves recibirás un correo con la confirmación de tu pedido."
}}
</p>
</header>
<div class="checkout-container">
<div class="checkout-steps">
<div
v-for="({ active, description, name, value }, i) in stepsFormated()"
v-for="({ active, description, name, value }, i) in stepsFormated"
class="step-item-container"
:key="i"
>
@ -116,7 +315,7 @@
placeholder="Código postal*"
name="postalCode"
type="text"
mask="#####-###"
mask="#####"
v-model="postalCode"
v-bind:="postalCodeAttrs"
:error="!!errors.postalCode"
@ -135,11 +334,7 @@
:options="provinceOptions"
option-value="code"
option-label="name"
:label="
!province
? 'Complete la dirección y el código postal'
: 'Provincia*'
"
label="Provincia*"
stack-label
map-options
emit-value
@ -149,7 +344,7 @@
<div class="field-control field-select">
<q-input
placeholder="Ciudade*"
placeholder="Ciudad*"
name="city"
type="text"
v-model="city"
@ -282,11 +477,11 @@
<ul class="checkout-summary-list">
<li
class="checkout-summary-item"
v-for="({ title, price }, index) in cart"
v-for="({ title, price, quantity }, index) in cartList"
:key="index"
>
<p>
{{ title }}
{{ title }} ({{ quantity }})
<span>{{ price }}</span>
</p>
</li>
@ -309,7 +504,7 @@
</header>
<div class="checkout-payment-body">
<q-radio
<!-- <q-radio
v-model="paymentMethod"
v-bind="paymentMethodAttrs"
val="credit"
@ -321,7 +516,7 @@
<IconMaster /><IconVisa /> <IconAny /> <IconExpress />
</span>
</p>
</q-radio>
</q-radio> -->
<q-radio
v-model="paymentMethod"
@ -359,7 +554,7 @@
<div class="checkout-success-content">
<ul class="checkout-success-list">
<li
v-for="({ title, price }, index) in cart"
v-for="({ title, price, quantity }, index) in cartList"
:key="index"
class="checkout-success-item"
>
@ -370,7 +565,9 @@
alt="product"
class="checkout-product-img"
/>
<p class="checkout-product-title">{{ title }}</p>
<p class="checkout-product-title">
{{ title }} ({{ quantity }})
</p>
</div>
<p class="checkout-product-price">
@ -383,7 +580,9 @@
<footer class="checkout-success-footer">
<p class="checkout-success-paragraph">Total</p>
<p class="checkout-success-paragraph">{{ totalPrice }}.00</p>
<p class="checkout-success-paragraph">
{{ totalPrice?.toFixed(2) }}
</p>
</footer>
</div>
</div>
@ -392,230 +591,32 @@
</q-page>
</template>
<script>
import { toTypedSchema } from "@vee-validate/zod";
import { storeToRefs } from "pinia";
import { useForm } from "vee-validate";
import { defineComponent, reactive, ref } from "vue";
import IconAny from "src/components/icons/credit-flags/IconAny.vue";
import IconExpress from "src/components/icons/credit-flags/IconExpress.vue";
import IconMaster from "src/components/icons/credit-flags/IconMaster.vue";
import IconVisa from "src/components/icons/credit-flags/IconVisa.vue";
import Container from "src/components/ui/Container.vue";
import { useCartStore } from "src/stores/cart";
import { useFormStore } from "src/stores/forms";
import { checkoutSchema } from "src/utils/zod/schemas/checkoutSchema";
export default defineComponent({
name: "CheckoutPage",
components: {
Container,
IconAny,
IconVisa,
IconExpress,
IconMaster,
},
setup() {
const cartStore = useCartStore();
const { cart, totalPrice } = storeToRefs(cartStore);
const formStore = useFormStore();
const { handleCheckoutData } = formStore;
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
validationSchema: toTypedSchema(checkoutSchema),
initialValues: {
paymentMethod: "credit",
terms: false,
},
});
const [name, nameAttrs] = defineField("name");
const [surname, surnameAttrs] = defineField("surname");
const [address, addressAttrs] = defineField("address");
const [postalCode, postalCodeAttrs] = defineField("postalCode");
const [phone, phoneAttrs] = defineField("phone");
const [city, cityAttrs] = defineField("city");
const [province, provinceAttrs] = defineField("province");
const [senderName, senderNameAttrs] = defineField("senderName");
const [senderCifNif, senderCifNifAttrs] = defineField("senderCifNif");
const [senderEmail, senderEmailAttrs] = defineField("senderEmail");
const [senderPhone, senderPhoneAttrs] = defineField("senderPhone");
const [senderNotes, senderNotesAttrs] = defineField("senderNotes");
const [paymentMethod, paymentMethodAttrs] = defineField("paymentMethod");
const [terms, termsAttrs] = defineField("terms");
const stepActive = reactive({ data: 1 });
const stepList = reactive({
data: [
{
value: 1,
name: "Paso 1",
description: "Datos de facturación",
active: true,
},
{
value: 2,
name: "Paso 2",
description: "Confirmación",
active: false,
},
{
value: 3,
name: "Paso 3",
description: "Pago",
active: false,
},
],
});
const checkoutBlock = ref(true);
const onSubmit = handleSubmit((values) => {
handleCheckoutData(values);
stepList.data[2].active = true;
checkoutBlock.value = false;
resetForm();
});
const handleClickStep = (value) => {
stepActive["data"] = value;
};
const stepsFormated = () => {
return stepList["data"].map((step) => {
if (step.value === stepActive["data"]) {
return { ...step, active: true };
}
return step;
});
};
const provinceOptions = ref([
{ code: "01", name: "Araba/Álava" },
{ code: "02", name: "Albacete" },
{ code: "03", name: "Alicante/Alacant" },
{ code: "04", name: "Almería" },
{ code: "05", name: "Ávila" },
{ code: "06", name: "Badajoz" },
{ code: "07", name: "Balears, Illes" },
{ code: "08", name: "Barcelona" },
{ code: "09", name: "Burgos" },
{ code: "10", name: "Cáceres" },
{ code: "11", name: "Cádiz" },
{ code: "12", name: "Castellón/Castelló" },
{ code: "13", name: "Ciudad Real" },
{ code: "14", name: "Córdoba" },
{ code: "15", name: "Coruña, A" },
{ code: "16", name: "Cuenca" },
{ code: "17", name: "Girona" },
{ code: "18", name: "Granada" },
{ code: "19", name: "Guadalajara" },
{ code: "20", name: "Gipuzkoa" },
{ code: "21", name: "Huelva" },
{ code: "22", name: "Huesca" },
{ code: "23", name: "Jaén" },
{ code: "24", name: "León" },
{ code: "25", name: "Lleida" },
{ code: "26", name: "Rioja, La" },
{ code: "27", name: "Lugo" },
{ code: "28", name: "Madrid" },
{ code: "29", name: "Málaga" },
{ code: "30", name: "Murcia" },
{ code: "31", name: "Navarra" },
{ code: "32", name: "Ourense" },
{ code: "33", name: "Asturias" },
{ code: "34", name: "Palencia" },
{ code: "35", name: "Palmas, Las" },
{ code: "36", name: "Pontevedra" },
{ code: "37", name: "Salamanca" },
{ code: "38", name: "Santa Cruz de Tenerife" },
{ code: "39", name: "Cantabria" },
{ code: "40", name: "Segovia" },
{ code: "41", name: "Sevilla" },
{ code: "42", name: "Soria" },
{ code: "43", name: "Tarragona" },
{ code: "44", name: "Teruel" },
{ code: "45", name: "Toledo" },
{ code: "46", name: "Valencia/València" },
{ code: "47", name: "Valladolid" },
{ code: "48", name: "Bizkaia" },
{ code: "49", name: "Zamora" },
{ code: "50", name: "Zaragoza" },
{ code: "51", name: "Ceuta" },
{ code: "52", name: "Melilla" },
]);
return {
handleClickStep,
stepsFormated,
onSubmit,
stepList,
provinceOptions,
totalPrice,
step: ref(1),
cart,
checkoutBlock,
meta,
errors,
name,
nameAttrs,
surname,
surnameAttrs,
address,
addressAttrs,
postalCode,
postalCodeAttrs,
phone,
phoneAttrs,
city,
cityAttrs,
province,
provinceAttrs,
senderName,
senderNameAttrs,
senderCifNif,
senderCifNifAttrs,
senderEmail,
senderEmailAttrs,
senderPhone,
senderPhoneAttrs,
senderNotes,
senderNotesAttrs,
terms,
termsAttrs,
paymentMethod,
paymentMethodAttrs,
};
},
});
</script>
<style lang="scss" scoped>
.checkout-page {
.checkout-steps {
& .checkout-steps {
display: flex;
justify-content: center;
align-items: center;
}
.step-item-container {
& .step-item-container {
min-width: 200px;
}
.border-step {
& .border-step {
width: 90px;
height: 1px;
background-color: $primary-dark;
}
.circle-step-container {
& .circle-step-container {
display: grid;
justify-content: center;
align-items: center;
grid-template-columns: 1fr auto 1fr;
}
.circle-step {
& .circle-step {
width: 56px;
height: 56px;
border: 1px solid $primary-dark;
@ -637,7 +638,7 @@ export default defineComponent({
}
}
.step-content {
& .step-content {
display: flex;
flex-direction: column;
align-items: center;
@ -659,7 +660,7 @@ export default defineComponent({
& .checkout-content {
width: min(100%, 1144px);
margin: 50px auto 0;
margin: 50px auto calc(146px - 72px);
display: flex;
flex-wrap: wrap;
gap: 20px;
@ -670,10 +671,12 @@ export default defineComponent({
width: 100%;
margin-bottom: 21px;
border-radius: 5px;
h3 {
& h3 {
color: $text-default;
font-weight: 600;
font-size: 0.875rem;
line-height: 1.5;
line-height: 21px;
letter-spacing: 0.28px;
}
@media only screen and (max-width: $med-lg) {
@ -909,7 +912,7 @@ export default defineComponent({
}
}
.form-fields-container {
& .form-fields-container {
display: flex;
flex-wrap: wrap;
&.delivery {
@ -942,15 +945,21 @@ export default defineComponent({
label {
padding-bottom: 10px;
}
.q-field__control {
& .q-field__control {
background-color: #fff;
height: 40px;
border: 1px solid $primary-light;
input {
padding: 0px 30px;
& input {
padding: 0px 0px 0px 20px;
font-family: $font-questrial;
color: $text-default !important;
}
& select {
font-family: $font-questrial;
color: $text-default !important;
}
&.text-negative {
border-color: $negative;
}

View File

@ -1,3 +1,15 @@
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "ContactaPage",
components: {},
setup() {
return {};
},
});
</script>
<template>
<q-page class="">
<p>Contacta</p>
@ -6,16 +18,4 @@
</q-page>
</template>
<script lang="js">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'ContactaPage',
components: {},
setup() {
return {};
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,11 +1,46 @@
<script>
import { defineComponent, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
export default defineComponent({
name: "ErrorNotFound",
setup() {
const counter = ref(10);
const { push } = useRouter();
function startCountdown() {
// Cria um intervalo que executa a cada segundo
const interval = setInterval(() => {
// Decrementa o valor de count
counter.value--;
// Se o valor de count for zero, para o intervalo
if (counter.value === 1) {
clearInterval(interval);
push("/");
}
}, 1000);
}
// Chama a função para iniciar o contador quando o componente for montado
onMounted(startCountdown);
return {
counter,
};
},
});
</script>
<template>
<div
class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center"
class="not-found fullscreen 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>
<div class="text-h2" style="opacity: 0.4" :key="counter">
Redirigiendo a la home en... {{ counter }}
</div>
<q-btn
class="q-mt-xl"
@ -20,10 +55,8 @@
</div>
</template>
<script lang="js">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'ErrorNotFound',
});
</script>
<style lang="scss" scoped>
.not-found {
background-color: $primary;
}
</style>

21
src/pages/ExamplePage.vue Normal file
View File

@ -0,0 +1,21 @@
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "ExamplePage",
components: {},
setup() {
return {};
},
});
</script>
<template>
<div>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quam rerum omnis
repellat. Harum ducimus nulla repellendus neque officia eveniet corporis
odio sequi animi ut, non incidunt est error esse aperiam?
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -1,3 +1,15 @@
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "FaqPage",
components: {},
setup() {
return {};
},
});
</script>
<template>
<q-page class="">
<p>Faq</p>
@ -6,16 +18,4 @@
</q-page>
</template>
<script lang="js">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'FaqPage',
components: {},
setup() {
return {};
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,3 +1,53 @@
<script>
import { storeToRefs } from "pinia";
import { defineComponent, onBeforeMount } from "vue";
import VerticalCarouselImgs from "src/components/quasar-components/carousel/VerticalCarouselImgs.vue";
import Swiper from "src/components/swiper/Swiper.vue";
import Card from "src/components/ui/Card.vue";
import Container from "src/components/ui/Container.vue";
import { useCartStore } from "src/stores/cart";
import { useMobileStore } from "src/stores/mobileNav";
export default defineComponent({
name: "HomePage",
components: {
VerticalCarouselImgs,
Container,
Swiper,
Card,
},
setup() {
const mobileStore = useMobileStore();
const { isCarouselVisible, isOpenNav, screenWidth } =
storeToRefs(mobileStore);
const cartStore = useCartStore();
const { getProducts } = cartStore;
const { products } = storeToRefs(cartStore);
onBeforeMount(async () => {
await getProducts();
});
const slidesContent = [
"assets/1.jpg",
"assets/2.jpg",
"assets/3.jpg",
"assets/4.jpg",
"assets/5.jpg",
];
return {
isCarouselVisible,
slidesContent,
screenWidth,
isOpenNav,
products,
};
},
});
</script>
<template>
<q-page>
<q-no-ssr>
@ -19,18 +69,24 @@
<div class="products-body">
<Container cardContainer>
<Card
<template
v-for="(
{ imgSrc, discount, isNew, title, value, id }, i
) in cardMock"
:productValue="value"
:productName="title"
{ id, slug, name, price, images, isNew, discount }, i
) in products.data.products"
>
<Card
v-if="i < 8"
:id="slug"
:price="price"
:title="name"
:discount="discount"
:imgSrc="imgSrc"
:imgSrc="images[0]"
:isNew="isNew"
:key="i"
:id="id"
:key="id"
imgClass="list-products"
size="md-card"
/>
</template>
</Container>
<RouterLink class="btn rounded outlined" to="/">
@ -53,40 +109,28 @@
</header>
<div class="products-selection-body">
<!-- <HorizontalCarousel>
<SwiperSlideOne
v-for="{ id, discount, isNew, value, title, imgSrc } in cardMock"
:key="id"
>
<Card
:id="id"
:key="id"
:productValue="value"
:productName="title"
:discount="discount"
:imgSrc="imgSrc"
:isNew="isNew"
/>
</SwiperSlideOne>
</HorizontalCarousel> -->
<q-no-ssr>
<Swiper>
<swiper-slide
v-for="{ id, discount, isNew, value, title, imgSrc } in cardMock"
:key="id"
class="swiper-slide"
<template
v-for="(
{ slug, discount, isNew, price, name, images }, i
) in products.data.products"
:key="slug"
>
<swiper-slide class="swiper-slide" v-if="i < 10">
<Card
:id="id"
:key="id"
:productValue="value"
:productName="title"
:id="slug"
:key="slug"
:price="price"
:title="name"
:discount="discount"
:imgSrc="imgSrc"
:imgSrc="images[0]"
:isNew="isNew"
imgClass="carousel"
size="lg-card"
/>
</swiper-slide>
</template>
</Swiper>
</q-no-ssr>
</div>
@ -100,55 +144,6 @@
</q-page>
</template>
<script lang="js">
import { storeToRefs } from 'pinia';
import { defineAsyncComponent, defineComponent, ref } from 'vue';
import Container from 'src/components/ui/Container.vue';
import { cardMock } from 'src/mock/cards';
import { useMobileStore } from 'src/stores/mobileNav';
export default defineComponent({
name: 'HomePage',
components: {
VerticalCarouselImgs: defineAsyncComponent(
() =>
import(
'src/components/quasar-components/carousel/VerticalCarouselImgs.vue'
)
),
Swiper: defineAsyncComponent(
() => import('src/components/swiper/Swiper.vue')
),
Card: defineAsyncComponent(() => import('src/components/ui/Card.vue')),
Container,
},
setup() {
const mobileStore = useMobileStore();
const { isCarouselVisible, isOpenNav, screenWidth } =
storeToRefs(mobileStore);
const slidesContent = [
'assets/1.jpg',
'assets/2.jpg',
'assets/3.jpg',
'assets/4.jpg',
'assets/5.jpg',
];
const data = ref(null);
return {
isCarouselVisible,
slidesContent,
screenWidth,
isOpenNav,
cardMock,
data,
};
},
});
</script>
<style lang="scss" scoped>
.home-carousel {
margin-bottom: 132px;

View File

@ -1,49 +1,179 @@
<script>
import { storeToRefs } from "pinia";
import { useMeta } from "quasar";
import { useForm } from "vee-validate";
import { defineComponent, onBeforeMount, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import IconArrowCircleFilledLeft from "components/icons/IconArrowCircleFilledLeft.vue";
import IconArrowCircleFilledRight from "components/icons/IconArrowCircleFilledRight.vue";
import IconPencilGreen from "components/icons/IconPencilGreen.vue";
import IconEmail from "components/icons/social/IconEmail.vue";
import IconLinkedin from "components/icons/social/IconLinkedin.vue";
import IconShare from "components/icons/social/IconShare.vue";
import IconTwitter from "components/icons/social/IconTwitter.vue";
import IconWhatsapp from "components/icons/social/IconWhatsapp.vue";
import ProductCarousel from "components/quasar-components/carousel/ProductCarousel.vue";
import DudasSection from "components/sections/DudasSection.vue";
import Card from "components/ui/Card.vue";
import Container from "components/ui/Container.vue";
import Modal from "components/ui/Modal.vue";
import { dedicationSchema } from "src/utils/zod/schemas";
import { useCartStore } from "stores/cart";
import { useModalStore } from "stores/modalStore";
export default defineComponent({
name: "ProductPage",
components: {
IconPencilGreen,
IconWhatsapp,
IconLinkedin,
IconTwitter,
IconShare,
IconEmail,
DudasSection,
Container,
Card,
IconArrowCircleFilledRight,
IconArrowCircleFilledLeft,
ProductCarousel,
Modal,
},
setup() {
const route = useRoute();
const modalStore = useModalStore();
const { openModal } = modalStore;
const cartStore = useCartStore();
const { getProduct, getProducts, products } = cartStore;
const { prevProduct, currentProduct, nextProduct, addCartLoadingBtn } =
storeToRefs(cartStore);
onBeforeMount(() => {
getProduct(route.params.id);
getProducts();
});
watch(currentProduct.value, (newValue) => {
useMeta(() => {
return {
title: `${newValue.value?.title}`,
titleTemplate: (title) => `${title} - FloraNet`,
meta: {
description: {
name: "description",
content: `${newValue.value?.description}`,
},
keywords: { name: "keywords", content: `${newValue.value?.title}` },
equiv: {
"http-equiv": "Content-Type",
content: "text/html; charset=UTF-8",
},
ogTitle: {
property: "og:title",
template(ogTitle) {
return `${ogTitle} - FloraNet`;
},
},
noscript: {
default:
"This is content for browsers with no JS (or disabled JS)",
},
},
};
});
});
watch(
() => route.params.id,
(newId) => {
getProduct(newId);
}
);
const currentData = reactive({});
watch(currentProduct.value, (newData) => {
if (newData.value) {
const { id, ...newDataWhithoutId } = newData.value;
currentData.value = {
...newDataWhithoutId,
productId: +route.params.id,
};
}
});
const category = reactive({
1: "Planta",
2: "Ramos",
});
const { handleSubmit, defineField, handleReset } = useForm({
validationSchema: dedicationSchema,
});
const [dedication, dedicationAttrs] = defineField("dedication");
/* const onSubmit = handleSubmit(() => {
openModal({ modal: "availability" });
// addToCart(currentData.value, dedication);
// handleReset();
}); */
return {
slide: ref(1),
fullscreen: ref(false),
dedication,
dedicationAttrs,
products,
addCartLoadingBtn,
prevProduct,
currentProduct,
nextProduct,
currentData,
category,
openModal,
};
},
});
</script>
<template>
<q-page>
<!-- <p>{{ $route.params.id }}</p> -->
<Container class="product-container" tag="section">
<ProductCarousel>
<template v-for="(img, i) in currentProduct?.images" :key="i">
<q-carousel-slide
v-if="img"
:img-src="img"
class="product-gallery-item"
:name="i + 1"
/>
<q-carousel-slide
v-else
:img-src="'../assets/empty-img.jpg'"
class="product-gallery-item"
:name="1"
img-src="https://cdn.quasar.dev/img/mountains.jpg"
class="product-gallery-item"
/>
<q-carousel-slide
:name="2"
img-src="https://cdn.quasar.dev/img/parallax1.jpg"
class="product-gallery-item"
/>
<q-carousel-slide
:name="3"
img-src="https://cdn.quasar.dev/img/parallax2.jpg"
class="product-gallery-item"
/>
<q-carousel-slide
:name="4"
img-src="https://cdn.quasar.dev/img/quasar.jpg"
class="product-gallery-item"
/>
</template>
</ProductCarousel>
<div class="product-content">
<header class="product-content-header">
<h3 class="product-content-title subtitle">
{{ currentProduct.value?.title }}
<q-skeleton type="rect" v-if="!currentProduct.value?.title" />
{{ currentProduct?.name }}
<q-skeleton type="rect" v-if="!currentProduct?.name" />
</h3>
<!-- <div>{{ currentData.value }}</div> -->
<div class="product-header-block">
<p class="product-content-paragraph">
SKU:
<span class="green-text" style="display: inline-flex">
{{ currentProduct.value?.sku }}
{{ currentProduct?.slug }}
<q-skeleton
width="100px"
type="text"
v-if="!currentProduct.value?.sku"
v-if="!currentProduct?.slug"
/>
</span>
</p>
@ -51,11 +181,11 @@
<p class="product-content-paragraph">
Categoría:
<span class="green-text">
{{ currentProduct.value?.category }}
{{ category[currentProduct?.category] }}
<q-skeleton
type="text"
width="50px"
v-if="!currentProduct.value?.category"
v-if="!currentProduct?.category"
/>
</span>
</p>
@ -65,29 +195,23 @@
<div class="product-content-body">
<div class="product-content-paragraphs">
<p class="product-price green-text">
{{ currentProduct.value?.price }}
{{ currentProduct?.price }}
<q-skeleton
type="text"
height="90px"
width="80px"
v-if="!currentProduct.value?.price"
v-if="!currentProduct?.price"
/>
</p>
<p class="product-delivery green-text">Envío Gratuito</p>
<p class="product-description">
{{ currentProduct.value?.description }}
<q-skeleton
type="text"
v-if="!currentProduct.value?.description"
/>
<q-skeleton
type="text"
v-if="!currentProduct.value?.description"
/>
{{ currentProduct?.description }}
<q-skeleton type="text" v-if="!currentProduct?.description" />
<q-skeleton type="text" v-if="!currentProduct?.description" />
</p>
</div>
<form class="product-form" @submit="onSubmit">
<div class="product-form">
<div class="product-dedication">
<header class="product-dedication-header">
<IconPencilGreen />
@ -113,12 +237,12 @@
<q-btn
:loading="addCartLoadingBtn"
type="submit"
color="primary"
class="btn sm-btn"
label="AÑADIR AL CARRITO"
@click="openModal({ modal: 'availability' })"
/>
</form>
</div>
</div>
<footer class="product-content-footer">
@ -143,6 +267,7 @@
class="btn outlined rounded sm-btn product-pag-item product-prev-btn"
:to="`${+$route.params.id - 1}`"
v-if="+$route.params.id > 1"
@click="currentProduct.value = undefined"
>
<IconArrowCircleFilledLeft />
@ -161,6 +286,8 @@
color="white"
class="btn outlined rounded sm-btn product-pag-item product-next-btn"
:to="`${+$route.params.id + 1}`"
v-if="nextProduct.value?.id"
@click="currentProduct.value = undefined"
>
<div class="btn-pag-paragraphs">
<p class="btn-paragraph-top green-text">Siguiente producto</p>
@ -178,6 +305,7 @@
</Container>
<DudasSection isWhite />
<Modal modalItem="isOpenAvailability" typeModal="product" />
<Container class="like-another-container gray-bg" tag="section">
<header class="like-another-header">
@ -192,19 +320,20 @@
</p>
</header>
<Container cardContainer>
<Container cardContainer class="no-padding">
<template
v-for="({ imgSrc, discount, isNew, title, value, id }, i) in cardMock"
v-for="({ images, discount, isNew, name, price, slug }, i) in products
.data.products"
>
<card
<Card
v-if="i < 4"
:productValue="value"
:productName="title"
:discount="discount.toString()"
:imgSrc="imgSrc"
:price="price"
:title="name"
:discount="discount"
:imgSrc="images[0]"
:isNew="isNew"
:key="i"
:id="id"
:key="slug"
:id="slug"
/>
</template>
</Container>
@ -212,128 +341,6 @@
</q-page>
</template>
<script>
import { fakerES } from "@faker-js/faker";
import { useMeta } from "quasar";
import { useForm } from "vee-validate";
import { defineComponent, onBeforeMount, ref, watch } from "vue";
import { useRoute } from "vue-router";
import IconArrowCircleFilledLeft from "components/icons/IconArrowCircleFilledLeft.vue";
import IconArrowCircleFilledRight from "components/icons/IconArrowCircleFilledRight.vue";
import IconPencilGreen from "components/icons/IconPencilGreen.vue";
import IconEmail from "components/icons/social/IconEmail.vue";
import IconLinkedin from "components/icons/social/IconLinkedin.vue";
import IconShare from "components/icons/social/IconShare.vue";
import IconTwitter from "components/icons/social/IconTwitter.vue";
import IconWhatsapp from "components/icons/social/IconWhatsapp.vue";
import ProductCarousel from "components/quasar-components/carousel/ProductCarousel.vue";
import DudasSection from "components/sections/DudasSection.vue";
import Card from "components/ui/Card.vue";
import Container from "components/ui/Container.vue";
import { storeToRefs } from "pinia";
import { dedicationSchema } from "src/utils/zod/schemas";
import { useCartStore } from "stores/cart";
export default defineComponent({
name: "ProductPage",
components: {
IconPencilGreen,
IconWhatsapp,
IconLinkedin,
IconTwitter,
IconShare,
IconEmail,
DudasSection,
Container,
Card,
IconArrowCircleFilledRight,
IconArrowCircleFilledLeft,
ProductCarousel,
},
setup() {
const route = useRoute();
const cardMock = Array.from({ length: 4 }, (_, i) => ({
id: i + 1,
title: fakerES.lorem.word(),
attributes: [""],
imgSrc: `../assets/flowers/flower-${i + 1}.png`,
value: fakerES.commerce.price({ symbol: "€", min: 15, max: 200 }),
isNew: fakerES.datatype.boolean(),
discount: fakerES.number.int({ min: 5, max: 25 }),
}));
useMeta({
title: "Product",
titleTemplate: (title) => `${title} ${route.params.id} - FloraNet`,
meta: {
description: { name: "description", content: "Page 1" },
keywords: { name: "keywords", content: "Quasar website" },
equiv: {
"http-equiv": "Content-Type",
content: "text/html; charset=UTF-8",
},
ogTitle: {
property: "og:title",
template(ogTitle) {
return `${ogTitle} - FloraNet`;
},
},
noscript: {
default: "This is content for browsers with no JS (or disabled JS)",
},
},
});
const cartStore = useCartStore();
const { addToCart, getProduct } = cartStore;
const { prevProduct, currentProduct, nextProduct, addCartLoadingBtn } =
storeToRefs(cartStore);
onBeforeMount(() => {
getProduct(+route.params.id);
});
watch(
() => route.params.id,
(newId) => {
getProduct(+newId);
}
);
const currentData = ref({});
watch(currentProduct.value, (newData) => {
const { id, ...newDataWhithoutId } = newData.value;
currentData.value = { ...newDataWhithoutId, productId: +route.params.id };
});
const { handleSubmit, defineField, handleReset } = useForm({
validationSchema: dedicationSchema,
});
const [dedication, dedicationAttrs] = defineField("dedication");
const onSubmit = handleSubmit(() => {
addToCart(currentData.value, dedication);
handleReset();
});
return {
cardMock,
slide: ref(1),
fullscreen: ref(false),
dedication,
dedicationAttrs,
onSubmit,
addCartLoadingBtn,
prevProduct,
currentProduct,
nextProduct,
currentData,
};
},
});
</script>
<style lang="scss">
.product-container {
display: flex;

View File

@ -62,8 +62,14 @@ const routes = [
name: "Contacta",
component: () => import("pages/ContactaPage.vue"),
},
{
path: "/example",
name: "Example",
component: () => import("pages/ExamplePage.vue"),
},
{
path: "/:catchAll(.*)*",
name: "NotFound",
component: () => import("pages/ErrorNotFound.vue"),
},
];

View File

@ -1397,54 +1397,42 @@
],
"cart": [
{
"title": "Increible Algodón Silla",
"description": "Ergonomic executive chair upholstered in bonded black leather and PVC padded seat and back for all-day comfort and support",
"price": "€198",
"sku": "9781774532096",
"category": "Acero",
"title": "Guapa Algodón Ordenador",
"description": "Boston's most advanced compression wear technology increases muscle oxygenation, stabilizes active muscles",
"price": "€150",
"sku": "9781616149024",
"category": "Plástico",
"images": [
"https://picsum.photos/seed/jnIFY2/640/480",
"https://picsum.photos/seed/sMMlbf6zc8/640/480",
"https://picsum.photos/seed/NpRZteKf/640/480",
"https://picsum.photos/seed/cQ7Oei/640/480",
"https://picsum.photos/seed/wUVXjS1zi/640/480",
"https://picsum.photos/seed/ym8fuWY8/640/480"
"https://picsum.photos/seed/dyj4Y/640/480",
"https://picsum.photos/seed/vQWtap1t/640/480"
],
"productId": 10,
"productId": 3,
"id": 1
},
{
"title": "Increible Algodón Silla",
"description": "Ergonomic executive chair upholstered in bonded black leather and PVC padded seat and back for all-day comfort and support",
"price": "€198",
"sku": "9781774532096",
"category": "Acero",
"title": "Inteligente Acero Pantalones",
"description": "Carbonite web goalkeeper gloves are ergonomically designed to give easy fit",
"price": "€24",
"sku": "9780350182908",
"category": "Hormigon",
"images": [
"https://picsum.photos/seed/jnIFY2/640/480",
"https://picsum.photos/seed/sMMlbf6zc8/640/480",
"https://picsum.photos/seed/NpRZteKf/640/480",
"https://picsum.photos/seed/cQ7Oei/640/480",
"https://picsum.photos/seed/wUVXjS1zi/640/480",
"https://picsum.photos/seed/ym8fuWY8/640/480"
"https://picsum.photos/seed/c3QvIh5QR/640/480",
"https://picsum.photos/seed/NZWI0TRX3E/640/480"
],
"productId": 10,
"productId": 1,
"id": 2
},
{
"title": "Increible Algodón Silla",
"description": "Ergonomic executive chair upholstered in bonded black leather and PVC padded seat and back for all-day comfort and support",
"price": "€198",
"sku": "9781774532096",
"category": "Acero",
"title": "Inteligente Acero Pantalones",
"description": "Carbonite web goalkeeper gloves are ergonomically designed to give easy fit",
"price": "€24",
"sku": "9780350182908",
"category": "Hormigon",
"images": [
"https://picsum.photos/seed/jnIFY2/640/480",
"https://picsum.photos/seed/sMMlbf6zc8/640/480",
"https://picsum.photos/seed/NpRZteKf/640/480",
"https://picsum.photos/seed/cQ7Oei/640/480",
"https://picsum.photos/seed/wUVXjS1zi/640/480",
"https://picsum.photos/seed/ym8fuWY8/640/480"
"https://picsum.photos/seed/c3QvIh5QR/640/480",
"https://picsum.photos/seed/NZWI0TRX3E/640/480"
],
"productId": 10,
"productId": 1,
"id": 3
},
{
@ -1461,43 +1449,57 @@
"id": 4
},
{
"title": "Inteligente Acero Pantalones",
"description": "Carbonite web goalkeeper gloves are ergonomically designed to give easy fit",
"price": "€24",
"sku": "9780350182908",
"category": "Hormigon",
"title": "Pequeño Ladrillo Pollo",
"description": "The beautiful range of Apple Naturalé that has an exciting mix of natural ingredients. With the Goodness of 100% Natural Ingredients",
"price": "€195",
"sku": "9781027438533",
"category": "Algodón",
"images": [
"https://picsum.photos/seed/c3QvIh5QR/640/480",
"https://picsum.photos/seed/NZWI0TRX3E/640/480"
"https://picsum.photos/seed/akHdlbK3/640/480",
"https://picsum.photos/seed/KSxDr8aQqe/640/480",
"https://picsum.photos/seed/e3x6PdCNg/640/480"
],
"productId": 1,
"productId": 2,
"id": 5
},
{
"title": "Inteligente Acero Pantalones",
"description": "Carbonite web goalkeeper gloves are ergonomically designed to give easy fit",
"price": "€24",
"sku": "9780350182908",
"category": "Hormigon",
"title": "Guapa Algodón Ordenador",
"description": "Boston's most advanced compression wear technology increases muscle oxygenation, stabilizes active muscles",
"price": "€150",
"sku": "9781616149024",
"category": "Plástico",
"images": [
"https://picsum.photos/seed/c3QvIh5QR/640/480",
"https://picsum.photos/seed/NZWI0TRX3E/640/480"
"https://picsum.photos/seed/dyj4Y/640/480",
"https://picsum.photos/seed/vQWtap1t/640/480"
],
"productId": 1,
"productId": 3,
"id": 6
},
{
"title": "Inteligente Acero Pantalones",
"description": "Carbonite web goalkeeper gloves are ergonomically designed to give easy fit",
"price": "€24",
"sku": "9780350182908",
"category": "Hormigon",
"title": "Guapa Algodón Ordenador",
"description": "Boston's most advanced compression wear technology increases muscle oxygenation, stabilizes active muscles",
"price": "€150",
"sku": "9781616149024",
"category": "Plástico",
"images": [
"https://picsum.photos/seed/c3QvIh5QR/640/480",
"https://picsum.photos/seed/NZWI0TRX3E/640/480"
"https://picsum.photos/seed/dyj4Y/640/480",
"https://picsum.photos/seed/vQWtap1t/640/480"
],
"productId": 1,
"productId": 3,
"id": 7
},
{
"title": "Guapa Algodón Ordenador",
"description": "Boston's most advanced compression wear technology increases muscle oxygenation, stabilizes active muscles",
"price": "€150",
"sku": "9781616149024",
"category": "Plástico",
"images": [
"https://picsum.photos/seed/dyj4Y/640/480",
"https://picsum.photos/seed/vQWtap1t/640/480"
],
"productId": 3,
"id": 8
}
]
}

View File

@ -0,0 +1,3 @@
{
"/jsonServer/*": "/$1"
}

View File

@ -1,15 +1,30 @@
import { defineStore } from "pinia";
import { api } from "src/boot/axios";
import { computed, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { api, apiBack } from "src/boot/axios";
import { quasarNotify } from "src/functions/quasarNotify";
export const useCartStore = defineStore("cart", () => {
const cart = ref([]);
const cartList = ref([]);
const products = ref({
data: {
page: undefined,
productsPerPage: undefined,
products: [],
},
prev: {},
current: {},
next: {},
});
const dedicationTxt = ref("");
const prevProduct = reactive({});
const currentProduct = reactive({});
const currentProduct = ref();
const nextProduct = reactive({});
const addCartLoadingBtn = ref(false);
const cartLength = computed(() => cart.value.length);
const routeId = ref(null);
const totalPrice = computed(() => {
return cart.value.reduce((acc, { price }) => {
if (price) {
@ -18,6 +33,7 @@ export const useCartStore = defineStore("cart", () => {
}
}, 0);
});
const { push } = useRouter();
/**
*
@ -27,6 +43,22 @@ export const useCartStore = defineStore("cart", () => {
async function getCart({ debug }) {
try {
const { data } = await api.get("cart");
const cartItems = data.reduce((obj, { title, price, ...rest }) => {
const priceWithoutLetter = +price.replace("€", "");
if (obj[title]) {
obj[title].quantity++;
} else {
obj[title] = {
title,
price: `${priceWithoutLetter}`,
quantity: 1,
...rest,
};
}
return obj;
}, {});
cartList.value = Object.values(cartItems);
cart.value = data;
if (debug) {
@ -35,38 +67,144 @@ export const useCartStore = defineStore("cart", () => {
console.groupEnd();
}
} catch (err) {
/* throw */ new Error(`FATAL ERROR ::: ${err}`);
new Error(`FATAL ERROR ::: ${err}`);
}
}
getCart({ debug: true });
async function getProducts(
options = {
itens: undefined,
featured: undefined,
page: undefined,
category: undefined,
postalCode: undefined,
dateExpired: undefined,
minPrice: undefined,
maxPrice: undefined,
bigPrice: undefined,
lowPrice: undefined,
isNew: undefined,
},
navigate
) {
const optionsObj = {
itens: options.itens,
featured: options.featured,
page: options.page,
category: options.category,
postalCode: options.postalCode,
dateExpired: options.dateExpired,
minPrice: options.minPrice,
maxPrice: options.maxPrice,
bigPrice: options.bigPrice,
lowPrice: options.lowPrice,
isNew: options.isNew,
};
const validKeys = Object.keys(options).filter(
(key) => options[key] !== undefined
);
const params = validKeys.reduce((acc, key) => {
acc[key] = optionsObj[key];
return acc;
}, {});
try {
const {
data: { data },
} = await apiBack.get("products", {
params,
});
if (data[0].products.length === 0) {
return quasarNotify({
message:
"No hay productos disponibles para la fecha y el código postal seleccionados",
type: "erro",
});
}
products.value.data = data[0];
if (navigate) {
navigate();
}
const currentProductIndex = data.findIndex(
({ slug }) => slug === routeId.value
);
const prevProductIndex = currentProductIndex - 1;
const nextProductIndex = currentProductIndex + 1;
products.value.prev = data.data[prevProductIndex];
products.value.current = data.data[currentProductIndex];
products.value.next = data.data[nextProductIndex];
console.groupCollapsed("%c PRODUCTS FETCHED!", "color: green;");
console.groupCollapsed("%c PRODUCTS DATA", "color: tomato;");
console.table(products.value.data);
console.groupEnd();
console.groupCollapsed("%c PREV PRODUCT", "color: tomato;");
console.table(products.value.prev);
console.groupEnd();
console.groupCollapsed("%c CURRENT PRODUCT", "color: tomato;");
console.table(products.value.current);
console.groupEnd();
console.groupCollapsed("%c NEXT PRODUCT", "color: tomato;");
console.table(products.value.next);
console.groupEnd();
console.groupEnd();
} catch (err) {
new Error(`FATAL ERROR ::: ${err}`);
}
}
/**
*
* @param id Id to get product
* @returns 'id: number; title: string; description: string; price: string; sku: string; category: string; images: string[]'
*
*/
async function getProduct(id) {
if (id) {
routeId.value = id;
try {
const { data } = await api.get(`flowers/${id}`);
currentProduct.value = data;
const { data: dataNext } = await api.get(`flowers/${id + 1}`);
if (dataNext) {
nextProduct.value = dataNext;
}
/* const promises = [
api.get(`flowers/${id - 1}`),
api.get(`flowers/${id}`),
api.get(`flowers/${id + 1}`),
];
const results = await Promise.allSettled(promises);
const [prev, current, next] = results.map((res) => {
const result = {
fulfilled: res.value?.data,
rejected: res.reason,
};
console.groupCollapsed("%c Produtos recebido!", "color: green;");
if (id - 1 > 0) {
const { data: dataPrev } = await api.get(`flowers/${id - 1}`);
prevProduct.value = dataPrev;
return result[res.status];
}); */
const { data } = await apiBack.get(`products/slug/${id}`);
prevProduct.value = {};
currentProduct.value = data.data[0];
nextProduct.value = {};
console.groupCollapsed(
`%c PRODUCT FETCHED! SLUG: ${routeId.value}`,
"color: green;"
);
console.time();
console.table(prevProduct.value);
}
console.table(currentProduct.value);
console.table(nextProduct.value);
console.timeEnd();
console.groupEnd();
if (currentProduct.value.response?.status === 404) {
push({ name: "NotFound" });
}
} catch (err) {
new Error(`FATAL ERROR ::: ${err}`);
console.error(`FATAL ERROR ::: ${err}`);
}
}
}
@ -79,23 +217,25 @@ export const useCartStore = defineStore("cart", () => {
try {
await api.post("cart", product);
addCartLoadingBtn.value = false;
} catch (err) {
addCartLoadingBtn.value = false;
new Error(`FATAL ERROR ::: ${err}`);
}
// push("/checkout");
console.groupCollapsed("%c Adicionado com sucesso!", "color: green");
console.table(cart.value);
console.groupEnd();
} catch (err) {
addCartLoadingBtn.value = false;
new Error(`FATAL ERROR ::: ${err}`);
}
}
function removeFromCart(id) {
cart.value = cart.value.filter((p) => p.id !== id);
cart.value = cart.value.filter((p) => id !== p.id);
api.delete(`cart/${id}`);
}
return {
cart,
cartList,
totalPrice,
dedicationTxt,
cartLength,
@ -103,7 +243,9 @@ export const useCartStore = defineStore("cart", () => {
currentProduct,
nextProduct,
addCartLoadingBtn,
products,
getProducts,
addToCart,
removeFromCart,
getProduct,

View File

@ -50,6 +50,10 @@ export const useFormStore = defineStore("forms", {
this.availability = values;
},
registerAvailability() {
console.log(this.availability);
},
handleCheckoutData(values) {
// console.log(values);
this.checkout = values;

View File

@ -1,7 +1,8 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useLanguageStore = defineStore("language", {
state: () => ({
lang: "es",
}),
export const useLanguageStore = defineStore("language", () => {
const lang = ref("es");
return { lang };
});

View File

@ -1,21 +1,27 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useMobileStore = defineStore("mobile", {
state: () => ({
isOpenNav: false,
isCarouselVisible: false,
screenWidth: 0,
}),
export const useMobileStore = defineStore("mobile", () => {
const isOpenNav = ref(false);
const isCarouselVisible = ref(false);
const screenWidth = ref(0);
actions: {
handleOpenMobileNav() {
this.isOpenNav = !this.isOpenNav;
},
handleResize() {
this.screenWidth = screen.width;
if (this.screenWidth > 768) {
this.isOpenNav = false;
function handleOpenMobileNav() {
isOpenNav.value = !isOpenNav.value;
}
},
},
function handleResize() {
screenWidth.value = screen.width;
if (screenWidth.value > 768) {
isOpenNav.value = false;
}
}
return {
isOpenNav,
isCarouselVisible,
screenWidth,
handleOpenMobileNav,
handleResize,
};
});

View File

@ -1,26 +1,31 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { useFormStore } from "./forms";
export const useModalStore = defineStore("modal", {
state: () => ({
isOpenAvailability: false,
isOpenFilters: false,
}),
export const useModalStore = defineStore("modal", () => {
const formStore = useFormStore();
const availability = computed(() => formStore.availability);
actions: {
openModal({ modal }) {
const isOpenAvailability = ref(false);
const isOpenFilters = ref(false);
function openModal({ modal }) {
const open = {
availability: () =>
(this.isOpenAvailability = !this.isOpenAvailability),
filters: () => (this.isOpenFilters = !this.isOpenFilters),
(isOpenAvailability.value = !isOpenAvailability.value),
filters: () => (isOpenFilters.value = !isOpenFilters.value),
};
open[modal]();
},
handleSubmit({ isModalContent }) {
}
function handleSubmit({ content }) {
const isModal = {
isOpenAvailability: () => "Contenido modal availability",
isOpenFilters: () => "Contenido modal filters",
};
console.log(isModal[isModalContent]());
},
},
console.log(availability.value);
console.log(isModal[content]());
}
return { openModal, handleSubmit, isOpenAvailability, isOpenFilters };
});

View File

@ -1,20 +1,17 @@
import { defineStore } from "pinia";
import { reactive } from "vue";
export const useRangePriceStore = defineStore("range-price", {
state: () => ({
rangeValue: {
export const useRangePriceStore = defineStore("range-price", () => {
const rangeValue = reactive({
min: 0,
max: 200,
},
}),
actions: {
handlePriceRange({ min, max }) {
console.log(min, max);
this.rangeValue = {
max,
min,
};
},
},
});
function handlePriceRange({ min, max }) {
console.log({ min, max });
rangeValue.min = min;
rangeValue.max = max;
}
return { rangeValue, handlePriceRange };
});

View File

@ -1,13 +1,14 @@
import { defineStore } from "pinia";
export const useTextInputStore = defineStore("text-input", {
state: () => ({
dedication: "",
}),
export const useTextInputStore = defineStore("text-input", () => {
const dedication = ref("");
actions: {
handleDedicationSubmit() {
console.log(this.dedication);
},
},
function handleDedicationSubmit() {
console.log(dedication);
}
return {
dedication,
handleDedicationSubmit,
};
});

View File

@ -8,5 +8,5 @@ export const postalCode = z
const valWithoutHifen = val.replaceAll("-", "");
const valLength = valWithoutHifen.length;
return valLength === 8 && valLength >= 1;
}, "El código postal debe tener 8 caracteres numéricos válidos");
return valLength === 5 && valLength >= 1;
}, "El código postal debe tener 5 caracteres numéricos válidos");

View File

@ -1,8 +1,9 @@
import { z } from "zod";
import { postalCode } from "..";
import * as M from "../messages";
const availabilityObj = {
date: z.string().refine((val) => {
date: z.string({ required_error: M.requiredMessage }).refine((val) => {
const [day, month, year] = val.split("/");
const regex = /\//g;
const valWithoutSlash = val.replace(regex, "");

View File

@ -29,7 +29,7 @@ const checkoutObjVal = {
senderPhone: z.string().refine(handlePhoneVal, M.phoneMessage),
senderNotes: z.string(),
paymentMethod: z.enum(["credit", "stripe"], {
required_error: "Seleccione uno de los métodos de pago",
required_error: "Seleccione uno de los métodos de pago!",
}),
terms: z.boolean().refine((val) => {
return val === true;

View File

@ -0,0 +1,12 @@
import { z } from "zod";
const rangePriceObj = {
range: z.object({
min: z
.number()
.refine((n) => n > 0, "El valor mínimo debe ser superior a cero"),
max: z.number(),
}),
};
export const rangePriceSchema = z.object(rangePriceObj);