Creamos api en node para la conexión a base de datos y cambios que pidió pablo #4

Merged
pablone merged 8 commits from develop into master 2024-01-18 06:13:48 +00:00
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 = 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>
jsolis marked this conversation as resolved
Review

Te falta un espacio entre compra y la y "compray" → "compra y"

Te falta un espacio entre compra y la y "compray" → "compra y"
Review

Ok

Ok
<template>
<section
class="reasons-container"
@ -68,7 +94,7 @@
<li class="reason-footer-item">
<IconCheck />
<p class="reason-item-paragraph">
Simplicidad de compray pago seguro
Simplicidad de compra y pago seguro
</p>
</li>
@ -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>
jsolis marked this conversation as resolved
Review

para que utilizas este componente?

para que utilizas este componente?
Review

Lo íbamos a utilizar para el modal de las categorías

Lo íbamos a utilizar para el modal de las categorías
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: {
jsolis marked this conversation as resolved
Review

De momento descartamos el descuento

De momento descartamos el descuento
Review

Correcto, pero en próximas fases entendemos que se utilizara no?

Correcto, pero en próximas fases entendemos que se utilizara no?
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 {
jsolis marked this conversation as resolved
Review

Descartamos las traducciones

Descartamos las traducciones
Review

Como comentamos en la reunion, lo dejamos para proximas fases.

Como comentamos en la reunion, lo dejamos para proximas fases.
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);
jsolis marked this conversation as resolved Outdated

No veo donde se utiliza

No veo donde se utiliza

En el proximo PR lo tenemos

En el proximo PR lo tenemos
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();
jsolis marked this conversation as resolved
Review

Antes que comentar código bórralo git se acuerda de el

Antes que comentar código bórralo git se acuerda de el
Review

Ok

Ok
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
jsolis marked this conversation as resolved
Review

El idioma de los comentarios debe ser ingles

El idioma de los comentarios debe ser ingles
Review

Ok

Ok
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);