develop #8

Merged
jsolis merged 7 commits from develop into master 2024-03-14 11:54:32 +00:00
73 changed files with 3466 additions and 3984 deletions

11
.vscode/settings.json vendored
View File

@ -23,7 +23,6 @@
"source.fixAll.eslint", "source.fixAll.eslint",
"source.fixAll.stylelint" "source.fixAll.stylelint"
], ],
"files.exclude": { "files.exclude": {
"**/.git": true, "**/.git": true,
"**/.svn": true, "**/.svn": true,
@ -61,11 +60,6 @@
"terminal.integrated.enableImages": true, "terminal.integrated.enableImages": true,
"figma.autocompleteBlocks": true, "figma.autocompleteBlocks": true,
"figma.assetExportDirectory": "src/assets", "figma.assetExportDirectory": "src/assets",
"editor.codeActionsOnSave": [
"source.addMissingImports",
"source.organizeImports",
"source.fixAll.eslint"
],
"gitlens.gitCommands.skipConfirmations": ["fetch:command", "switch:command"], "gitlens.gitCommands.skipConfirmations": ["fetch:command", "switch:command"],
"diffEditor.ignoreTrimWhitespace": false, "diffEditor.ignoreTrimWhitespace": false,
"svg.preview.mode": "svg", "svg.preview.mode": "svg",
@ -78,8 +72,5 @@
"workbench.tree.indent": 16, "workbench.tree.indent": 16,
"window.zoomLevel": -1, "window.zoomLevel": -1,
"git.ignoreRebaseWarning": true, "git.ignoreRebaseWarning": true,
"editor.largeFileOptimizations": false, "editor.largeFileOptimizations": false
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
} }

View File

@ -2,4 +2,8 @@ HOST="127.0.0.1"
DB_USER="root" DB_USER="root"
DB_PASSWORD="root" DB_PASSWORD="root"
PORT ="3306" PORT ="3306"
DATABASE="floranet" DATABASE="floranet"
BASE_URL =http://localhost:9100
CLIENT_ID="Ab5vEddhdvdJhLUkXtTiS2pe43W6PD1JNKns7XMnlw8FvC31H2VYakyVEHvuFBi2b543QIHiPh8j4FLF"
SECRET_KEY="EAxLf05kp08cvbLgZrqjwdx-NXnhQtnP4Y0B4LHAM_7T9-HOh4RaNTirinWfTV8GR6DJWg9djry5yHfO"

View File

@ -0,0 +1,22 @@
const db = require("../../db/db");
class ContactController {
async Create(req, res) {
try {
const { name, phone, email, message } = req.body;
console.log(name, phone, email, message);
const contact = await db.contact_Request(name, phone, email, message);
return res.status(200).send({
data: contact[0]
})
} catch (error) {
console.log(error);
return res.status(422).send({
message: "error al guardar contacto"
})
}
}
}
module.exports = new ContactController();

View File

@ -0,0 +1,13 @@
const db = require("../../db/db");
class DeliveryController {
async findByPostalCode(req, res) {
const { postalCode } = req.query;
const dates = await db.deliveryDate_get(postalCode);
return res.status(200).send({
data: dates[0]
})
}
}
module.exports = new DeliveryController();

View File

@ -0,0 +1,23 @@
const db = require("../../db/db");
const paypal = require('paypal-rest-sdk');
const paymentServices = require('./payment.services')
class PaymentController {
async Create(req, res) {
return await paymentServices.Create(req, res)
}
async Success(req, res) {
return await paymentServices.Success(req, res)
}
Cancel(req, res) {
return res.status(200).send({
data: {
menssage: "cancelado"
}
})
}
}
module.exports = new PaymentController();

View File

@ -0,0 +1,113 @@
const db = require("../../db/db");
const payPalProviders = require('./paypal.providers')
const redsysProviders = require('./redsys.providers')
class PaymentServices {
async Create(req, res) {
try {
//parâmetros para retornar os produtos que serão comprados
const { products, dateExpired, postalCode, customer, type } = req.body
const _products = await db.getProducts(dateExpired, postalCode)
const productsFilter = _products[0].filter((item) => {
if (products.includes(item.id)) {
return item
}
});
if (productsFilter.length !== products.length) {
return res.status(422).send({
data: {
message: "Uno de los productos no existe."
}
})
}
let priceIntial = 0
let price = productsFilter.reduce((accumulator, curValue) => accumulator + Number(curValue.price), priceIntial)
let productsIds = ''
for (let i = 0; i < products.length; i++) {
productsIds += `${products[i]}${i === products.length - 1 ? '' : '-'}`
}
//Create new order
const jsonOrderData = JSON.stringify({
"customer": {
customerData: {
...customer.customerData,
type: type,
products: productsFilter
}
},
})
const order = await db.orderData_put(jsonOrderData);
const orderFk = order[0][0].orderFk
if (type === "paypal") {
const data = await payPalProviders.New(orderFk, price)
return res.status(200).send({
data: { ...data, orderId: orderFk }
})
}
if (type === "redsys") {
const data = await redsysProviders.New(orderFk, price)
return res.status(200).send({
data: { ...data, orderId: orderFk }
})
}
/* if (newOrder) {
return res.status(200).send({
data: { link: newOrder.links, orderId: orderFk }
})
} */
} catch (error) {
console.log(error);
return res.status(422).send({
data: {
message: "Error al iniciar el pago"
}
})
}
}
async Success(req, res) {
try {
//Parameters payment
const { paymentId, PayerID, orderId } = req.body
const payerId = { 'payer_id': PayerID };
//API validation payent and confirnm order
paypal.payment.execute(paymentId, payerId, async function (error, payment) {
if (error) {
return res.status(422).send({
data: {
message: "payment not successful"
}
})
} else {
if (payment.state == 'approved') {
await db.order_confirm(orderId)
return res.status(200).send({
data: {
id: payment.id,
message: "payment completed successfully",
}
})
} else {
return res.status(422).send({
data: {
message: "payment not successful"
}
})
}
}
});
} catch (error) {
throw error;
}
}
}
module.exports = new PaymentServices();

View File

@ -0,0 +1,59 @@
const paypal = require('paypal-rest-sdk');
class PayPalProviders {
async New(orderFk, price) {
try {
const payReq = JSON.stringify({
'intent': 'sale',
'redirect_urls': {
'return_url': `${process.env.BASE_URL}/checkout/success?orderId=${orderFk}`,
'cancel_url': `${process.env.BASE_URL}/checkout/error`
},
'payer': {
'payment_method': 'paypal'
},
'transactions': [{
'amount': {
'total': 0.0000000001,
'currency': 'EUR'
},
'description': 'This is the payment transaction description.'
}]
});
//Starting checkout process and returning sandbox url
const newOrder = await new Promise(async (resolve, reject) => {
paypal.payment.create(payReq, function (error, payment) {
if (error) {
reject(error)
} else {
//capture HATEOAS links
var links = {};
payment.links.forEach(function (linkObj) {
links[linkObj.rel] = {
'href': linkObj.href,
'method': linkObj.method
};
})
//if redirect url present, redirect user
if (links.hasOwnProperty('approval_url')) {
resolve(
{
id: payment.id,
link: links['approval_url'].href,
}
)
} else {
console.error('no redirect URI present');
}
}
});
}).then(res => res)
return newOrder
} catch (error) {
throw error;
}
}
}
module.exports = new PayPalProviders();

View File

@ -0,0 +1,39 @@
const Redsys = require('redsys-easy');
const {
SANDBOX_URLS,
PRODUCTION_URLS,
TRANSACTION_TYPES
} = Redsys
class RedsysProviders {
async New(orderFk, price) {
console.log("Chama");
try {
const redsys = new Redsys({
secretKey: 'sq7HjrUOBfKmC576ILgskD5srU870gJ7',
urls: SANDBOX_URLS, // Also PRODUCTION_URLS
});
const obj = {
amount: price,
currency: 'EUR',
order: orderFk,
merchantName: 'Floraner',
merchantCode: '999008881',
transactionType: TRANSACTION_TYPES.AUTHORIZATION, // '0'
terminal: '001',
merchantURL: `${process.env.BASE_URL}/payments/redsys/notification`,
successURL: `${process.env.BASE_URL}/checkout/success?orderId=${orderFk}`,
errorURL: `${process.env.BASE_URL}/checkout/error`
}
const form = redsys.redirectPetition(obj)
console.log(form);
return true
} catch (error) {
throw error;
}
}
}
module.exports = new RedsysProviders();

View File

@ -0,0 +1,34 @@
const RedsysPos = require('redsys-pos');
const {
CURRENCIES, TRANSACTION_TYPES
} = RedsysPos;
class RedsysProviders {
async New(orderFk, price) {
console.log("Chama");
try {
const MERCHANT_KEY = "sq7HjrUOBfKmC576ILgskD5srU870gJ7";
const redsys = new RedsysPos(MERCHANT_KEY);
const obj = JSON.stringify({
amount: 100, // 100 euros
orderReference: orderFk,
merchantName: "Floranet",
merchantCode: "999008881",
currency: CURRENCIES.EUR,
transactionType: TRANSACTION_TYPES.AUTHORIZATION, // '0'
terminal: "001",
merchantURL: `${process.env.BASE_URL}/payments/redsys/notification`,
successURL: `${process.env.BASE_URL}/checkout/success?orderId=${orderFk}`,
errorURL: `${process.env.BASE_URL}/checkout/error`
});
console.log(obj);
const result = redsys.makePaymentParameters(obj);
return ""
} catch (error) {
throw error;
}
}
}
module.exports = new RedsysProviders();

View File

@ -0,0 +1,33 @@
const RedsysPos = require('redsys-pos');
const {
CURRENCIES, TRANSACTION_TYPES
} = RedsysPos;
class RedsysProviders {
async New(orderFk, price) {
try {
const MERCHANT_KEY = "sq7HjrUOBfKmC576ILgskD5srU870gJ7";
const redsys = new RedsysPos(MERCHANT_KEY);
const obj = {
amount: String(price),
orderReference: String(orderFk),
merchantName: "Floranet",
merchantCode: "999008881",
currency: CURRENCIES.EUR,
transactionType: TRANSACTION_TYPES.AUTHORIZATION, // '0'
terminal: "001",
merchantURL: `${process.env.BASE_URL}/payments/redsys/notification`,
successURL: `${process.env.BASE_URL}/checkout/success?orderId=${orderFk}`,
errorURL: `${process.env.BASE_URL}/checkout/error`
};
const result = redsys.makePaymentParameters(obj);
console.log(result);
return result
} catch (error) {
throw error;
}
}
}
module.exports = new RedsysProviders();

View File

@ -0,0 +1,108 @@
const db = require("../../db/db");
class ProductController {
async findAll(req, res) {
const params = req.query;
const _products = await db.getProducts(params.dateExpired, params.postalCode);
let productsFilter = _products[0]
if (Number(params.recommend)) {
productsFilter = productsFilter.filter(item => item.recommend == params.recommend)
}
if (params.type) {
productsFilter = productsFilter.filter(item => item.type === params.type)
}
if (params.minPrice && !params.maxPrice) {
productsFilter = productsFilter.filter(item => {
const price = Number(item.price)
if (price >= Number(params.minPrice)) {
return item
}
})
}
if (params.maxPrice && !params.minPrice) {
productsFilter = productsFilter.filter(item => {
const price = Number(item.price)
if (price <= Number(params.maxPrice)) {
return item
}
})
}
if (params.maxPrice && params.minPrice) {
productsFilter = productsFilter.filter(item => {
const price = Number(item.price)
if (price >= Number(params.minPrice) && price <= Number(params.maxPrice)) {
return item
}
})
}
if (Number(params.bigPrice)) {
productsFilter.sort((a, b) => {
const itemA = Number(a.price)
const itemB = Number(b.price)
return itemB - itemA;
})
}
if (Number(params.lowPrice)) {
productsFilter.sort((a, b) => {
const itemA = Number(a.price)
const itemB = Number(b.price)
return itemA - itemB;
})
}
if (Number(params.order_descending)) {
productsFilter.sort((a, b) => {
const itemA = a.order_position
const itemB = b.order_position
return itemB - itemA;
})
}
if (Number(params.order_crescent)) {
productsFilter.sort((a, b) => {
const itemA = a.order_position
const itemB = b.order_position
return itemA - itemB;
})
}
if (Number(params.isNew)) {
productsFilter = productsFilter.filter(item => item.isNew == Number(params.isNew))
}
/* 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: productsFilter
})
}
async findById(req, res) {
const id = Number(req.params.id)
const _products = await db.getProducts();
const filterProduct = _products[0].filter(item => item.id === id)
return res.status(200).send({
data: filterProduct
})
}
}
module.exports = new ProductController();

View File

@ -0,0 +1,23 @@
const db = require("../../db/db");
class ProvincesController {
async findAll(req, res) {
const params = req.query;
const tmpProvinces = await db.getProvinces();
let provinces = [];
tmpProvinces.forEach(element => {
provinces = [...provinces,{
code: element.id,
name: element.name
}];
})
return res.status(200).send({
data: provinces
})
}
}
module.exports = new ProvincesController();

View File

@ -1,34 +1,23 @@
const db = require("../db/db"); const db = require("../../db/db");
<<<<<<<< HEAD:api/controller/product/product.controller.js
const productsJson = require("./products.json") const productsJson = require("./products.json")
========
>>>>>>>> master:api/controller/Product/product.controller.js
class ProductController { class ProductController {
async findAll(req, res) { async findAll(req, res) {
const params = req.query; const params = req.query;
const _products = await db.getProducts(params.dateExpired, params.postalCode); const _products = await db.getProducts(params.dateExpired, params.postalCode);
let productsFilter = _products[0]; let productsFilter = _products[0]
if (Number(params.recommend)) { if (Number(params.recommend)) {
productsFilter = productsFilter.filter(item => item.recommend == Number(params.recommend)) productsFilter = productsFilter.filter(item => item.recommend == params.recommend)
} }
if (params.type) { if (params.type) {
productsFilter = productsFilter.filter(item => item.type === params.type) productsFilter = productsFilter.filter(item => item.type === params.type)
} }
/*if (params.postalCode) {
productsFilter = productsFilter.filter(item => item.postalCode === params.postalCode)
}
if (params.dateExpired) {
const dateSearch = new Date(params.dateExpired);
productsFilter = productsFilter.filter(item => {
const dateProduct = new Date(item.dateExpired);
if (dateProduct >= dateSearch) {
return item
}
})
}*/
console.log(productsFilter.length);
if (params.minPrice && !params.maxPrice) { if (params.minPrice && !params.maxPrice) {
productsFilter = productsFilter.filter(item => { productsFilter = productsFilter.filter(item => {
@ -89,7 +78,6 @@ class ProductController {
} }
if (Number(params.isNew)) { if (Number(params.isNew)) {
console.log(params.isNew);
productsFilter = productsFilter.filter(item => item.isNew == Number(params.isNew)) productsFilter = productsFilter.filter(item => item.isNew == Number(params.isNew))
} }
@ -105,6 +93,7 @@ class ProductController {
products: products products: products
}) */ }) */
return res.status(200).send({ return res.status(200).send({
data: productsFilter data: productsFilter
}) })

File diff suppressed because it is too large Load Diff

View File

@ -12,17 +12,65 @@ async function connect() {
const mysql = require("mysql2/promise"); const mysql = require("mysql2/promise");
const connection = await mysql.createConnection("mysql://" + user + ":" + password + "@" + host + ":" + port + "/" + database + ""); const connection = await mysql.createConnection("mysql://" + user + ":" + password + "@" + host + ":" + port + "/" + database + "");
console.log("Connected to MySQL!");
global.connection = connection; global.connection = connection;
return connection; return connection;
} }
//Procedure for get products
async function getProducts(dateExpired, postalCode) { async function getProducts(dateExpired, postalCode) {
console.log("Query in table MySQL!");
const conn = await connect(); const conn = await connect();
const [rows] = await conn.query(`CALL catalogue_get("${dateExpired}", "${postalCode}")`); const [rows] = await conn.query(`CALL catalogue_get("${dateExpired}", "${postalCode}")`);
return rows; return rows;
} }
//Procedure for create transactions, do not carry out any manipulation at the bank
async function orderData_put(jsonOrderData) {
const conn = await connect();
const [rows] = await conn.query(`CALL orderData_put(?)`, [jsonOrderData], (err, results) => {
if (err) {
console.error(err);
} else {
console.log('Result:', results);
}
});
return rows;
}
async function order_confirm(orderFk) {
const conn = await connect();
const [rows] = await conn.query(`CALL order_confirm(${orderFk})`);
return rows;
}
module.exports = { getProducts }
//Procedure for get transactions, do not carry out any manipulation at the bank
async function orderData_get() {
const conn = await connect();
const [rows] = await conn.query(`CALL orderData_get()`);
return rows;
}
async function getProvinces() {
const conn = await connect();
const [rows] = await conn.query(`SELECT p.id, p.name, c.code, c.country
FROM vn.province p
JOIN vn.country c ON c.id = p.countryFk
WHERE c.country IN('España', 'Francia', 'Portugal')`);
return rows;
}
async function deliveryDate_get(postalCode) {
const conn = await connect();
const [rows] = await conn.query(`CALL deliveryDate_get("${postalCode}")`);
return rows;
}
async function contact_Request(name, phone, email, message) {
const conn = await connect();
const [rows] = await conn.query(`CALL contact_Request("${name}", "${phone}", "${email}", "${message}")`);
return rows;
}
module.exports = { getProducts, orderData_get, orderData_put, getProvinces, deliveryDate_get, contact_Request, order_confirm }

View File

@ -1,18 +1,37 @@
const cors = require('cors'); const cors = require('cors');
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const productController = require('./controller/product.controller'); const paypal = require('paypal-rest-sdk');
const productController = require('./controller/Product/product.controller');
const paymengtController = require('./controller/Payment/payment.controller');
const provincesController = require('./controller/Provinces/provinces.controller');
const deliveryController = require('./controller/Delivery/delivery.controller');
const contactController = require('./controller/Contact/contact.controller');
paypal.configure({
'mode': 'sandbox',
'client_id': process.env.CLIENT_ID,
'client_secret': process.env.SECRET_KEY
});
const app = express(); const app = express();
const port = 9999; const port = 9999;
const allowedOrigins = ['http://localhost:9100', 'https://floranet.onecommerce.dev/']; const allowedOrigins = ['http://localhost:9100', 'https://floranet.onecommerce.dev','http://49.13.85.117','http://floranet.onecommerce.dev'];
const corsOptions = { const corsOptions = {
origin: allowedOrigins, origin: allowedOrigins,
optionsSuccessStatus: 200, optionsSuccessStatus: 200,
}; };
app.use(cors(corsOptions)); app.use(cors(corsOptions));
app.use(express.json());
app.use(
express.urlencoded({
extended: true,
}),
);
app.get('/', (req, res) => { app.get('/', (req, res) => {
const indexPath = path.join(__dirname, './', 'index.html'); const indexPath = path.join(__dirname, './', 'index.html');
res.sendFile(indexPath); res.sendFile(indexPath);
@ -21,7 +40,15 @@ app.get('/', (req, res) => {
//Products //Products
app.get('/api/products', productController.findAll); app.get('/api/products', productController.findAll);
app.get('/api/products/:id', productController.findById); app.get('/api/products/:id', productController.findById);
app.post('/api/payment/', paymengtController.Create)
app.post('/api/payment/success', paymengtController.Success)
app.get('/api/payment/cancel', paymengtController.Cancel)
app.get('/api/provinces', provincesController.findAll)
app.get('/api/delivery/dates', deliveryController.findByPostalCode)
app.post('/api/contact/save', contactController.Create)
app.listen(port, () => { app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`); console.log(`Server listening at http://localhost:${port}`);
}); });

View File

@ -8,8 +8,5 @@
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC"
"dependencies": {
"express": "^4.18.2"
}
} }

View File

@ -39,6 +39,10 @@
<link rel="icon" type="image/ico" href="icons/floranet-favicon.jpg" /> <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.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://unpkg.com/vue-tel-input/dist/vue-tel-input.css"
/>
<link <link
href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Mulish:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;0,1000;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900;1,1000&family=Questrial&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Mulish:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;0,1000;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900;1,1000&family=Questrial&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet" rel="stylesheet"

1103
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "floranet", "name": "floranet",
"version": "0.0.1", "version": "0.0.1",
"description": "A floranet app", "description": "A floranet app",
"productName": "Floranet App", "productName": "Floranet",
"author": "user", "author": "user",
"private": true, "private": true,
"scripts": { "scripts": {
@ -16,20 +16,29 @@
"backend": "json-server -p 3000 -d 600 -w src/services/json-server/db.json --routes src/services/json-server/routes.json" "backend": "json-server -p 3000 -d 600 -w src/services/json-server/db.json --routes src/services/json-server/routes.json"
}, },
"dependencies": { "dependencies": {
"@floating-ui/vue": "^1.0.6",
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"@vee-validate/zod": "^4.12.2", "@vee-validate/zod": "^4.12.2",
"@vue-stripe/vue-stripe": "^4.5.0", "@vue-stripe/vue-stripe": "^4.5.0",
"@vueuse/core": "^10.7.0", "@vueuse/core": "^10.7.0",
"axios": "^1.2.1", "axios": "^1.2.1",
"express": "^4.18.2", "express": "^4.18.2",
"fs": "^0.0.1-security",
"mysql2": "^3.7.0", "mysql2": "^3.7.0",
"node-redsys-api": "^0.0.5",
"paypal-rest-sdk": "^1.8.1",
"pinia": "^2.0.11", "pinia": "^2.0.11",
"quasar": "^2.6.0", "quasar": "^2.6.0",
"redsys-easy": "^5.2.3",
"redsys-pay": "^1.2.0",
"redsys-pos": "^1.0.2",
"vee-validate": "^4.12.2", "vee-validate": "^4.12.2",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-country-flag-next": "^2.3.2",
"vue-i18n": "^9.0.0", "vue-i18n": "^9.0.0",
"vue-image-zoomer": "^2.2.3", "vue-image-zoomer": "^2.2.3",
"vue-router": "^4.0.0", "vue-router": "^4.0.0",
"vue-tel-input": "^8.3.1",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {

View File

@ -11,7 +11,7 @@
const { configure } = require("quasar/wrappers"); const { configure } = require("quasar/wrappers");
const path = require("path"); const path = require("path");
module.exports = configure(function (/* ctx */) { module.exports = configure(function (ctx) {
return { return {
eslint: { eslint: {
// fix: true, // fix: true,
@ -28,7 +28,15 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot) // app boot file (/src/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files // https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: ["i18n", "axios" /* , { path: "stripe", server: false } */], boot: [
"i18n",
"axios",
{
path: "VueCountryFlag",
server: false,
},
{ path: "VueTelInput", server: false },
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ["app.scss"], css: ["app.scss"],
@ -49,6 +57,12 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: { build: {
env: {
API: ctx.dev
? "http://localhost:9999/api/"
: "https://floranet-back.onecommerce.dev/api/",
},
target: { target: {
browser: ["es2019", "edge88", "firefox78", "chrome87", "safari13.1"], browser: ["es2019", "edge88", "firefox78", "chrome87", "safari13.1"],
node: "node16", node: "node16",

View File

@ -0,0 +1,6 @@
import { boot } from "quasar/wrappers";
import CountryFlag from "vue-country-flag-next";
export default boot(({ app }) => {
app.use(CountryFlag);
});

16
src/boot/VueTelInput.js Normal file
View File

@ -0,0 +1,16 @@
import { boot } from "quasar/wrappers";
import VueTelInput from "vue-tel-input";
export default boot(({ app }) => {
const options = {
mode: "auto",
dropdownOptions: {
showDialCodeInSelection: true,
showFlags: true,
showDialCodeInList: true,
},
autoFormat: true,
};
app.use(VueTelInput, options);
});

View File

@ -7,8 +7,8 @@ import { boot } from "quasar/wrappers";
// good idea to move this instance creation inside of the // good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually // "export default () => {}" function below (which runs individually
// for each client) // for each client)
const api = axios.create({ baseURL: "http://localhost:3000/jsonServer/" });
const apiBack = axios.create({ baseURL: "http://localhost:9999/api/" }); const apiBack = axios.create({ baseURL: process.env.API });
export default boot(({ app }) => { export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api // for use inside Vue files (Options API) through this.$axios and this.$api
@ -17,10 +17,9 @@ export default boot(({ app }) => {
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form) // ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file // so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api;
app.config.globalProperties.$apiBack = apiBack; app.config.globalProperties.$apiBack = apiBack;
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form) // ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API // so you can easily perform requests against your app's API
}); });
export { api, apiBack }; export { apiBack };

View File

@ -1,45 +1,65 @@
<script> <script>
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { defineComponent, onBeforeMount, ref, watch } from "vue";
import { fullCurrentDate } from "src/constants/date"; import { fullCurrentDate } from "src/constants/date";
import { invertDate } from "src/functions/invertDate";
import { useFormStore } from "src/stores/forms"; import { useFormStore } from "src/stores/forms";
import { defineComponent, ref } from "vue";
import IconCalendar from "../icons/IconCalendar.vue"; import IconCalendar from "../icons/IconCalendar.vue";
export default defineComponent({ export default defineComponent({
name: "calendar-input", name: "calendar-input",
components: { IconCalendar }, components: { IconCalendar },
inheritAttrs: true, props: ["modelValue", "bindValue", "setValues"],
props: { setup({ setValues }, { emit }) {
setValues: {
type: Function,
default: () => {},
},
},
setup({ setValues }) {
const formStore = useFormStore(); const formStore = useFormStore();
const { availability } = storeToRefs(formStore); const { availability, postalCodeValid } = storeToRefs(formStore);
const proxyDate = ref(fullCurrentDate); const [year, month, day] = fullCurrentDate.replaceAll("/", "-").split("-");
const currentDate = `${day}-${month}-${year}`;
const proxyDate = ref(invertDate(currentDate));
function updateProxy() { function updateProxy() {
proxyDate.value = fullCurrentDate; proxyDate.value = invertDate(currentDate);
} }
function optionsValidDates(date) { function optionsValidDates(date) {
return date >= fullCurrentDate; return date >= fullCurrentDate;
} }
function save() { function updateModel(value) {
availability.value.date = proxyDate.value; emit("update:modelValue", value);
setValues({ date: proxyDate.value });
} }
onBeforeMount(() => {
setValues({ date: invertDate(proxyDate.value) });
});
watch(proxyDate, (newProxy) => {
setValues({ date: invertDate(newProxy) });
});
const LOCALE = {
days: "Domingo_Lunes_Martes_Miércoles_Jueves_Viernes_Sábado".split("_"),
daysShort: "Dom_Lun_Mar_Mié_Jue_Vie_Sáb".split("_"),
months:
"Enero_Febrero_Marzo_Abril_Mayo_Junio_Julio_Agosto_Septiembre_Octubre_Noviembre_Diciembre".split(
"_"
),
monthsShort: "Ene_Feb_Mar_Abr_May_Jun_Jul_Ago_Sep_Oct_Nov_Dic".split("_"),
firstDayOfWeek: 1,
format24h: false,
pluralDay: "dias",
};
return { return {
availability, availability,
postalCodeValid,
proxyDate, proxyDate,
LOCALE,
updateProxy, updateProxy,
optionsValidDates, optionsValidDates,
save, updateModel,
}; };
}, },
}); });
@ -58,19 +78,14 @@ export default defineComponent({
> >
<q-date <q-date
v-model="proxyDate" v-model="proxyDate"
v-bind="calendarAttrs" :options="postalCodeValid.dataOptions"
:options="optionsValidDates" :locale="LOCALE"
mask="DD-MM-YYYY" :readonly="!postalCodeValid.isValid"
mask="YYYY-MM-DD"
> >
<div class="row items-center justify-end q-gutter-sm"> <div class="row items-center justify-end q-gutter-sm">
<q-btn label="Cancel" color="primary" flat v-close-popup /> <q-btn label="Cancel" color="primary" flat v-close-popup />
<q-btn <q-btn label="OK" color="primary" flat v-close-popup />
label="OK"
color="primary"
flat
@click="save"
v-close-popup
/>
</div> </div>
</q-date> </q-date>
</q-popup-proxy> </q-popup-proxy>
@ -79,7 +94,18 @@ export default defineComponent({
<div class="custom-block-content"> <div class="custom-block-content">
<p class="custom-head-paragraph">¿Cuándo?</p> <p class="custom-head-paragraph">¿Cuándo?</p>
<slot></slot> <q-input
:model-value="modelValue"
@update:model-value="updateModel"
v-bind="bindValue"
class="custom-date-input"
:error="false"
placeholder="Elige una fecha"
mask="##/##/####"
borderless
dense
:disable="!postalCodeValid.isValid"
/>
</div> </div>
</div> </div>
</template> </template>

View File

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

@ -1,62 +0,0 @@
<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,47 +1,75 @@
<script> <script>
import { toTypedSchema } from "@vee-validate/zod";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useForm } from "vee-validate"; import { defineComponent, ref } from "vue";
import { defineComponent, watch } from "vue";
import { apiBack } from "src/boot/axios";
import { quasarNotify } from "src/functions/quasarNotify"; import { quasarNotify } from "src/functions/quasarNotify";
import { useFormStore } from "src/stores/forms"; import { useFormStore } from "src/stores/forms";
import { availabilitySchema } from "src/utils/zod/schemas/availabilitySchema"; import * as M from "src/utils/zod/messages";
import IconPostalCode from "../icons/IconPostalCode.vue"; import IconPostalCode from "../icons/IconPostalCode.vue";
export default defineComponent({ export default defineComponent({
name: "postal-code", name: "postal-code",
components: { IconPostalCode }, components: { IconPostalCode /* IconInfo, */ /* IconSearch */ },
setup() { props: ["modelValue", "bindValue", "setFieldError"],
setup({ setFieldError, modelValue }, { emit }) {
const formStore = useFormStore(); const formStore = useFormStore();
const { availability } = storeToRefs(formStore); const { postalCodeValid } = storeToRefs(formStore);
const validationSchema = toTypedSchema(
availabilitySchema.pick({ postalCode: true }).partial()
);
const { errors, defineField, values } = useForm({
validationSchema,
initialValues: {
postalCode: availability.value.postalCode,
},
});
const [postalCode, postalCodeAttrs] = defineField("postalCode");
const onBlur = () => {
availability.value.postalCode = postalCode.value;
};
availability.value.postalCode = values.postalCode;
watch(errors, (newErrors) => { const postalCodeInput = ref(modelValue);
if (newErrors.postalCode) {
quasarNotify({ message: newErrors.postalCode, type: "erro" }); function updateModel(value) {
emit("update:modelValue", value);
postalCodeInput.value = value;
}
const isPostalCodeLoading = ref(false);
async function onBlur() {
try {
if (postalCodeInput.value.length < 5) {
quasarNotify({
type: "info",
message: `${M.fiveLength}, Cantidad de caracteres: ${postalCodeInput.value.length}`,
});
setFieldError("postalCode", M.fiveLength);
return;
}
isPostalCodeLoading.value = true;
//TODO - Promesa consultando la api para ver las fechas existentes
const {
data: { data },
} = await apiBack.get(`/delivery/dates`, {
params: { postalCode: postalCodeInput.value },
});
const dates = data.map(({ dated }) => {
const getDate = new Date(dated);
const day = getDate.getDate().toString().padStart(2, "0");
const month = (getDate.getMonth() + 1).toString().padStart(2, "0");
const year = getDate.getFullYear();
const formattedDate = `${year}/${month}/${day}`;
return formattedDate;
});
postalCodeValid.value.dataOptions = dates;
postalCodeValid.value.isValid = true;
isPostalCodeLoading.value = false;
} catch (error) {
quasarNotify({
type: "erro",
message:
"Se ha producido un error en el proceso de identificación del código postal",
});
isPostalCodeLoading.value = false;
console.error(`FATAL ERROR ::: ${error}`);
} finally {
// console.log("click");
} }
}); }
return { return { updateModel, onBlur, isPostalCodeLoading };
postalCode,
postalCodeAttrs,
errors,
onBlur,
};
}, },
}); });
</script> </script>
@ -51,20 +79,35 @@ export default defineComponent({
<IconPostalCode /> <IconPostalCode />
<div class="custom-block-content"> <div class="custom-block-content">
<p class="custom-head-paragraph">¿Dónde?</p> <p class="custom-head-paragraph">
¿Dónde?
<!-- <IconInfo /> -->
</p>
<slot></slot> <q-input
<!-- <q-input :model-value="modelValue"
borderless @update:model-value="updateModel"
v-bind="bindValue"
class="custom-main-paragraph" class="custom-main-paragraph"
v-model="postalCode" :error="false"
v-bind="postalCodeAttrs"
:error="!!errors.postalCode"
placeholder="código postal" placeholder="código postal"
mask="#####" mask="#####"
@blur="onBlur" borderless
dense dense
/> --> @blur="onBlur"
/>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.custom-input-el .custom-block-content .search-btn {
padding: 8px;
& .q-btn__content {
& svg {
width: 14px;
height: 14px;
}
}
}
</style>

View File

@ -11,16 +11,36 @@ export default defineComponent({
default: 0, default: 0,
required: true, required: true,
}, },
minDefault: {
type: Number,
default: 0,
required: true,
},
max: { max: {
type: Number, type: Number,
default: 200, default: 200,
required: true, required: true,
}, },
maxDefault: {
type: Number,
default: 200,
required: true,
},
modelValue: {
type: String,
},
bindValue: {
type: Object,
},
}, },
setup() { setup(props, { emit }) {
const rangePriceStore = useRangePriceStore(); const rangePriceStore = useRangePriceStore();
return { rangePriceStore }; const updateModel = (value) => {
emit("update:modelValue", value);
};
return { rangePriceStore, updateModel };
}, },
}); });
</script> </script>
@ -29,7 +49,14 @@ export default defineComponent({
<div class="range-container"> <div class="range-container">
<p class="filter-item-paragraph">Precio</p> <p class="filter-item-paragraph">Precio</p>
<slot></slot> <q-range
:model-value="modelValue"
@update:model-value="updateModel"
v-bind="bindValue"
:min="minDefault"
:max="maxDefault"
color="primary"
/>
<div class="range-price-content"> <div class="range-price-content">
<p class="filter-item-paragraph min-price"> <p class="filter-item-paragraph min-price">

View File

@ -10,7 +10,7 @@ export default defineComponent({
const formStore = useFormStore(); const formStore = useFormStore();
const { sortProductFilters } = storeToRefs(formStore); const { sortProductFilters } = storeToRefs(formStore);
async function handleOrder(order) { function handleOrder(order) {
sortProductFilters.value.order = order; sortProductFilters.value.order = order;
sortProductFilters.value.isOpenOrderFilter = false; sortProductFilters.value.isOpenOrderFilter = false;
} }

View File

@ -33,8 +33,7 @@ export default defineComponent({
</li> </li>
<li class="footer-list-item"> <li class="footer-list-item">
<p class="footer-list-content"> <p class="footer-list-content"></p>
</p>
</li> </li>
</ul> </ul>
@ -44,7 +43,7 @@ export default defineComponent({
<RouterLink to="/categoria/plantas">Plantas</RouterLink><br /> <RouterLink to="/categoria/plantas">Plantas</RouterLink><br />
<!-- <RouterLink to="/">Nosotros</RouterLink><br /> <!-- <RouterLink to="/">Nosotros</RouterLink><br />
<RouterLink to="/faq">FAQs</RouterLink><br /> --> <RouterLink to="/faq">FAQs</RouterLink><br /> -->
<RouterLink to="/contacta">Contacta</RouterLink> <RouterLink to="/#question-section">Contacta</RouterLink>
</li> </li>
<li class="footer-list-item"> <li class="footer-list-item">
@ -57,7 +56,7 @@ export default defineComponent({
<IconArrowCircleRight /> Preguntas frecuentes <IconArrowCircleRight /> Preguntas frecuentes
</RouterLink> --> </RouterLink> -->
<br /> <br />
<RouterLink to="/example"> <RouterLink to="/#question-section">
<IconArrowCircleRight /> Contacta con nosotros <IconArrowCircleRight /> Contacta con nosotros
</RouterLink> </RouterLink>
</p> </p>
@ -66,12 +65,10 @@ export default defineComponent({
<li class="footer-list-item"> <li class="footer-list-item">
<p class="footer-list-content"> <p class="footer-list-content">
Floranet &copy;{{ year }} <br /><br /> Floranet &copy;{{ year }} <br /><br />
<RouterLink to="/example">Aviso Legal</RouterLink> <br /> <RouterLink to="/">Aviso Legal</RouterLink> <br />
<RouterLink to="/example">Condiciones de uso</RouterLink><br /> <RouterLink to="/">Condiciones de uso</RouterLink><br />
<RouterLink to="/example">Política de cookies</RouterLink><br /> <RouterLink to="/">Política de cookies</RouterLink><br />
<RouterLink to="/example"> <RouterLink to="/"> Política de Redes Sociales </RouterLink>
Política de Redes Sociales
</RouterLink>
<br /><br /> <br /><br />
Desarrollado por diligent Desarrollado por diligent
@ -141,6 +138,12 @@ a:hover {
} }
} }
@media only screen and (max-width: calc($med-xlg + 100px)) {
&.footer-primary {
flex: 0 0 min(100%, 480px);
}
}
@media only screen and (max-width: $med-lg) { @media only screen and (max-width: $med-lg) {
&.footer-primary { &.footer-primary {
flex: 0 0 min(100%, 275px); flex: 0 0 min(100%, 275px);
@ -150,6 +153,11 @@ a:hover {
flex: 0 0 min(100%, 545px); flex: 0 0 min(100%, 545px);
} }
} }
@media only screen and (max-width: $med-md + 600px) {
&.footer-primary {
flex: 0 0 min(100%, 300px);
}
}
@media only screen and (max-width: $med-md) { @media only screen and (max-width: $med-md) {
justify-content: space-evenly; justify-content: space-evenly;

View File

@ -29,9 +29,9 @@ export default defineComponent({
:class="isOpenNav && 'mobile-nav'" :class="isOpenNav && 'mobile-nav'"
> >
<send-banner <send-banner
left-text="ENVÍO GRATIS a partir de 60€ | Compra el sábado hasta 14h y entrega el domingo" left-text="ENVÍO GRATIS"
right-text="Envíos 24-48 h a toda España, Portugal y sur de Francia" right-text="Envíos 24-48 h a toda España, Portugal y sur de Francia"
mobile-text="ENVÍO GRATIS a partir de 60€" mobile-text="ENVÍO GRATIS"
v-if="!isOpenNav" v-if="!isOpenNav"
/> />

View File

@ -24,9 +24,9 @@ export default defineComponent({
<template> <template>
<q-header class="header-container transparent"> <q-header class="header-container transparent">
<send-banner <send-banner
left-text="ENVÍO GRATIS a partir de 60€ | Compra el sábado hasta 14h y entrega el domingo" left-text="ENVÍO GRATIS"
right-text="Envíos 24-48 h a toda España, Portugal y sur de Francia" right-text="Envíos 24-48 h a toda España, Portugal y sur de Francia"
mobile-text="ENVÍO GRATIS a partir de 60€" mobile-text="ENVÍO GRATIS"
class="remove-mobile" class="remove-mobile"
/> />

View File

@ -1,10 +1,11 @@
<script> <script>
import { defineComponent } from "vue"; import { computed, defineComponent } from "vue";
import IconCart from "components/icons/IconCart.vue"; import IconCart from "components/icons/IconCart.vue";
import IconHamburger from "components/icons/IconHamburger.vue"; import IconHamburger from "components/icons/IconHamburger.vue";
import { useLocalStorage } from "src/hooks/useLocalStorage"; import { storeToRefs } from "pinia";
import { useCartStore } from "src/stores/cart";
import { useMobileStore } from "stores/mobileNav"; import { useMobileStore } from "stores/mobileNav";
export default defineComponent({ export default defineComponent({
@ -14,14 +15,15 @@ export default defineComponent({
IconHamburger, IconHamburger,
}, },
setup() { setup() {
const { getItem } = useLocalStorage(); const cartStore = useCartStore();
const { cart } = storeToRefs(cartStore);
const mobileStore = useMobileStore(); const mobileStore = useMobileStore();
const { handleOpenMobileNav } = mobileStore; const { handleOpenMobileNav } = mobileStore;
const cartLength = getItem("cart").length; const currentLength = computed(() => cart.value.length);
return { handleOpenMobileNav, cartLength }; return { handleOpenMobileNav, currentLength };
}, },
}); });
</script> </script>
@ -38,11 +40,11 @@ export default defineComponent({
<RouterLink <RouterLink
class="user-area-link cart" class="user-area-link cart"
to="/checkout" to="/checkout"
v-if="cartLength > 0" v-if="currentLength > 0"
> >
<icon-cart /> <icon-cart />
<span class="cart-count" :class="cartLength > 0 && 'active'"> <span class="cart-count" :class="currentLength > 0 && 'active'">
{{ cartLength }} {{ currentLength }}
</span> </span>
</RouterLink> </RouterLink>

View File

@ -0,0 +1,75 @@
<script>
import { autoUpdate, useFloating } from "@floating-ui/vue";
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "icon-info",
setup() {
const iconRef = ref(null);
const tooltipRef = ref(null);
const isHidden = ref(true);
const hideTooltip = () => {
isHidden.value = true;
};
const showTooltip = () => {
isHidden.value = false;
};
const { floatingStyles } = useFloating(iconRef, tooltipRef, {
placement: "top-start",
whileElementsMounted: autoUpdate,
});
return { floatingStyles, isHidden, hideTooltip, showTooltip };
},
});
</script>
<template>
<div class="info-container">
<svg
height="48"
viewBox="0 0 48 48"
width="48"
xmlns="http://www.w3.org/2000/svg"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<path d="M0 0h48v48h-48z" fill="transparent" />
<path
d="M22 34h4v-12h-4v12zm2-30c-11.05 0-20 8.95-20 20s8.95 20 20 20 20-8.95 20-20-8.95-20-20-20zm0 36c-8.82 0-16-7.18-16-16s7.18-16 16-16 16 7.18 16 16-7.18 16-16 16zm-2-22h4v-4h-4v4z"
fill="#117564"
/>
</svg>
<a
href="https://www.google.com/maps"
rel="noreferrer noopener"
target="_blank"
:style="[floatingStyles, '--clr: #117564']"
:class="[isHidden && 'hidden', 'tooltip info', 'paragraph-sm', 'link']"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
Si no sabes tu código postal pincha aquí
</a>
</div>
</template>
<style lang="scss" scoped>
.info-container {
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
aspect-ratio: 1/1;
width: 16px;
height: 16px;
& svg {
width: inherit;
height: inherit;
}
}
</style>

View File

@ -1,13 +1,11 @@
<script> <script>
import { useIntersectionObserver } from "@vueuse/core"; import { defineComponent } from "vue";
import { storeToRefs } from "pinia";
import { defineComponent, ref } from "vue";
import Calendar from "src/components/@inputs/Calendar.vue"; import Calendar from "src/components/@inputs/Calendar.vue";
import PostalCode from "src/components/@inputs/PostalCode.vue"; import PostalCode from "src/components/@inputs/PostalCode.vue";
import IconSearch from "src/components/icons/IconSearch.vue"; import IconSearch from "src/components/icons/IconSearch.vue";
import { usePostalCalendar } from "src/hooks/usePostalCalendar"; import { usePostalCalendar } from "src/hooks/usePostalCalendar";
import { useMobileStore } from "src/stores/mobileNav"; import { useVerticalCarouselImgs } from "src/hooks/useVerticalCarouselImgs";
export default defineComponent({ export default defineComponent({
name: "vertical-carousel-imgs", name: "vertical-carousel-imgs",
@ -17,42 +15,32 @@ export default defineComponent({
default: () => [""], default: () => [""],
}, },
}, },
components: { IconSearch, Calendar, PostalCode },
setup() { setup() {
const { const {
onSubmit, onSubmit,
setValues, setValues,
setFieldError,
fields: { calendar, calendarAttrs, postalCode, postalCodeAttrs }, fields: { calendar, calendarAttrs, postalCode, postalCodeAttrs },
errors, errors,
} = usePostalCalendar({ type: "home" }); } = usePostalCalendar({ type: "home" });
const { navPos, screenWidth, slide, target } = useVerticalCarouselImgs();
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 { return {
slide: ref("style"), slide,
navPos, navPos,
target, target,
screenWidth, screenWidth,
errors, errors,
onSubmit, onSubmit,
setValues, setValues,
setFieldError,
postalCode, postalCode,
postalCodeAttrs, postalCodeAttrs,
calendar, calendar,
calendarAttrs, calendarAttrs,
}; };
}, },
components: { IconSearch, Calendar, PostalCode },
}); });
</script> </script>
@ -86,48 +74,29 @@ export default defineComponent({
<h1 class="carousel-header-title"> <h1 class="carousel-header-title">
Regala un verano lleno de flores y plantas Regala un verano lleno de flores y plantas
</h1> </h1>
<p class="carousel-header-paragraph">
</p>
</header> </header>
<form @submit="onSubmit" class="carousel-content-body"> <form @submit="onSubmit" class="carousel-content-body">
<div class="carousel-content-item"> <div class="carousel-content-item">
<Calendar :setValues="setValues"> <PostalCode
<q-input v-model="postalCode"
borderless v-bind:bindValue="postalCodeAttrs"
class="custom-date-input" :setFieldError="setFieldError"
v-model="calendar" />
v-bind="calendarAttrs"
:error="!!errors.date"
placeholder="Elige una fecha"
mask="##/##/####"
dense
/>
</Calendar>
</div> </div>
<div class="carousel-content-item"> <div class="carousel-content-item">
<PostalCode> <Calendar
<q-input v-model="calendar"
borderless v-bind:bindValue="calendarAttrs"
class="custom-main-paragraph" :setValues="setValues"
v-model="postalCode" />
v-bind="postalCodeAttrs"
:error="!!errors.postalCode"
placeholder="código postal"
mask="#####"
dense
/>
</PostalCode>
</div> </div>
<q-btn type="submit" class="btn carousel-content-item"> <q-btn type="submit" class="btn carousel-content-item">
<IconSearch /> ver disponibilidad <IconSearch /> ver disponibilidad
</q-btn> </q-btn>
</form> </form>
<!-- <footer class="carousel-content-footer"></footer> -->
</div> </div>
</div> </div>
</template> </template>
@ -174,6 +143,18 @@ export default defineComponent({
border-radius: 10px 0 0 10px; border-radius: 10px 0 0 10px;
overflow: hidden; overflow: hidden;
min-height: 96px; min-height: 96px;
/* & .carousel-content {
border-radius: 10px 10px 0 0;
overflow: hidden;
flex-direction: column;
justify-content: center;
align-items: center;
display: flex;
flex: 2;
background-color: $white;
} */
& .carousel-content-item { & .carousel-content-item {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -23,9 +23,11 @@ export default defineComponent({
> >
<Container class="question-container-form"> <Container class="question-container-form">
<header class="question-content"> <header class="question-content">
<LogoWhite /> <LogoWhite class="white-logo" />
<p class="question-paragraph"> <p class="question-paragraph">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore...
</p> </p>
</header> </header>
@ -70,6 +72,16 @@ p {
& .question-content { & .question-content {
flex: 1 0 min(100%, 540px); flex: 1 0 min(100%, 540px);
& .white-logo {
width: min(100%, 335px);
height: 75px;
@media only screen and (max-width: $med-xmd) {
width: min(100%, 175px);
height: 50px;
}
}
& .question-paragraph { & .question-paragraph {
line-height: 35px; line-height: 35px;
font-size: $font-25; font-size: $font-25;

View File

@ -1,65 +0,0 @@
<template>
<StripeCheckout
ref="checkoutRef"
mode="payment"
:pk="pK"
:line-items="cartItems"
:success-url="successURL"
:cancel-url="cancelURL"
@loading="(v) => (loading = v)"
style="display: none"
/>
<slot></slot>
</template>
<script>
import { storeToRefs } from "pinia";
import { defineComponent, ref, toRefs } from "vue";
import { useCartStore } from "src/stores/cart";
import { onUpdated } from "vue";
export default defineComponent({
name: "StripeCheckoutComponent",
components: {},
props: {
submitLoading: {
type: Boolean,
default: false,
},
onSubmit: {
type: Function,
default: () => {},
},
cartItems: {
type: Array,
default: () => [],
},
},
setup({ submitLoading, cartItems }) {
const cartStore = useCartStore();
const { checkoutRef } = storeToRefs(cartStore);
const loading = toRefs(submitLoading);
const pK = ref(
"pk_test_51OZaJdIK1lTlG93d2y0B81n4XrjvjQwqfIUZ7ggb9wEBa1e4h34GlYFYPwjtGl3OUT7DJZlVNX9EMXaCdOBkIC3T007mLnfvCu"
);
onUpdated(() => {
console.log(checkoutRef.value);
console.log(cartItems);
});
return {
pK,
loading,
checkoutRef,
successURL: ref("/checkout/success"),
cancelURL: ref("/checkout/cancel"),
};
},
});
</script>
<style lang="scss"></style>

View File

@ -20,8 +20,6 @@ export default defineComponent({
const nextSwiperBtn = ref(null); const nextSwiperBtn = ref(null);
onMounted(() => { onMounted(() => {
// console.log('Montado!');
swiperContainer.value = swiperContainer.value =
document.querySelector("swiper-container").shadowRoot; document.querySelector("swiper-container").shadowRoot;
prevSwiperBtn.value = swiperContainer.value.querySelector( prevSwiperBtn.value = swiperContainer.value.querySelector(
@ -40,11 +38,9 @@ export default defineComponent({
return { return {
screenWidth, screenWidth,
handlePrev() { handlePrev() {
// console.log('Prev click');
prevSwiperBtn.value.click(); prevSwiperBtn.value.click();
}, },
handleNext() { handleNext() {
// console.log('Next click');
nextSwiperBtn.value.click(); nextSwiperBtn.value.click();
}, },
}; };

View File

@ -33,10 +33,7 @@ export default defineComponent({
type: String, type: String,
default: "", default: "",
}, },
isNew: { isNew: String,
type: Boolean,
default: false,
},
size: { size: {
type: String, type: String,
default: "md-card", default: "md-card",
@ -54,10 +51,8 @@ export default defineComponent({
const isLoaded = ref(false); const isLoaded = ref(false);
const isError = ref(false); const isError = ref(false);
const percent = +discount / 100; const percent = +discount / 100;
//const priceWithoutLetter = ~~price?.replaceAll("", "");
const priceWithoutLetter = price; const priceWithoutLetter = price;
const finalValue = ~~(priceWithoutLetter - priceWithoutLetter * percent); const finalValue = ~~(priceWithoutLetter - priceWithoutLetter * percent);
console.log(price);
const onLoad = () => { const onLoad = () => {
isLoaded.value = true; isLoaded.value = true;

View File

@ -6,9 +6,7 @@ export default defineComponent({
name: "chat-component", name: "chat-component",
components: { IconChat }, components: { IconChat },
setup() { setup() {
const handleClick = () => { const handleClick = () => {};
console.log("click");
};
return { handleClick }; return { handleClick };
}, },
}); });

View File

@ -18,7 +18,6 @@ export default defineComponent({
function closeNav() { function closeNav() {
isOpenNav.value = false; isOpenNav.value = false;
console.log("foi click");
} }
watch(isOpenNav, (newValue) => { watch(isOpenNav, (newValue) => {

View File

@ -31,6 +31,7 @@ export default defineComponent({
const { const {
onSubmit, onSubmit,
setValues, setValues,
setFieldError,
fields: { fields: {
calendar, calendar,
calendarAttrs, calendarAttrs,
@ -39,6 +40,8 @@ export default defineComponent({
priceRange, priceRange,
priceRangeAttrs, priceRangeAttrs,
}, },
isPostalCalendarEmpty,
isAvailabilityEmpty,
errors, errors,
modalStore, modalStore,
} = usePostalCalendar({ } = usePostalCalendar({
@ -63,6 +66,10 @@ export default defineComponent({
modalStore, modalStore,
modalTextContent, modalTextContent,
setValues, setValues,
setFieldError,
isAvailabilityEmpty,
isPostalCalendarEmpty,
postalCode, postalCode,
postalCodeAttrs, postalCodeAttrs,
@ -106,16 +113,14 @@ export default defineComponent({
class="modal-body-filters" class="modal-body-filters"
> >
<div class="filter-field"> <div class="filter-field">
<PriceRange :min="priceRange.min" :max="priceRange.max"> <PriceRange
<q-range :minDefault="0"
v-model="priceRange" :maxDefault="200"
v-bind="priceRangeAttrs" v-model="priceRange"
:min="0" v-bind:bindValue="priceRangeAttrs"
:max="200" :min="priceRange.min"
color="primary" :max="priceRange.max"
/> />
<!-- @change="rangePriceStore.handlePriceRange" -->
</PriceRange>
</div> </div>
</div> </div>
@ -123,31 +128,17 @@ export default defineComponent({
v-if="modalItem === 'isOpenAvailability'" v-if="modalItem === 'isOpenAvailability'"
class="modal-body-availability" class="modal-body-availability"
> >
<Calendar :setValues="setValues"> <PostalCode
<q-input v-model="postalCode"
borderless v-bind:bindValue="postalCodeAttrs"
class="custom-date-input" :setFieldError="setFieldError"
v-model="calendar" />
v-bind="calendarAttrs"
:error="!!errors.date"
placeholder="Elige una fecha"
mask="##/##/####"
dense
/>
</Calendar>
<PostalCode> <Calendar
<q-input v-model="calendar"
borderless v-bind:bindValue="calendarAttrs"
class="custom-main-paragraph" :setValues="setValues"
v-model="postalCode" />
v-bind="postalCodeAttrs"
:error="!!errors.postalCode"
placeholder="código postal"
mask="#####"
dense
/>
</PostalCode>
</div> </div>
</form> </form>
</div> </div>
@ -159,23 +150,11 @@ export default defineComponent({
align="center" align="center"
class="modal-footer" class="modal-footer"
form="filters-form" form="filters-form"
:disabled="modalItem === 'isOpenFilters' && isPostalCalendarEmpty"
> >
<!-- v-close-popup -->
<IconSearch /> <IconSearch />
<p>ver disponibilidad</p> <p>ver disponibilidad</p>
</q-btn> </q-btn>
<!-- <q-btn
flat
type="button"
align="center"
class="modal-footer"
form="filters-form"
@click="handleSubmit({ content: modalItem })"
v-close-popup
>
<IconSearch />
<p>ver disponibilidad</p>
</q-btn> -->
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>
@ -238,6 +217,10 @@ export default defineComponent({
margin-bottom: 29px; margin-bottom: 29px;
&.availability { &.availability {
gap: 65px; gap: 65px;
@media only screen and (max-width: $med-md) {
gap: 10px;
}
} }
& .modal-body-paragraph { & .modal-body-paragraph {

View File

@ -1,49 +1,37 @@
<script> <script>
import { useQuasar } from "quasar";
import { useFormStore } from "src/stores/forms";
import { useForm } from "vee-validate";
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import IconArrowRightOne from "src/components/icons/IconArrowRightOne.vue"; import IconArrowRightOne from "src/components/icons/IconArrowRightOne.vue";
import { questionSchema } from "src/utils/zod/schemas/questionSchema"; import { useQuestionForm } from "src/hooks/useQuestionForm";
export default defineComponent({ export default defineComponent({
name: "QuestionForm", name: "QuestionForm",
components: { IconArrowRightOne }, components: { IconArrowRightOne },
setup() { setup() {
const $q = useQuasar(); const {
const formStore = useFormStore(); questionPhoneRef,
const { handleQuestionData } = formStore; formState: { isQuestionSubmitLoading, onSubmit, errors, meta },
fields: {
const { errors, meta, defineField, handleSubmit, handleReset } = useForm({ email,
validationSchema: questionSchema, emailAttrs,
initialValues: { firstName,
terms: false, firstNameAttrs,
message,
messageAttrs,
phone,
phoneAttrs,
query,
queryAttrs,
secondName,
secondNameAttrs,
terms,
termsAttrs,
}, },
}); } = useQuestionForm();
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 { return {
errors, isQuestionSubmitLoading,
questionPhoneRef,
firstName, firstName,
firstNameAttrs, firstNameAttrs,
secondName, secondName,
@ -59,6 +47,7 @@ export default defineComponent({
terms, terms,
termsAttrs, termsAttrs,
onSubmit, onSubmit,
errors,
meta, meta,
}; };
}, },
@ -78,6 +67,7 @@ export default defineComponent({
class="name" class="name"
outlined outlined
/> />
<q-input <q-input
v-model="secondName" v-model="secondName"
v-bind="secondNameAttrs" v-bind="secondNameAttrs"
@ -88,6 +78,7 @@ export default defineComponent({
class="nickname" class="nickname"
outlined outlined
/> />
<q-input <q-input
v-model="email" v-model="email"
v-bind="emailAttrs" v-bind="emailAttrs"
@ -97,20 +88,25 @@ export default defineComponent({
type="email" type="email"
label="Email" label="Email"
class="email" class="email"
autocomplete="email"
outlined outlined
/> />
<q-input
v-model="phone" <div class="field-control field-input telephone">
v-bind="phoneAttrs" <vue-tel-input
:error-message="errors.phone" v-model="phone"
:error="!!errors.phone" v-bind="phoneAttrs"
bg-color="white" :styleClasses="['custom-input', !!errors.phone && 'error']"
type="tel" ref="questionPhoneRef"
label="Teléfono" :inputOptions="{
class="telephone" placeholder: 'Teléfono*',
mask="(##) ##### ####" }"
outlined />
/> <p v-if="!!errors.phone" class="error">
{{ errors.phone }}
</p>
</div>
<q-input <q-input
v-model="query" v-model="query"
v-bind="queryAttrs" v-bind="queryAttrs"
@ -151,6 +147,7 @@ export default defineComponent({
class="question-submit-btn btn rounded" class="question-submit-btn btn rounded"
flat flat
:disable="!meta.valid" :disable="!meta.valid"
:loading="isQuestionSubmitLoading"
> >
Enviar solicitud <IconArrowRightOne /> Enviar solicitud <IconArrowRightOne />
</q-btn> </q-btn>
@ -202,6 +199,49 @@ export default defineComponent({
} }
} }
& .field-control.field-input {
flex: 1 0 min(100%, 218px);
&.telephone {
flex: 0 0 calc(50% - 5px);
@media only screen and (max-width: $med-lg) {
flex: 1 0 min(100%, 390px);
}
}
& .custom-input {
padding: 10.5px 1px;
border-radius: 4px;
transition: 200ms ease-in-out;
background-color: $white;
&:hover {
border-color: $black;
}
&:focus-within {
border-color: $primary;
box-shadow: inset 0 0 0 1px $primary;
}
&.error {
border-color: $negative;
box-shadow: inset 0 0 0 1px $negative;
}
& .vti__input::placeholder {
font-family: $font-questrial;
font-size: $font-12;
}
}
& p.error {
font-family: $font-questrial;
color: $negative;
font-size: $font-12;
padding: 8px 12px 0;
}
}
& .question-submit-btn { & .question-submit-btn {
align-self: flex-start; align-self: flex-start;
@media only screen and (max-width: $med-lg) { @media only screen and (max-width: $med-lg) {

2
src/constants/index.js Normal file
View File

@ -0,0 +1,2 @@
export * from "./date";
export * from "./objValitation";

View File

@ -0,0 +1,15 @@
export let countryFlagObj = {};
const countryFlagObjMessage = (code = "") => {
const { default: defaultValue, ...rest } = countryFlagObj;
const objectKeys = Object.keys(rest).join(", ");
return `El código de país ${code} no es válido! Apoyamos los siguientes códigos de país: ${objectKeys}.`;
};
countryFlagObj = {
34: "es",
351: "pt",
33: "fr",
default: countryFlagObjMessage,
};

View File

@ -321,8 +321,70 @@ body {
margin-bottom: -14px; margin-bottom: -14px;
} }
//! QUASAR .error-message {
min-height: 525px !important;
color: $primary;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
& h1 {
font-size: $font-40;
}
@media only screen and (max-width: $med-md) {
min-height: 400px !important;
& h1 {
line-height: 1.2;
font-size: $font-28;
}
}
}
.tooltip {
user-select: none;
display: flex;
justify-content: center;
align-items: center;
gap: 2px;
padding: 4px 10px;
border-radius: 4px;
background-color: $white;
border: 1px solid $primary-dark;
&.info {
left: 16px !important;
top: -20px !important;
padding: 2px 6px;
white-space: nowrap;
opacity: 1;
}
}
.paragraph-sm {
color: $primary;
font-size: $font-14;
font-family: $font-questrial;
color: var(--clr);
&.link {
text-decoration: underline;
}
}
//! Vue-tel-input
.vti__input {
font-family: $font-questrial;
font-size: $font-14;
&::placeholder {
font-size: $font-14;
font-family: $font-questrial;
}
}
//! QUASAR
.q-virtual-scroll__content .q-item .q-item__label { .q-virtual-scroll__content .q-item .q-item__label {
font-family: $font-questrial; font-family: $font-questrial;
font-size: $font-14; font-size: $font-14;

View File

@ -22,11 +22,13 @@
& .custom-head-paragraph, & .custom-head-paragraph,
& .custom-main-paragraph { & .custom-main-paragraph {
line-height: 20px; line-height: 20px;
display: flex;
gap: 2px;
} }
& .custom-head-paragraph { & .custom-head-paragraph {
font-size: $font-12; font-size: $font-12;
opacity: 0.4; // opacity: 0.4;
} }
& .custom-main-paragraph { & .custom-main-paragraph {

View File

@ -26,20 +26,25 @@ export function quasarNotify({ message = "", type, timeout = 1000 }) {
icon: "report_problem", icon: "report_problem",
timeout, timeout,
}), }),
info: () => Notify.create({ info: () =>
message, Notify.create({
color: "info", message,
position: "top", color: "info",
icon: "info", position: "top",
timeout, icon: "info",
}), timeout,
}),
default: () => { default: () => {
console.error(`Type is invalid! TYPE: ${type}`) console.error(`Type is invalid! TYPE: ${type}`);
} },
}; };
if (type) { if (type) {
return obj[type]() || obj['default'](); return obj[type]() || obj["default"]();
} }
console.error("Type is required, success, warning or erro");
const { default: defaultValue, ...rest } = obj;
const objTypes = Object.keys(rest).join(", ");
console.error(`Type is required, valid types: ${objTypes}`);
} }

View File

@ -1,23 +1,78 @@
import { autoUpdate, useFloating } from "@floating-ui/vue";
import { toTypedSchema } from "@vee-validate/zod"; import { toTypedSchema } from "@vee-validate/zod";
import { storeToRefs } from "pinia";
import { useForm } from "vee-validate"; import { useForm } from "vee-validate";
import { computed, reactive, ref } from "vue"; import { computed, reactive, ref, watch } from "vue";
import { apiBack } from "src/boot/axios";
import { quasarNotify } from "src/functions/quasarNotify";
import { useFormStore } from "src/stores/forms"; import { useFormStore } from "src/stores/forms";
import { checkoutSchema } from "src/utils/zod/schemas"; import { checkoutSchema } from "src/utils/zod/schemas";
import { useRouter } from "vue-router";
import { useLocalStorage } from "./useLocalStorage"; import { useLocalStorage } from "./useLocalStorage";
export function useCheckoutForm() { export function useCheckoutForm() {
const { getItem } = useLocalStorage(); const { addItem, getItem, removeItem } = useLocalStorage();
const { push } = useRouter()
//! Elements ref
const postalCodeRef = ref(null);
const postalCodeTooltip = ref(null);
const phoneInputRef = ref(null);
const phoneSenderInputRef = ref(null);
const redsysFormRef = ref(null);
//! Form
const formStore = useFormStore(); const formStore = useFormStore();
const { availability: availabilityForm } = storeToRefs(formStore);
const { handleCheckoutData } = formStore; const { handleCheckoutData } = formStore;
const availability =
availabilityForm.value.dateExpired || getItem("availability");
const phoneData = ref({
country: {
name: "",
iso2: "",
dialCode: "",
priority: 0,
areaCodes: null,
},
countryCallingCode: "",
nationalNumber: "",
number: "",
countryCode: "",
valid: false,
formatted: "",
});
const phoneSenderData = ref({
country: {
name: "",
iso2: "",
dialCode: "",
priority: 0,
areaCodes: null,
},
countryCallingCode: "",
nationalNumber: "",
number: "",
countryCode: "",
valid: false,
formatted: "",
});
const provinceOptions = ref([
{ code: "es", name: "España" },
// { code: "fr", name: "Francia" },
// { code: "pt", name: "Portugal" },
]);
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({ const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
validationSchema: toTypedSchema(checkoutSchema), validationSchema: toTypedSchema(checkoutSchema),
initialValues: { initialValues: {
paymentMethod: "stripe", paymentMethod: "",
terms: false, terms: false,
postalCode: availabilityForm.value.postalCode || availability.postalCode,
phone: "",
senderPhone: "",
}, },
}); });
@ -25,71 +80,51 @@ export function useCheckoutForm() {
const [surname, surnameAttrs] = defineField("surname"); const [surname, surnameAttrs] = defineField("surname");
const [address, addressAttrs] = defineField("address"); const [address, addressAttrs] = defineField("address");
const [postalCode, postalCodeAttrs] = defineField("postalCode"); const [postalCode, postalCodeAttrs] = defineField("postalCode");
const [phone, phoneAttrs] = defineField("phone"); const [phone, phoneAttrs] = defineField("phone", {
validateOnModelUpdate: false,
});
const [city, cityAttrs] = defineField("city"); const [city, cityAttrs] = defineField("city");
const [province, provinceAttrs] = defineField("province"); const [province, provinceAttrs] = defineField("province");
const [senderName, senderNameAttrs] = defineField("senderName"); const [senderName, senderNameAttrs] = defineField("senderName");
const [senderCifNif, senderCifNifAttrs] = defineField("senderCifNif"); const [senderCifNif, senderCifNifAttrs] = defineField("senderCifNif");
const [senderEmail, senderEmailAttrs] = defineField("senderEmail"); const [senderEmail, senderEmailAttrs] = defineField("senderEmail");
const [senderPhone, senderPhoneAttrs] = defineField("senderPhone"); const [senderPhone, senderPhoneAttrs] = defineField("senderPhone", {
validateOnModelUpdate: false,
});
const [senderNotes, senderNotesAttrs] = defineField("senderNotes"); const [senderNotes, senderNotesAttrs] = defineField("senderNotes");
const [paymentMethod, paymentMethodAttrs] = defineField("paymentMethod"); const [paymentMethod, paymentMethodAttrs] = defineField("paymentMethod");
const [terms, termsAttrs] = defineField("terms"); const [terms, termsAttrs] = defineField("terms");
const provinceOptions = ref([ //! Tooltip hook
{ code: "01", name: "Araba/Álava" }, const { floatingStyles } = useFloating(postalCodeRef, postalCodeTooltip, {
{ code: "02", name: "Albacete" }, placement: "top-start",
{ code: "03", name: "Alicante/Alacant" }, whileElementsMounted: autoUpdate,
{ code: "04", name: "Almería" }, });
{ code: "05", name: "Ávila" },
{ code: "06", name: "Badajoz" }, const isHidden = ref(true);
{ code: "07", name: "Balears, Illes" }, const hideTooltip = () => {
{ code: "08", name: "Barcelona" }, isHidden.value = true;
{ code: "09", name: "Burgos" }, };
{ code: "10", name: "Cáceres" }, const showTooltip = () => {
{ code: "11", name: "Cádiz" }, isHidden.value = false;
{ code: "12", name: "Castellón/Castelló" }, };
{ code: "13", name: "Ciudad Real" },
{ code: "14", name: "Córdoba" }, // TODO hacer el await de las provincias
{ code: "15", name: "Coruña, A" }, /**
{ code: "16", name: "Cuenca" }, * const provinceOptions = getProvinces();
{ code: "17", name: "Girona" }, * onBeforeMount(async () => {});
{ code: "18", name: "Granada" }, * */
{ code: "19", name: "Guadalajara" },
{ code: "20", name: "Gipuzkoa" }, watch(
{ code: "21", name: "Huelva" }, [
{ code: "22", name: "Huesca" }, () => phoneInputRef.value?.modelValue,
{ code: "23", name: "Jaén" }, () => phoneSenderInputRef.value?.modelValue,
{ code: "24", name: "León" }, ],
{ code: "25", name: "Lleida" }, ([a, b]) => {
{ code: "26", name: "Rioja, La" }, phoneData.value = phoneInputRef.value.phoneObject;
{ code: "27", name: "Lugo" }, phoneSenderData.value = phoneSenderInputRef.value.phoneObject;
{ 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" },
]);
const stepActive = reactive({ data: 1 }); const stepActive = reactive({ data: 1 });
const stepList = reactive({ const stepList = reactive({
@ -122,27 +157,162 @@ export function useCheckoutForm() {
return step; return step;
}); });
}); });
const isError = ref(false);
const onError = () => {
isError.value = true;
};
const handleClickStep = (value) => { const handleClickStep = (value) => {
stepActive["data"] = value; stepActive["data"] = value;
}; };
const checkoutBlock = ref(true); const checkoutBlock = ref(true);
const onSubmit = handleSubmit((values) => { const cart = getItem("cart");
handleCheckoutData(values); const totalPrice = computed(() => {
stepList.data[2].active = true; return cart?.reduce((acc, { price }) => {
checkoutBlock.value = false; if (price) {
resetForm(); //const priceWithoutLetter = price?.replace("€", "");
return +price + acc;
}
}, 0);
}); });
const cart = getItem("cart"); const redsysData = ref({
const totalPrice = ref(0) Ds_MerchantParameters: "",
totalPrice.value = cart?.reduce((acc, { price }) => { Ds_Signature: "",
if (price) { Ds_SignatureVersion: "",
const priceWithoutLetter = price?.replace("€", ""); orderId: null,
return +priceWithoutLetter + acc; });
const isLoadingSubmit = ref(false);
const isErrorSubmit = ref(false);
/**
* Handles the fetching of the payment method.
*
* @param {string} type - The type of payment method paypal or redsys are valid!.
* @param {Object} values - The values needed for the payment method.
* @returns {Promise<void>} - A promise that resolves when the payment method is fetched.
*/
const handleFetchPaymentMethod = async ({ type, values }) => {
try {
const productsId = cart.map((item) => item.id);
const cartItensData = cart.map(({ id, message, ...rest }) => ({
id,
message: message || "",
}));
const deliveryData = {
customerData: {
custumerName: `${values.name} ${values.surname}`,
email: values.senderEmail,
custumerPhone: phoneData.value.number,
},
itemData: cartItensData,
deliveryData: {
dated: availability.dateExpired,
deliveryName: values.senderName,
address: values.address,
postalCode: availability.postalCode,
deliveryPhone: phoneSenderData.value.number,
deliveryMessage: values.senderNotes,
},
};
const customerName = `${values.name} ${values.surname}`;
addItem("costumer", deliveryData);
const productData = {
products: productsId,
dateExpired: availability.dateExpired,
postalCode: postalCode.value,
customer: {
customerData: {
customerName,
email: values.senderEmail,
customerPhone: phoneData.value.number,
message: values.senderNotes,
deliveryName: values.senderName || customerName,
address: values.address,
deliveryPhone:
phoneSenderData.value.number || phoneData.value.number,
},
},
type: values.paymentMethod,
};
addItem("payment", values.paymentMethod);
const typeObj = {
paypal: async () => {
const {
data: { data },
} = await apiBack.post("payment", productData);
location.href = data.link;
},
redsys: async () => {
const {
data: { data },
} = await apiBack.post("payment", productData);
redsysData.value = data;
redsysFormRef.value.click();
},
default: () => {
console.error(
`FATAL ERROR ::: Payment method not found, TYPE: ${type}`
);
},
};
const paymentMethod = typeObj[type] || typeObj["default"];
await paymentMethod();
// removeItem("cart");
// removeItem("availability");
} catch (error) {
console.error(`FATAL ERROR ::: ${error}`);
quasarNotify({
type: "erro",
message:
"Se produjo un error al procesar tu compra, inténtalo de nuevo.",
});
isErrorSubmit.value = true;
} finally {
isLoadingSubmit.value = false;
handleCheckoutData(values);
resetForm();
} }
}, 0); };
const onSuccess = async (values, actions) => {
const INVALID_NUMBER =
"Número no válido introducido, por favor, compruébelo e inténtelo de nuevo";
if (!phoneData.value.valid) {
actions.setFieldError("phone", INVALID_NUMBER);
return;
}
if (values.senderPhone.length > 0 && !phoneSenderData.value.valid) {
actions.setFieldError("senderPhone", INVALID_NUMBER);
return;
}
isLoadingSubmit.value = true;
stepsFormated.value[1].active = true;
await handleFetchPaymentMethod({
type: values.paymentMethod,
values,
});
};
const onSubmit = handleSubmit(onSuccess);
return { return {
handleClickStep, handleClickStep,
@ -152,11 +322,31 @@ export function useCheckoutForm() {
checkoutBlock, checkoutBlock,
cart, cart,
totalPrice, totalPrice,
isError,
redsysData,
phoneInputRef,
phoneSenderInputRef,
redsysFormRef,
phone: { phoneData, phoneSenderData },
onError,
tooltip: {
postalCode: {
postalCodeRef,
postalCodeTooltip,
floatingStyles,
isHidden,
hideTooltip,
showTooltip,
},
},
formState: { formState: {
meta, meta,
errors, errors,
onSubmit, onSubmit,
submitLoading: ref(false), isLoadingSubmit,
}, },
fields: { fields: {
name, name,

View File

@ -1,22 +1,39 @@
import { LocalStorage } from "quasar"; import { LocalStorage } from "quasar";
export function useLocalStorage() { export function useLocalStorage() {
/**
* Adds an item to localStorage.
* @param {string} key - The key of the item to be added.
* @param {*} value - The value of the item to be added.
*/
const addItem = (key, value) => { const addItem = (key, value) => {
LocalStorage.set(`@${key}`, value); const stringifyValue = JSON.stringify(value);
LocalStorage.set(`@${key}`, stringifyValue);
}; };
/**
* Retrieves an item from the local storage based on the provided key.
*
* @param {string} key - The key of the item to retrieve.
* @returns {Object|Array} - The retrieved item from the local storage. If the key is "availability", it returns an object, otherwise it returns an array.
*/
const getItem = (key) => { const getItem = (key) => {
const data = JSON.parse(LocalStorage.getItem(`@${key}`)); const data = JSON.parse(LocalStorage.getItem(`@${key}`));
if (key === "availability") return data || {};
return (data || []); return data || [];
}; };
/**
* Remove an item from local storage.
*
* @param {string} key - The key of the item to remove.
*/
const removeItem = (key) => { const removeItem = (key) => {
LocalStorage.remove(`@${key}`); LocalStorage.remove(`@${key}`);
}; };
return { return {
addItem, addItem,
getItem, getItem,

View File

@ -1,9 +1,10 @@
import { toTypedSchema } from "@vee-validate/zod"; import { toTypedSchema } from "@vee-validate/zod";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useForm } from "vee-validate"; import { useForm } from "vee-validate";
import { ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { fullCurrentDate } from "src/constants";
import { invertDate } from "src/functions/invertDate"; import { invertDate } from "src/functions/invertDate";
import { quasarNotify } from "src/functions/quasarNotify"; import { quasarNotify } from "src/functions/quasarNotify";
import { useCartStore } from "src/stores/cart"; import { useCartStore } from "src/stores/cart";
@ -12,172 +13,295 @@ import { useModalStore } from "src/stores/modalStore";
import { useRangePriceStore } from "src/stores/rangePrice"; import { useRangePriceStore } from "src/stores/rangePrice";
import { availabilitySchema } from "src/utils/zod/schemas"; import { availabilitySchema } from "src/utils/zod/schemas";
import { rangePriceSchema } from "src/utils/zod/schemas/rangePriceSchema"; import { rangePriceSchema } from "src/utils/zod/schemas/rangePriceSchema";
import { useLocalStorage } from "./useLocalStorage";
/** /**
* Custom hook for managing the postal and calendar functionality. * Custom hook for managing postal calendar functionality.
* *
* @param {Object} options - The options for the hook. * @param {Object} options - The options for the hook.
* @param {string} options.modalItem - The modal item isOpenAvailability || isOpenFilters. * @param {string} options.modalItem - The modal item.
* @param {string} options.type - The type of the hook. home || product || availability || filter * @param {string} options.type - The type of the calendar.
* @returns {Object} - The hook functions and data. * @returns {Object} - The hook functions and properties.
*/ */
export function usePostalCalendar({ modalItem = "", type = "home" }) { export function usePostalCalendar({ modalItem = "", type = "home" }) {
const route = useRoute(); const route = useRoute();
const { push } = useRouter() const { push } = useRouter();
const { addItem, getItem, removeItem } = useLocalStorage();
const modalStore = useModalStore();
const rangePriceStore = useRangePriceStore(); const rangePriceStore = useRangePriceStore();
const { rangeValue } = storeToRefs(rangePriceStore); const { rangeValue } = storeToRefs(rangePriceStore);
const modalStore = useModalStore();
const { openModal } = modalStore
const formStore = useFormStore(); const formStore = useFormStore();
const { sortProductFilters } = storeToRefs(formStore); const { sortProductFilters, availability: availabilityForm } =
storeToRefs(formStore);
const cartStore = useCartStore(); const cartStore = useCartStore();
const { addToCart, getProducts } = cartStore; const { addToCart, getProducts } = cartStore;
const { products, homeSection } = storeToRefs(cartStore); const { products, cart } = storeToRefs(cartStore);
const min = 0; const min = 0;
const max = 200; const max = 200;
const category = ref(route.path.split("/")[2]) const category = ref(route.path.split("/")[2]);
const categoryObj = {
plantas: "Floranet Plantas",
ramos: "Floranet Ramos",
};
const availability = ref(getItem("availability"));
const { handleSubmit, handleReset, defineField, errors, setValues } = useForm( const availabilityFormKeys = computed(() => {
{ return Object.fromEntries(
validationSchema: toTypedSchema( Object.entries(availabilityForm.value).filter(
type !== "filter" ? availabilitySchema : rangePriceSchema ([key, value]) => value !== ""
), )
initialValues: { );
range: { });
min,
max, const isAvailabilityEmpty = computed(() => {
}, return (
postalCode: "", Object.keys(availability.value || availabilityFormKeys.value).length === 0
date: "", );
}, });
const isPostalCalendarEmpty = computed(() => {
if (category.value === "ramos" || category.value === "plantas") {
const isAvailabilityEmptyForm =
Object.keys(availabilityFormKeys.value).length === 0;
return isAvailabilityEmptyForm;
} }
);
const [calendar, calendarAttrs] = defineField("date"); return isAvailabilityEmpty.value;
const [postalCode, postalCodeAttrs] = defineField("postalCode"); });
const [priceRange, priceRangeAttrs] = defineField("range");
const [YEAR, MONTH, DAY] = fullCurrentDate.replaceAll("/", "-").split("-");
const CURRENT_DATE = `${DAY}-${MONTH}-${YEAR}`;
const {
handleSubmit,
handleReset,
defineField,
errors,
setValues,
setFieldError,
} = useForm({
validateOnMount: false,
validationSchema: toTypedSchema(
type !== "filter" ? availabilitySchema : rangePriceSchema
),
initialValues: {
range: {
min,
max,
},
postalCode: "",
date: CURRENT_DATE,
},
initialTouched: {
date: false,
postalCode: true,
},
});
const options = {
validateOnBlur: false,
validateOnChange: false,
validateOnInput: false,
validateOnModelUpdate: false,
};
const [calendar, calendarAttrs] = defineField("date", options);
const [postalCode, postalCodeAttrs] = defineField("postalCode", options);
const [priceRange, priceRangeAttrs] = defineField("range", options);
const [dedication, dedicationAttrs] = defineField("dedication"); const [dedication, dedicationAttrs] = defineField("dedication");
watch(errors, (newErrors) => { watch(errors, (newErrors) => {
const errorsObj = { const hasErrors = {
postalCode: () => range: newErrors.range,
quasarNotify({ message: newErrors.postalCode, type: "erro" }), dedication: newErrors.dedication,
date: () => quasarNotify({ message: newErrors.date, type: "erro" }),
range: () => quasarNotify({ message: newErrors.range, type: "erro" }),
dedication: () =>
quasarNotify({ message: newErrors.dedication, type: "erro" }),
}; };
const keys = Object.keys(newErrors); for (const [field, hasError] of Object.entries(hasErrors)) {
keys.forEach((key) => { if (hasError) {
errorsObj[key](); quasarNotify({ message: newErrors[field], type: "erro" });
}); }
}
}); });
watch( watch([() => route.path, () => sortProductFilters.value], ([newPath]) => {
[() => route.path, () => sortProductFilters.value], const categoryPath = newPath.split("/")[2];
([newPath]) => { category.value = categoryPath;
const categoryPath = newPath.split("/")[2]; availabilityForm.value.dateExpired = "";
category.value = categoryPath; availability.value.postalCode = "";
} sortProductFilters.value.isOpenOrderFilter = false;
); sortProductFilters.value.order = undefined;
});
const onSubmit = handleSubmit((values) => { const removeCart = () => {
const postalAndDateParams = { removeItem("cart");
postalCode: values.postalCode, cart.value = [];
dateExpired: invertDate(values.date), };
const onSuccess = async (values) => {
const handleAvailability = async () => {
addItem("availability", {
postalCode: values.postalCode,
dateExpired: invertDate(values.date),
});
removeCart();
availabilityForm.value.dateExpired = invertDate(values.date);
availabilityForm.value.postalCode = values.postalCode;
await getProducts({
type: categoryObj[category.value],
postalCode: values.postalCode,
dateExpired: invertDate(values.date),
});
}; };
const categoryObj = { const handleHome = async () => {
plantas: "Floranet Plantas",
ramos: "Floranet Ramos",
};
const objVal = { addItem("availability", {
home: async () => { postalCode: values.postalCode,
console.log(type); dateExpired: invertDate(values.date),
});
availabilityForm.value.dateExpired = invertDate(values.date);
availabilityForm.value.postalCode = values.postalCode;
removeCart();
await getProducts( const callback = async () => {
{ await push("/categoria/all");
postalCode: values.postalCode, };
dateExpired: invertDate(values.date),
},
() => homeSection.value.scrollIntoView()
);
},
product: async () => {
console.log(type);
await getProducts(postalAndDateParams); await getProducts(
{
const hasProduct = products.value.data.some((item) => {
const date = new Date(item.dateExpired);
const day = date.getDate();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
const dateExpired = `${day}/${month}/${year}`;
const id = +route.path.split('/')[2];
return item.postalCode === values.postalCode && item.id === id && values.date <= dateExpired
});
if (!hasProduct) {
push('/categoria/ramos')
quasarNotify({ message: 'Seleccione una nueva fecha y un nuevo código postal.', type: 'warning' })
setTimeout(() => {
openModal({ modal: 'availability' })
}, 2000)
return
}
addToCart(products.value.current, dedication)
},
availability: async () => {
console.log(type);
await getProducts({
postalCode: values.postalCode, postalCode: values.postalCode,
dateExpired: invertDate(values.date), dateExpired: invertDate(values.date),
}); },
}, callback
filter: async () => { );
console.log(type);
rangeValue.value.max = values.range.max;
rangeValue.value.min = values.range.min;
const params = {
type: categoryObj[category.value],
minPrice: values.range.min,
maxPrice: values.range.max,
};
await getProducts(params);
},
default: () => {
console.error(
`INVALID TYPE! TYPE: ${type}, ONLY HOME, PRODUCT AND FILTER ARE VALID!`
);
},
}; };
objVal[type]() || objVal["default"]();
const handleProduct = async () => {
addItem("availability", {
postalCode: values.postalCode,
dateExpired: invertDate(values.date),
});
removeCart();
availabilityForm.value.dateExpired = invertDate(values.date);
availabilityForm.value.postalCode = values.postalCode;
await getProducts({
postalCode: values.postalCode,
dateExpired: invertDate(values.date),
});
const hasProduct = computed(() => {
return products.value.data.some((item) => {
const date = new Date(item.dateExpired);
const day = date.getDate();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
const dateExpired = `${day}/${month}/${year}`;
const dateSelected = values.date.replaceAll("-", "/");
const id = +route.path.split("/")[2];
return (
item.postalCode === values.postalCode &&
item.id === id &&
dateSelected <= dateExpired
);
});
});
if (!hasProduct.value) {
quasarNotify({
message: "Código postal y fecha de caducidad añadidos con éxito",
type: "success",
});
return;
}
// go();
addToCart(products.value.current, dedication);
};
const handleFilter = async () => {
rangeValue.value.max = values.range.max;
rangeValue.value.min = values.range.min;
const params = {
type: categoryObj[category.value],
minPrice: values.range.min,
maxPrice: values.range.max,
postalCode: availabilityForm.value.postalCode,
dateExpired: availabilityForm.value.dateExpired,
};
if (category.value === "all") {
params.postalCode =
availability.value.postalCode || availabilityForm.value.postalCode;
params.dateExpired =
availability.value.dateExpired || availabilityForm.value.dateExpired;
const { type, ...rest } = params;
await getProducts({ ...rest });
return;
}
getProducts(params);
};
const handleDefault = () => {
console.error(
`INVALID TYPE! TYPE: ${type}, ONLY HOME, PRODUCT AND FILTER ARE VALID!`
);
};
const handlers = {
availability: handleAvailability,
home: handleHome,
product: handleProduct,
filter: handleFilter,
default: handleDefault,
};
const handler = handlers[type] || handlers.default;
await handler();
if (modalItem) { if (modalItem) {
modalStore[modalItem] = false; modalStore[modalItem] = false;
} }
handleReset(); handleReset();
}); };
const onError = ({ values, errors, results }) => {
const hasErrors = {
postalCode: !!errors.postalCode,
date: !!errors.date,
};
for (const [field, hasError] of Object.entries(hasErrors)) {
if (hasError) {
quasarNotify({ message: errors[field], type: "erro" });
}
}
};
const onSubmit = handleSubmit(onSuccess, onError);
return { return {
onSubmit, onSubmit,
setValues, setValues,
handleReset,
modalStore, modalStore,
setFieldError,
isAvailabilityEmpty,
isPostalCalendarEmpty,
availabilityFormKeys,
category,
fields: { fields: {
calendar, calendar,
calendarAttrs, calendarAttrs,

View File

@ -1,51 +0,0 @@
import { storeToRefs } from "pinia";
import { useCartStore } from "src/stores/cart";
import { useModalStore } from "src/stores/modalStore";
import { watch } from "vue";
import { useRoute } from "vue-router";
import { usePostalCalendar } from "./usePostalCalendar";
export function useProductPage() {
const route = useRoute();
const {
fields: { dedication, dedicationAttrs },
} = usePostalCalendar({ modalItem: "isOpenAvailability" });
const modalStore = useModalStore();
const { openModal } = modalStore;
const cartStore = useCartStore();
const { getProduct, getProducts } = cartStore;
const { products, featuredProducts, addCartLoadingBtn } =
storeToRefs(cartStore);
watch(
() => products.value.current?.type,
(newCategory) => {
getProducts({
// type: newCategory,
});
}
);
watch(
() => route.params.id,
(newId) => {
getProduct(newId);
}
);
const checkImageValidity = (imageLink) => {
const validExtensions = [".jpg", ".jpeg", ".png"];
if (imageLink) {
const extension = imageLink.substring(imageLink.lastIndexOf("."));
return validExtensions.includes(extension.toLowerCase());
}
return true;
};
return { checkImageValidity, openModal }
}

View File

@ -0,0 +1,113 @@
import { useForm } from "vee-validate";
import { ref } from "vue";
import { apiBack } from "src/boot/axios";
import { quasarNotify } from "src/functions/quasarNotify";
import { useFormStore } from "src/stores/forms";
import { questionSchema } from "src/utils/zod/schemas";
import { watch } from "vue";
export function useQuestionForm() {
const formStore = useFormStore();
const { handleQuestionData } = formStore;
//! Elements
const questionPhoneRef = ref(null);
const questionPhoneData = ref({
country: {
name: "",
iso2: "",
dialCode: "",
priority: 0,
areaCodes: null,
},
countryCallingCode: "",
nationalNumber: "",
number: "",
countryCode: "",
valid: false,
formatted: "",
});
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", {
validateOnModelUpdate: false,
});
const [query, queryAttrs] = defineField("query");
const [message, messageAttrs] = defineField("message");
const [terms, termsAttrs] = defineField("terms");
watch(
() => questionPhoneRef.value?.modelValue,
() => {
questionPhoneData.value = questionPhoneRef.value.phoneObject;
}
);
const isQuestionSubmitLoading = ref(false);
const isQuestionSubmitError = ref(false);
const onSubmit = handleSubmit(async (values, actions) => {
isQuestionSubmitLoading.value = true;
if (!questionPhoneData.value.valid) {
actions.setFieldError("phone", "El teléfono no es válido");
return;
}
try {
const contactData = {
name: `${values.name} ${values.surname}`,
phone: questionPhoneData.value.number,
email: values.email,
message: values.message,
};
await apiBack.post("contact/save", contactData);
isQuestionSubmitLoading.value = false;
quasarNotify({ type: "success", message: "Mensaje enviado" });
} catch (error) {
console.error(`FATAL ERROR ::: ${error}`);
isQuestionSubmitLoading.value = false;
isQuestionSubmitError.value = true;
} finally {
handleQuestionData(values);
handleReset();
}
});
return {
questionPhoneData,
questionPhoneRef,
formState: {
isQuestionSubmitLoading,
onSubmit,
errors,
meta,
},
fields: {
firstName,
firstNameAttrs,
secondName,
secondNameAttrs,
email,
emailAttrs,
phone,
phoneAttrs,
query,
queryAttrs,
message,
messageAttrs,
terms,
termsAttrs,
},
};
}

View File

@ -0,0 +1,25 @@
import { useIntersectionObserver } from "@vueuse/core";
import { storeToRefs } from "pinia";
import { useMobileStore } from "src/stores/mobileNav";
import { onMounted, ref } from "vue";
export function useVerticalCarouselImgs() {
const mobileStore = useMobileStore();
const { screenWidth } = storeToRefs(mobileStore);
const { handleResize } = mobileStore;
const target = ref(null);
const navPos = ref("bottom");
const slide = ref("style");
onMounted(() => {
screenWidth.value = window.innerWidth;
});
useIntersectionObserver(target, ([{ isIntersecting }]) => {
mobileStore.isCarouselVisible = isIntersecting;
});
document.addEventListener("resize", handleResize);
return { screenWidth, navPos, slide, target };
}

View File

@ -1,7 +1,7 @@
<script> <script>
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { defineComponent, onBeforeMount, onUpdated, ref, watch } from "vue"; import { computed, defineComponent, onBeforeMount, ref, watch } from "vue";
import { useRoute } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import SortSelect from "src/components/@inputs/SortSelect.vue"; import SortSelect from "src/components/@inputs/SortSelect.vue";
import IconArrowCircleFilledRight from "src/components/icons/IconArrowCircleFilledRight.vue"; import IconArrowCircleFilledRight from "src/components/icons/IconArrowCircleFilledRight.vue";
@ -13,6 +13,9 @@ import Card from "src/components/ui/Card.vue";
import Container from "src/components/ui/Container.vue"; import Container from "src/components/ui/Container.vue";
import Modal from "src/components/ui/Modal.vue"; import Modal from "src/components/ui/Modal.vue";
import { quasarNotify } from "src/functions/quasarNotify";
import { useLocalStorage } from "src/hooks/useLocalStorage";
import { usePostalCalendar } from "src/hooks/usePostalCalendar";
import { useCartStore } from "src/stores/cart"; import { useCartStore } from "src/stores/cart";
import { useFormStore } from "src/stores/forms"; import { useFormStore } from "src/stores/forms";
import { useMobileStore } from "src/stores/mobileNav"; import { useMobileStore } from "src/stores/mobileNav";
@ -33,6 +36,9 @@ export default defineComponent({
}, },
setup() { setup() {
const route = useRoute(); const route = useRoute();
const { push } = useRouter();
const { getItem } = useLocalStorage();
const { isAvailabilityEmpty } = usePostalCalendar({});
const mobileStore = useMobileStore(); const mobileStore = useMobileStore();
const { screenWidth } = storeToRefs(mobileStore); const { screenWidth } = storeToRefs(mobileStore);
@ -41,29 +47,19 @@ export default defineComponent({
const { openModal } = modalStore; const { openModal } = modalStore;
const formStore = useFormStore(); const formStore = useFormStore();
const { availability, sortProductFilters } = storeToRefs(formStore); const { sortProductFilters, availability } = storeToRefs(formStore);
const cartStore = useCartStore(); const cartStore = useCartStore();
const { products } = storeToRefs(cartStore); const { products } = storeToRefs(cartStore);
const { getProducts } = cartStore; const { getProducts } = cartStore;
const monthTest = ref("");
const isOpenOrder = ref(false); const isOpenOrder = ref(false);
const availabilityStoraged = ref(getItem("availability"));
const isNotAllCategory = computed(() => {
return route.path.split("/")[2] !== "all";
});
const datePostalCode = ref({});
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 = { const orderText = {
"lowest-price": "menor precio", "lowest-price": "menor precio",
"highest-price": "mayor precio", "highest-price": "mayor precio",
@ -74,12 +70,32 @@ export default defineComponent({
plantas: "Floranet Plantas", plantas: "Floranet Plantas",
ramos: "Floranet Ramos", ramos: "Floranet Ramos",
}; };
const dateExpiredMonth = computed(
() =>
availability.value.dateExpired?.split("-")[1] ||
availabilityStoraged.value.dateExpired?.split("-")[1]
);
const dateExpiredDay = computed(
() =>
availability.value.dateExpired?.split("-")[2] ||
availabilityStoraged.value.dateExpired?.split("-")[2]
);
watch(availability, (newDate) => { const months = {
const [_day, month, _year] = newDate.date.split("/"); "01": "Enero",
monthTest.value = monthES[+month - 1]; "02": "Febrero",
console.log(monthTest.value); "03": "Marzo",
}); "04": "Abril",
"05": "Mayo",
"06": "Junio",
"07": "Julio",
"08": "Agosto",
"09": "Septiembre",
10: "Octubre",
11: "Noviembre",
12: "Diciembre",
};
const currentMonth = months[dateExpiredMonth.value];
watch( watch(
[() => route.path, () => sortProductFilters.value.order], [() => route.path, () => sortProductFilters.value.order],
@ -87,14 +103,29 @@ export default defineComponent({
const categoryPath = newPath.split("/")[2]; const categoryPath = newPath.split("/")[2];
sortProductFilters.value.category = categoryPath; sortProductFilters.value.category = categoryPath;
const params = { const params = {};
type: categoryObj[categoryPath],
}; if (categoryPath !== "all") {
params.type = categoryObj[categoryPath];
}
if (categoryPath === "all") {
params.dateExpired = availabilityStoraged.value.dateExpired;
params.postalCode = availabilityStoraged.value.postalCode;
}
const paramsObj = { const paramsObj = {
"lowest-price": () => (params.lowPrice = 1), "lowest-price": () => {
"highest-price": () => (params.bigPrice = 1), params.lowPrice = 1;
latest: () => (params.isNew = 1), },
recommended: () => (params.recommend = 1), "highest-price": () => {
params.bigPrice = 1;
},
latest: () => {
params.isNew = 1;
},
recommended: () => {
params.recommend = 1;
},
}; };
if (newOrder) { if (newOrder) {
paramsObj[newOrder](); paramsObj[newOrder]();
@ -104,19 +135,37 @@ export default defineComponent({
} }
); );
watch(
() => route.path,
() => {
datePostalCode.value = isNotAllCategory.value
? availability.value
: availabilityStoraged.value;
}
);
onBeforeMount(async () => { onBeforeMount(async () => {
const categoryPath = route.path.split("/")[2]; const categoryPath = route.path.split("/")[2];
await getProducts({ if (categoryPath !== "all") {
type: categoryObj[categoryPath], await getProducts({
}); type: categoryObj[categoryPath],
}); });
datePostalCode.value = availability.value;
onUpdated(() => { return;
console.groupCollapsed("%c Updated!", "color: green;"); }
console.log(sortProductFilters.value);
console.log(availability.value); await getProducts(availabilityStoraged.value);
console.groupEnd(); datePostalCode.value = availabilityStoraged.value;
if (isAvailabilityEmpty.value) {
await push("/");
quasarNotify({
message: "Debes seleccionar una fecha y código postal",
type: "warning",
});
}
}); });
function openOrderFilter() { function openOrderFilter() {
@ -128,12 +177,17 @@ export default defineComponent({
openOrderFilter, openOrderFilter,
openModal, openModal,
sortProductFilters, sortProductFilters,
availability,
isOpenOrder, isOpenOrder,
screenWidth, screenWidth,
modalStore, modalStore,
orderText, orderText,
products, products,
isNotAllCategory,
availabilityStoraged,
currentMonth,
dateExpiredDay,
datePostalCode,
availability,
}; };
}, },
}); });
@ -144,12 +198,10 @@ export default defineComponent({
<section class="products-section"> <section class="products-section">
<header class="products-section-header"> <header class="products-section-header">
<Container> <Container>
<div class="product-header-content"> <div class="product-header-content" v-if="isNotAllCategory">
<h3 class="product-header-title subtitle"> <h3 class="product-header-title subtitle">
{{ sortProductFilters.category }} para obsequiar {{ sortProductFilters.category }} para obsequiar
</h3> </h3>
<p class="product-header-paragraph">
</p>
</div> </div>
<div class="product-header-filters"> <div class="product-header-filters">
@ -158,11 +210,15 @@ export default defineComponent({
<p class="filter-paragraph availability"> <p class="filter-paragraph availability">
Disponibilidad para: Disponibilidad para:
<span <span
v-if="availability.date && availability.postalCode" v-if="
datePostalCode.dateExpired && datePostalCode.postalCode
"
class="green-text" class="green-text"
> >
25 Julio en {{ dateExpiredDay }} {{ currentMonth }} en
{{ availability.postalCode.replace("-", "") }}</span {{
availability.postalCode || datePostalCode.postalCode
}}</span
> >
</p> </p>
@ -232,8 +288,8 @@ export default defineComponent({
</Container> </Container>
</div> </div>
<footer class="products-section-footer"> <footer class="products-section-footer" v-if="isNotAllCategory">
<RouterLink class="btn rounded outlined" to="/"> <RouterLink class="btn rounded outlined" to="/categoria/all">
Ver todos los diseños <IconArrowCircleFilledRight /> Ver todos los diseños <IconArrowCircleFilledRight />
</RouterLink> </RouterLink>
</footer> </footer>

View File

@ -0,0 +1,28 @@
<script>
import Container from "src/components/ui/Container.vue";
import { useLocalStorage } from "src/hooks/useLocalStorage";
import { defineComponent } from "vue";
export default defineComponent({
name: "CheckoutErrorPage",
components: { Container },
setup() {
const { removeItem } = useLocalStorage();
removeItem("cart");
},
});
</script>
<template>
<q-page class="checkout-error-page error-message">
<container>
<h1>¡Uy! Algo ha ido mal durante el proceso de compra.</h1>
</container>
</q-page>
</template>
<style lang="scss" scoped>
.checkout-error-page {
}
</style>

View File

@ -20,7 +20,20 @@ export default defineComponent({
checkoutBlock, checkoutBlock,
cart, cart,
totalPrice, totalPrice,
formState: { errors, meta, onSubmit, submitLoading }, isError,
onError,
redsysData,
tooltip: {
postalCode: {
postalCodeRef,
postalCodeTooltip,
floatingStyles,
isHidden,
hideTooltip,
showTooltip,
},
},
formState: { errors, meta, onSubmit, isLoadingSubmit },
fields: { fields: {
name, name,
nameAttrs, nameAttrs,
@ -51,21 +64,31 @@ export default defineComponent({
terms, terms,
termsAttrs, termsAttrs,
}, },
phoneInputRef,
phoneSenderInputRef,
redsysFormRef,
} = useCheckoutForm(); } = useCheckoutForm();
onBeforeMount(() => { onBeforeMount(() => {
if (cart.length === 0) return push("/"); if (cart.length === 0) return push("/");
}); });
const isError = ref(false);
const onError = () => {
isError.value = true;
};
return { return {
handleClickStep, handleClickStep,
onSubmit, onSubmit,
onError, onError,
redsysData,
phoneInputRef,
phoneSenderInputRef,
redsysFormRef,
postalCodeRef,
postalCodeTooltip,
floatingStyles,
isHidden,
hideTooltip,
showTooltip,
checkoutBlock, checkoutBlock,
stepsFormated, stepsFormated,
@ -74,7 +97,7 @@ export default defineComponent({
stepList, stepList,
cart, cart,
step: ref(1), step: ref(1),
submitLoading, isLoadingSubmit,
successURL: ref(""), successURL: ref(""),
cancelURL: ref(""), cancelURL: ref(""),
meta, meta,
@ -117,21 +140,12 @@ export default defineComponent({
<template> <template>
<q-page class="checkout-page"> <q-page class="checkout-page">
<Container tag="section"> <Container tag="section">
<header class="header-title" :class="!checkoutBlock && 'success'"> <header class="header-title">
<h1 class="pege-title" v-if="checkoutBlock"> <h1 class="pege-title">¿A quién y dónde lo entregamos"</h1>
{{
checkoutBlock
? "¿A quién y dónde lo entregamos?"
: '"¡Muchas gracias Jerom!"'
}}
</h1>
<p class="page-subtitle checkout" v-if="checkoutBlock"> <p class="page-subtitle checkout">
{{ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
checkoutBlock eiusmod tempor incididunt ut labore et dolore magna aliqua.
? "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
: "¡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> </p>
</header> </header>
@ -175,337 +189,354 @@ export default defineComponent({
</div> </div>
</div> </div>
<template v-if="checkoutBlock"> <div class="checkout-content">
<div class="checkout-content"> <div class="checkout-form">
<div class="checkout-form"> <q-form method="post" id="checkout-form" @submit.prevent="onSubmit">
<q-form <div class="form-fields-container delivery">
method="post" <header class="checkout-header-form">
id="checkout-form" <h3>Instrucciones para la entrega</h3>
@submit.prevent="onSubmit" </header>
>
<div class="form-fields-container delivery">
<header class="checkout-header-form">
<h3>Instrucciones para la entrega</h3>
</header>
<div class="checkout-fields"> <div class="checkout-fields">
<div class="field-control field-input"> <div class="field-control field-input">
<q-input <q-input
placeholder="Nombre*" placeholder="Nombre*"
name="name" name="name"
type="text" type="text"
v-model="name" v-model="name"
v-bind:="nameAttrs" v-bind:="nameAttrs"
:error="!!errors.name" :error="!!errors.name"
:error-message="errors.name" :error-message="errors.name"
outlined outlined
/> />
</div> </div>
<div class="field-control field-input"> <div class="field-control field-input">
<q-input <q-input
placeholder="Apellidos*" placeholder="Apellidos*"
name="surname" name="surname"
type="text" type="text"
v-model="surname" v-model="surname"
v-bind:="surnameAttrs" v-bind:="surnameAttrs"
:error="!!errors.surname" :error="!!errors.surname"
:error-message="errors.surname" :error-message="errors.surname"
outlined outlined
/> />
</div> </div>
<div class="field-control field-input"> <div class="field-control field-input">
<q-input <q-input
placeholder="Dirección*" placeholder="Dirección*"
name="address" name="address"
type="text" type="text"
v-model="address" v-model="address"
v-bind:="addressAttrs" v-bind:="addressAttrs"
:error="!!errors.address" :error="!!errors.address"
:error-message="errors.address" :error-message="errors.address"
outlined outlined
/> />
</div> </div>
<div class="field-control field-input"> <div class="field-control field-input">
<q-input <q-input
placeholder="Código postal*" placeholder="Código postal*"
name="postalCode" name="postalCode"
type="text" type="text"
mask="#####" mask="#####"
v-model="postalCode" v-model="postalCode"
v-bind:="postalCodeAttrs" v-bind:="postalCodeAttrs"
:error="!!errors.postalCode" :error="!!errors.postalCode"
:error-message="errors.postalCode" :error-message="errors.postalCode"
outlined ref="postalCodeRef"
/> readonly
</div> outlined
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<template #after>
<q-btn
to="/"
:style="'--clr: #ffffff'"
class="btn custom-btn-input paragraph-sm"
label="EDITAR"
/>
</template>
</q-input>
<div class="field-control field-select"> <div
<q-select ref="postalCodeTooltip"
name="province" :style="[floatingStyles, '--clr: #117564']"
v-model="province" :class="['tooltip ', isHidden && 'hidden']"
v-bind:="provinceAttrs" @mouseenter="showTooltip"
:error="!!errors.province" @mouseleave="hideTooltip"
:error-message="errors.province" >
:options="provinceOptions" <p class="paragraph-sm">
option-value="code" No se puede editar este campo
option-label="name" <a
label="Provincia*" href="https://www.google.com/maps"
stack-label rel="noreferrer noopener"
map-options target="_blank"
emit-value class="paragraph-sm link"
outlined >
/> ¿No conoce su código postal?
</div> </a>
</p>
<div class="field-control field-select">
<q-input
placeholder="Ciudad*"
name="city"
type="text"
v-model="city"
v-bind:="cityAttrs"
:error="!!errors.city"
:error-message="errors.city"
outlined
/>
</div>
<div class="field-control field-input telephone">
<q-input
placeholder="Teléfono*"
name="phone"
type="text"
mask="(##) ##### ####"
v-model="phone"
v-bind:="phoneAttrs"
:error="!!errors.phone"
:error-message="errors.phone"
outlined
/>
</div> </div>
</div> </div>
</div>
<div class="form-fields-container sender"> <div class="field-control field-select">
<header class="checkout-header-form"> <q-select
<h3>Remitente</h3> name="province"
</header> v-model="province"
v-bind:="provinceAttrs"
:error="!!errors.province"
:error-message="errors.province"
:options="provinceOptions"
option-value="code"
option-label="name"
label="País*"
stack-label
map-options
emit-value
outlined
/>
</div>
<div class="checkout-fields"> <div class="field-control field-select">
<div class="field-control field-input"> <q-input
<q-input placeholder="Ciudad*"
placeholder="Nombre y apellidos o nombre de empresa" name="city"
name="senderName" type="text"
type="text" v-model="city"
v-model="senderName" v-bind:="cityAttrs"
v-bind:="senderNameAttrs" :error="!!errors.city"
:error="!!errors.senderName" :error-message="errors.city"
:error-message="errors.senderName" outlined
outlined />
/> </div>
</div>
<div class="field-control field-input"> <div class="field-control field-input telephone">
<q-input <vue-tel-input
placeholder="CIF / NIF" v-model="phone"
name="senderCifNif" v-bind="phoneAttrs"
type="text" :styleClasses="[
mask="#########" 'custom-input',
v-model="senderCifNif" !!errors.phone && 'error',
v-bind:="senderCifNifAttrs" ]"
:error="!!errors.senderCifNif" ref="phoneInputRef"
:error-message="errors.senderCifNif" :inputOptions="{
outlined placeholder: 'Teléfono*',
/> }"
</div> />
<p v-if="!!errors.phone" class="error">
<div class="field-control field-input"> {{ errors.phone }}
<q-input </p>
placeholder="Email"
name="senderEmail"
type="email"
v-model="senderEmail"
v-bind:="senderEmailAttrs"
:error="!!errors.senderEmail"
:error-message="errors.senderEmail"
outlined
/>
</div>
<div class="field-control field-input">
<q-input
placeholder="Teléfono"
name="senderPhone"
type="text"
mask="(##) ##### ####"
v-model="senderPhone"
v-bind:="senderPhoneAttrs"
:error="!!errors.senderPhone"
:error-message="errors.senderPhone"
outlined
/>
</div>
<div class="field-control field-input">
<q-input
placeholder="Notas sobre tu pedido (Opcional), por ejemplo, notas especiales para la entrega"
name="senderNotes"
type="textarea"
v-model="senderNotes"
v-bind:="senderNotesAttrs"
:error="!!errors.senderNotes"
:error-message="errors.senderNotes"
class="message"
autogrow
outlined
/>
</div>
</div> </div>
</div> </div>
</q-form> </div>
<div class="form-fields-container sender">
<header class="checkout-header-form">
<h3>Remitente</h3>
</header>
<div class="checkout-fields">
<div class="field-control field-input">
<q-input
placeholder="Nombre y apellidos o nombre de empresa"
name="senderName"
type="text"
v-model="senderName"
v-bind:="senderNameAttrs"
:error="!!errors.senderName"
:error-message="errors.senderName"
outlined
/>
</div>
<div class="field-control field-input">
<q-input
placeholder="CIF / NIF"
name="senderCifNif"
type="text"
v-model="senderCifNif"
v-bind:="senderCifNifAttrs"
:error="!!errors.senderCifNif"
:error-message="errors.senderCifNif"
outlined
/>
</div>
<div class="field-control field-input">
<q-input
placeholder="Email"
name="senderEmail"
type="email"
v-model="senderEmail"
v-bind:="senderEmailAttrs"
:error="!!errors.senderEmail"
:error-message="errors.senderEmail"
outlined
/>
</div>
<div class="field-control field-input telephone">
<vue-tel-input
v-model="senderPhone"
v-bind="senderPhoneAttrs"
:styleClasses="[
'custom-input',
!!errors.senderPhone && 'error',
]"
ref="phoneSenderInputRef"
:inputOptions="{
placeholder: 'Teléfono*',
}"
/>
<p v-if="!!errors.senderPhone" class="error">
{{ errors.senderPhone }}
</p>
</div>
<div class="field-control field-input">
<q-input
placeholder="Notas sobre tu pedido (Opcional), por ejemplo, notas especiales para la entrega"
name="senderNotes"
type="textarea"
v-model="senderNotes"
v-bind:="senderNotesAttrs"
:error="!!errors.senderNotes"
:error-message="errors.senderNotes"
class="message"
autogrow
outlined
/>
</div>
</div>
</div>
</q-form>
<form
v-if="paymentMethod === 'redsys' && meta.valid"
name="from"
action="https://sis-t.redsys.es:25443/sis/realizarPago"
method="POST"
class="hide"
>
<input
type="hidden"
name="Ds_SignatureVersion"
:value="redsysData.Ds_SignatureVersion"
/>
<input
type="hidden"
name="Ds_MerchantParameters"
:value="redsysData.Ds_MerchantParameters"
/>
<input
type="hidden"
name="Ds_Signature"
:value="redsysData.Ds_Signature"
/>
<input ref="redsysFormRef" type="submit" value="Go to pay" />
</form>
</div>
<aside class="checkout-aside">
<div class="checkout-delivery-date" :class="meta.valid && 'active'">
<header class="checkout-aside-header green-text">
<strong class="checkout-aside-title"> Fecha de entrega </strong>
</header>
<div class="checkout-delivery-body">
<p class="green-text">13 de julio - De 11h - 12 h</p>
</div>
</div> </div>
<aside class="checkout-aside"> <div class="checkout-summary">
<div <header class="checkout-aside-header gray-bg">
class="checkout-delivery-date" <strong class="checkout-aside-title">
:class="(meta.valid || !checkoutBlock) && 'active'" Resumen del pedido
> </strong>
<header class="checkout-aside-header green-text"> </header>
<strong class="checkout-aside-title">
Fecha de entrega
</strong>
</header>
<div class="checkout-delivery-body"> <div class="checkout-summary-body gray-bg">
<p class="green-text">13 de julio - De 11h - 12 h</p> <ul class="checkout-summary-list">
</div> <li
</div> class="checkout-summary-item"
v-for="({ name, price }, index) in cart"
<div class="checkout-summary"> :key="index"
<header class="checkout-aside-header gray-bg">
<strong class="checkout-aside-title">
Resumen del pedido
</strong>
</header>
<div class="checkout-summary-body gray-bg">
<ul class="checkout-summary-list">
<li
class="checkout-summary-item"
v-for="({ name, price }, index) in cart"
:key="index"
>
<p>
{{ name }}
<span>{{ price }}</span>
</p>
</li>
</ul>
<p class="green-text">Envio Gratuíto</p>
</div>
<footer class="checkout-summary-footer">
<p class="checkout-summary-paragraph">Total</p>
<p class="checkout-summary-paragraph summary-price">
{{ totalPrice }}
</p>
</footer>
</div>
<div class="checkout-payment-methods gray-bg">
<header class="checkout-aside-header">
<strong class="checkout-aside-title">Método de pago</strong>
</header>
<div class="checkout-payment-body">
<!-- <q-radio
v-model="paymentMethod"
v-bind="paymentMethodAttrs"
val="credit"
color="primary"
> >
<p> <p>
Tarjeta {{ name }}
<span class="card-flags"> <span>{{ price }}</span>
<IconMaster /><IconVisa /> <IconAny /> <IconExpress />
</span>
</p> </p>
</q-radio> -->
<q-radio
v-model="paymentMethod"
v-bind="paymentMethodAttrs"
val="stripe"
color="primary"
>
<p>Stripe <a href="#">¿Qué es Stripe?</a></p>
</q-radio>
</div>
</div>
<div class="checkout-terms">
<q-checkbox v-model="terms" v-bind="termsAttrs" class="terms">
<p :style="!!errors.terms && 'color: red;'">
He leído y estoy de acuerdo con los términosy condiciones de
la tienda Floranet
</p>
</q-checkbox>
<q-btn flat class="btn" type="submit" form="checkout-form">
PROCEDER AL PAGO
</q-btn>
</div>
</aside>
</div>
</template>
<template v-else>
<div class="checkout-success" id="success-block">
<h6 class="checkout-success-title green-text">
Has efectuado la siguiente compra
</h6>
<div class="checkout-success-body">
<div class="checkout-success-content">
<ul class="checkout-success-list">
<li
v-for="({ name, price, image }, index) in cart"
:key="index"
class="checkout-success-item"
>
<div class="checkout-item-content">
<div class="checkout-product-details">
<img
:src="isError ? '../assets/empty-img.jpg' : image"
:alt="name"
class="checkout-product-img"
@error="onError"
/>
<p class="checkout-product-title">
{{ name }}
</p>
</div>
<p class="checkout-product-price">{{ price }}</p>
</div>
</li> </li>
</ul> </ul>
<p class="green-text">Envio Gratuíto</p>
</div> </div>
<footer class="checkout-success-footer"> <footer class="checkout-summary-footer">
<p class="checkout-success-paragraph">Total</p> <p class="checkout-summary-paragraph">Total</p>
<p class="checkout-success-paragraph"> <p class="checkout-summary-paragraph summary-price">
{{ totalPrice?.toFixed(2) }} {{ totalPrice }}
</p> </p>
</footer> </footer>
</div> </div>
</div>
</template> <div class="checkout-payment-methods gray-bg">
<header class="checkout-aside-header">
<strong
class="checkout-aside-title"
:style="!!errors.paymentMethod && 'color: red'"
>
Método de pago
</strong>
<p v-if="!!errors.paymentMethod"></p>
</header>
<div class="checkout-payment-body">
<q-radio
v-model="paymentMethod"
v-bind="paymentMethodAttrs"
val="paypal"
color="primary"
>
<p>Paypal</p>
</q-radio>
<q-radio
v-model="paymentMethod"
v-bind="paymentMethodAttrs"
val="redsys"
color="primary"
>
<p>Redsys</p>
</q-radio>
</div>
</div>
<div class="checkout-terms">
<q-checkbox v-model="terms" v-bind="termsAttrs" class="terms">
<p :style="!!errors.terms && 'color: red;'">
He leído y estoy de acuerdo con los términosy condiciones de
la tienda Floranet
</p>
</q-checkbox>
<q-btn
flat
class="btn"
type="submit"
form="checkout-form"
:loading="isLoadingSubmit"
>
PROCEDER AL PAGO
</q-btn>
</div>
</aside>
</div>
</div> </div>
</Container> </Container>
</q-page> </q-page>
@ -517,10 +548,19 @@ export default defineComponent({
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@media only screen and (max-width: $med-sm) {
flex-wrap: wrap;
& .border-step {
opacity: 0;
visibility: hidden;
}
}
} }
& .step-item-container { & .step-item-container {
min-width: 200px; width: min(100%, 200px);
} }
& .border-step { & .border-step {
@ -927,5 +967,44 @@ export default defineComponent({
} }
} }
} }
& .custom-input {
padding: 10.5px 1px;
border-radius: 4px;
transition: 200ms ease-in-out;
&:hover {
border-color: $black;
}
&:focus-within {
border-color: $primary;
box-shadow: inset 0 0 0 1px $primary;
}
&.error {
border-color: $negative;
box-shadow: inset 0 0 0 1px $negative;
}
& .vti__input::placeholder {
font-family: $font-questrial;
font-size: $font-12;
}
}
& p.error {
font-family: $font-questrial;
color: $negative;
font-size: $font-12;
padding: 8px 12px 0;
}
& .custom-btn-input {
border-radius: 4px;
}
& .q-field__native {
font-family: "Roboto" !important;
}
} }
</style> </style>

View File

@ -0,0 +1,321 @@
<script>
import { storeToRefs } from "pinia";
import { apiBack } from "src/boot/axios";
import { useCheckoutForm } from "src/hooks/useCheckoutForm";
import { useLocalStorage } from "src/hooks/useLocalStorage";
import { useCartStore } from "src/stores/cart";
import { defineComponent, onBeforeMount, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
export default defineComponent({
name: "CheckoutSuccessPage",
setup() {
const { query } = useRoute();
const { push } = useRouter();
const { getItem, removeItem } = useLocalStorage();
const cartStore = useCartStore();
const { cart: cartStoreArr } = storeToRefs(cartStore);
const cart = getItem("cart");
const totalPrice = ref(0);
if (cart) {
totalPrice.value = cart.reduce((acc, { price }) => acc + +price, 0);
}
async function getSuccessData() {
if (!query.orderId) return push("/checkout/error");
try {
await new Promise(async (resolve, reject) => {
try {
const {
data: { data },
} = await apiBack.post("payment/success", {
...query,
});
resolve(data.products);
removeItem("costumer");
} catch (error) {
reject(error);
}
}).then((res) => res);
} catch (error) {
console.error(`FATAL ERROR ::: ${error}`);
push("/checkout/error");
}
}
onBeforeMount(async () => {
/* const queryObj = {
orderId: query.orderId,
productsIds: query.productsIds,
PayerID: query.PayerID,
};
for (const [_, value] of Object.entries(queryObj)) {
if (!value) return push("/");
} */
if (cart.length === 0) return push("/");
await getSuccessData();
});
const { isError, onError } = useCheckoutForm();
const steppers = [
{
value: 1,
name: "Paso 1",
description: "Datos de facturación",
active: true,
},
{
value: 2,
name: "Paso 2",
description: "Confirmación",
active: true,
},
{
value: 3,
name: "Paso 3",
description: "Pago",
active: true,
},
];
cartStoreArr.value = [];
setTimeout(() => {
removeItem("cart");
removeItem("payment");
}, 5000);
return { isError, onError, steppers, totalPrice, cart };
},
});
</script>
<template>
<q-page class="success-container">
<div class="checkout-steps">
<div
v-for="({ active, description, name, value }, i) in steppers"
class="step-item-container"
:key="i"
>
<div class="step-item">
<div class="circle-step-container">
<span class="border-step" :class="[i == 0 && 'transparent']" />
<div class="circle-step" :class="active && 'active'">
<span class="step-value">{{ value }}</span>
</div>
<span
class="border-step"
:class="[i == steppers.length - 1 && 'transparent']"
/>
</div>
<div class="step-content">
<div class="title">
<h4>{{ name }}</h4>
</div>
<div class="description">
<p>{{ description }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="checkout-success" id="success-block">
<h6 class="checkout-success-title green-text">
Has efectuado la siguiente compra
</h6>
<div class="checkout-success-body">
<div class="checkout-success-content">
<ul class="checkout-success-list">
<li
v-for="({ name, price, image }, index) in cart"
:key="index"
class="checkout-success-item"
>
<div class="checkout-item-content">
<div class="checkout-product-details">
<img
:src="isError ? '../assets/empty-img.jpg' : image"
:alt="name"
class="checkout-product-img"
@error="onError"
/>
<p class="checkout-product-title">
{{ name }}
</p>
</div>
<p class="checkout-product-price">{{ price }}</p>
</div>
</li>
</ul>
</div>
<footer class="checkout-success-footer">
<p class="checkout-success-paragraph">Total</p>
<p class="checkout-success-paragraph">{{ totalPrice.toFixed(2) }}</p>
</footer>
</div>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.success-container {
display: flex;
flex-direction: column;
margin-top: 50px;
}
.checkout-steps {
display: flex;
justify-content: center;
align-items: center;
}
.step-item-container {
min-width: 200px;
}
.border-step {
width: 90px;
height: 1px;
background-color: $primary-dark;
}
.circle-step-container {
display: grid;
justify-content: center;
align-items: center;
grid-template-columns: 1fr auto 1fr;
}
.circle-step {
width: 56px;
height: 56px;
border: 1px solid $primary-dark;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
user-select: none;
.step-value {
font-family: $font-questrial;
color: $primary-dark;
font-size: 1.25rem;
}
&.active {
background-color: $primary-dark;
.step-value {
color: $white;
}
}
}
.step-content {
display: flex;
flex-direction: column;
align-items: center;
font-family: $font-questrial;
h4 {
font-size: 1rem;
font-weight: 700;
color: $text-default;
margin-top: 5px;
margin-bottom: 4px;
line-height: 1.3;
}
p {
font-size: 0.875rem;
color: $text-default;
font-family: $font-lora;
}
}
.checkout-success {
width: min(100%, 499px);
margin: 122px auto 0;
text-align: center;
& .checkout-success-title {
margin-bottom: 26px;
}
& .checkout-success-body {
& .checkout-success-content {
background-color: $secondary-5;
padding: 30px 46px 42px 38px;
border-radius: 5px 5px 0px 0px;
& .checkout-success-list {
display: flex;
flex-direction: column;
gap: 28px;
& .checkout-success-item {
display: flex;
flex: 1;
& .checkout-item-content {
display: flex;
justify-content: space-between;
flex: 1;
min-height: 61px;
& .checkout-product-details {
display: flex;
gap: 14px;
& .checkout-product-img {
object-fit: cover;
width: 54px;
height: 100%;
border-radius: 5px;
}
& .checkout-product-title {
font-size: $font-12;
line-height: 21px;
letter-spacing: 0.24px;
font-family: $font-questrial;
color: $text-default;
}
}
& .checkout-product-price {
color: $text-muted-one;
font-family: $font-roboto;
font-size: $font-12;
line-height: 21px;
letter-spacing: 0.24px;
}
}
}
}
@media only screen and (max-width: $med-lg) {
padding-right: 9px;
}
}
& .checkout-success-footer {
display: flex;
justify-content: space-between;
background-color: $secondary-40;
border-radius: 0px 0px 5px 5px;
padding: 14px 46px 7px 36px;
& .checkout-success-paragraph {
font-family: $font-lora;
letter-spacing: 0.32px;
line-height: 21px;
font-weight: 600;
color: $text-muted-one;
}
}
}
}
</style>

View File

@ -1,21 +0,0 @@
<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,6 +1,6 @@
<script> <script>
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { defineComponent, onBeforeMount } from "vue"; import { defineComponent, onBeforeMount, ref } from "vue";
import VerticalCarouselImgs from "src/components/quasar-components/carousel/VerticalCarouselImgs.vue"; import VerticalCarouselImgs from "src/components/quasar-components/carousel/VerticalCarouselImgs.vue";
import Swiper from "src/components/swiper/Swiper.vue"; import Swiper from "src/components/swiper/Swiper.vue";
@ -30,13 +30,23 @@ export default defineComponent({
await getProducts(); await getProducts();
}); });
const slidesContent = [ const slidesContent = ref([]);
"assets/1.jpg",
"assets/2.jpg", onBeforeMount(async () => {
"assets/3.jpg", const images = await new Promise((resolve) => {
"assets/4.jpg", setTimeout(() => {
"assets/5.jpg", resolve([
]; "assets/1.jpg",
"assets/2.jpg",
"assets/3.jpg",
"assets/4.jpg",
"assets/5.jpg",
]);
}, 1000);
});
slidesContent.value = images;
});
return { return {
isCarouselVisible, isCarouselVisible,
@ -62,8 +72,7 @@ export default defineComponent({
Diseños de ramos más vendidos Diseños de ramos más vendidos
</h3> </h3>
<p class="products-header-paragraph section-paragraph"> <p class="products-header-paragraph section-paragraph"></p>
</p>
</header> </header>
<div class="products-body"> <div class="products-body">
@ -85,7 +94,7 @@ export default defineComponent({
</template> </template>
</Container> </Container>
<RouterLink class="btn rounded outlined" to="/"> <RouterLink class="btn rounded outlined" to="/categoria/all">
Ver todos los diseños <IconArrowCircleFilledRight /> Ver todos los diseños <IconArrowCircleFilledRight />
</RouterLink> </RouterLink>
</div> </div>
@ -97,8 +106,7 @@ export default defineComponent({
Nuestra selección de plantas para el verano Nuestra selección de plantas para el verano
</h3> </h3>
<p class="products-selection-paragraph section-paragraph"> <p class="products-selection-paragraph section-paragraph"></p>
</p>
</header> </header>
<div class="products-selection-body"> <div class="products-selection-body">

View File

@ -1,7 +1,7 @@
<script> <script>
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useMeta } from "quasar"; import { useMeta } from "quasar";
import { defineComponent, onBeforeMount, ref, watch } from "vue"; import { computed, defineComponent, onBeforeMount, ref, watch } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import IconArrowCircleFilledLeft from "components/icons/IconArrowCircleFilledLeft.vue"; import IconArrowCircleFilledLeft from "components/icons/IconArrowCircleFilledLeft.vue";
@ -18,8 +18,10 @@ import Card from "components/ui/Card.vue";
import Container from "components/ui/Container.vue"; import Container from "components/ui/Container.vue";
import Modal from "components/ui/Modal.vue"; import Modal from "components/ui/Modal.vue";
import { useLocalStorage } from "src/hooks/useLocalStorage";
import { usePostalCalendar } from "src/hooks/usePostalCalendar"; import { usePostalCalendar } from "src/hooks/usePostalCalendar";
import { useCartStore } from "stores/cart"; import { useCartStore } from "stores/cart";
import { useFormStore } from "stores/forms";
import { useModalStore } from "stores/modalStore"; import { useModalStore } from "stores/modalStore";
export default defineComponent({ export default defineComponent({
@ -41,28 +43,48 @@ export default defineComponent({
}, },
setup() { setup() {
const route = useRoute(); const route = useRoute();
const { getItem } = useLocalStorage();
const formStore = useFormStore();
const { availability: availabilityForm } = storeToRefs(formStore);
const availability = ref(getItem("availability"));
const availabilityFormKeys = computed(() => {
return Object.fromEntries(
Object.entries(availabilityForm.value).filter(
([key, value]) => value !== ""
)
);
});
const isAvailabilityEmpty = computed(() => {
return (
Object.keys(availabilityFormKeys.value || availability.value).length ===
0
);
});
const { const {
handleReset,
fields: { dedication, dedicationAttrs }, fields: { dedication, dedicationAttrs },
} = usePostalCalendar({ modalItem: "isOpenAvailability" }); } = usePostalCalendar({ modalItem: "isOpenAvailability", type: "product" });
const modalStore = useModalStore(); const modalStore = useModalStore();
const { openModal } = modalStore; const { openModal } = modalStore;
const cartStore = useCartStore(); const cartStore = useCartStore();
const { getProduct, getProducts } = cartStore; const { getProduct, getProducts, addToCart } = cartStore;
const { products, featuredProducts, addCartLoadingBtn } = const { products, addCartLoadingBtn } = storeToRefs(cartStore);
storeToRefs(cartStore);
onBeforeMount(() => { onBeforeMount(async () => {
getProduct(route.params.id); await getProduct(route.params.id);
}); });
watch( watch(
() => products.value.current?.type, () => products.value.current?.type,
(newCategory) => { (newCategory) => {
getProducts({ getProducts({
// type: newCategory, type: newCategory,
}); });
} }
); );
@ -74,33 +96,34 @@ export default defineComponent({
} }
); );
useMeta(() => ({ useMeta(() => {
title: `${products.value.current?.title}`, return {
titleTemplate: (title) => `${title} - FloraNet`, title: "FloraNet",
meta: { meta: {
description: { description: {
name: "description", name: "description",
content: `${products.value.current?.description}`, content: `${products.value.current?.description}`,
}, },
keywords: { keywords: {
name: "keywords", name: "keywords",
content: `${products.value.current?.title}`, content: `${products.value.current?.title}`,
}, },
equiv: { equiv: {
"http-equiv": "Content-Type", "http-equiv": "Content-Type",
content: "text/html; charset=UTF-8", content: "text/html; charset=UTF-8",
}, },
ogTitle: { ogTitle: {
property: "og:title", property: "og:title",
template(ogTitle) { template(ogTitle) {
return `${ogTitle} - FloraNet`; return `${ogTitle} - FloraNet`;
},
},
noscript: {
default: "This is content for browsers with no JS (or disabled JS)",
}, },
}, },
noscript: { };
default: "This is content for browsers with no JS (or disabled JS)", });
},
},
}));
const checkImageValidity = (imageLink) => { const checkImageValidity = (imageLink) => {
const validExtensions = [".jpg", ".jpeg", ".png"]; const validExtensions = [".jpg", ".jpeg", ".png"];
@ -113,15 +136,28 @@ export default defineComponent({
return true; return true;
}; };
const addModal = () => {
if (!isAvailabilityEmpty.value) {
addToCart(products.value.current, dedication);
return;
}
openModal({ modal: "availability" });
};
const handlePagClick = () => {
handleReset();
};
return { return {
addModal,
openModal, openModal,
handlePagClick,
checkImageValidity, checkImageValidity,
slide: ref(1), slide: ref(1),
fullscreen: ref(false), fullscreen: ref(false),
dedication, dedication,
dedicationAttrs, dedicationAttrs,
products, products,
featuredProducts,
addCartLoadingBtn, addCartLoadingBtn,
}; };
}, },
@ -156,9 +192,9 @@ export default defineComponent({
<span class="green-text" style="display: inline-flex"> <span class="green-text" style="display: inline-flex">
{{ products.current?.id }} {{ products.current?.id }}
<q-skeleton <q-skeleton
v-if="!products.current?.id"
width="100px" width="100px"
type="text" type="text"
v-if="!products.current?.id"
/> />
</span> </span>
</p> </p>
@ -168,9 +204,9 @@ export default defineComponent({
<span class="green-text"> <span class="green-text">
{{ products.current?.type }} {{ products.current?.type }}
<q-skeleton <q-skeleton
v-if="!products.current?.type"
type="text" type="text"
width="50px" width="50px"
v-if="!products.current?.type"
/> />
</span> </span>
</p> </p>
@ -182,10 +218,10 @@ export default defineComponent({
<p class="product-price green-text"> <p class="product-price green-text">
{{ products.current?.price }} {{ products.current?.price }}
<q-skeleton <q-skeleton
v-if="!products.current?.price"
type="text" type="text"
height="90px" height="90px"
width="80px" width="80px"
v-if="!products.current?.price"
/> />
</p> </p>
<p class="product-delivery green-text">Envío Gratuito</p> <p class="product-delivery green-text">Envío Gratuito</p>
@ -226,8 +262,8 @@ export default defineComponent({
:loading="addCartLoadingBtn" :loading="addCartLoadingBtn"
color="primary" color="primary"
class="btn sm-btn" class="btn sm-btn"
label="AÑADIR AL CARRITO" label="COMPRAR"
@click="openModal({ modal: 'availability' })" @click="addModal"
/> />
</div> </div>
</div> </div>
@ -254,7 +290,7 @@ export default defineComponent({
color="white" color="white"
class="btn outlined rounded sm-btn product-pag-item product-prev-btn" class="btn outlined rounded sm-btn product-pag-item product-prev-btn"
:to="`${+$route.params.id - 1}`" :to="`${+$route.params.id - 1}`"
@click="products.current.value = undefined" @click="handlePagClick"
> >
<IconArrowCircleFilledLeft /> <IconArrowCircleFilledLeft />
@ -271,6 +307,7 @@ export default defineComponent({
color="white" color="white"
class="btn outlined rounded sm-btn product-pag-item product-next-btn" class="btn outlined rounded sm-btn product-pag-item product-next-btn"
:to="`${+$route.params.id + 1}`" :to="`${+$route.params.id + 1}`"
@click="handlePagClick"
> >
<div class="btn-pag-paragraphs"> <div class="btn-pag-paragraphs">
<p class="btn-paragraph-top green-text">Siguiente producto</p> <p class="btn-paragraph-top green-text">Siguiente producto</p>
@ -293,8 +330,7 @@ export default defineComponent({
Quizás también te gusten estos ramos Quizás también te gusten estos ramos
</h3> </h3>
<p class="like-another-paragraph"> <p class="like-another-paragraph"></p>
</p>
</header> </header>
<Container cardContainer class="no-padding"> <Container cardContainer class="no-padding">
@ -361,6 +397,7 @@ export default defineComponent({
height: 396px; height: 396px;
& .q-carousel__navigation { & .q-carousel__navigation {
bottom: -83px; bottom: -83px;
display: block;
& .q-carousel__navigation-inner { & .q-carousel__navigation-inner {
gap: 12px; gap: 12px;
& .q-carousel__thumbnail { & .q-carousel__thumbnail {

View File

@ -26,6 +26,11 @@ const routes = [
name: "Plantas", name: "Plantas",
component: () => import("pages/CategoryPage.vue"), component: () => import("pages/CategoryPage.vue"),
}, },
{
path: "all",
name: "All",
component: () => import("pages/CategoryPage.vue"),
},
], ],
}, },
{ {
@ -37,7 +42,17 @@ const routes = [
path: "", path: "",
name: "Checkout", name: "Checkout",
component: () => import("pages/CheckoutPage.vue"), component: () => import("pages/CheckoutPage.vue"),
} },
{
path: "success",
name: "CheckoutSuccess",
component: () => import("pages/CheckoutSuccessPage.vue"),
},
{
path: "error",
name: "CheckoutError",
component: () => import("pages/CheckoutErrorPage.vue"),
},
], ],
}, },
{ {
@ -62,11 +77,7 @@ const routes = [
name: "Contacta", name: "Contacta",
component: () => import("pages/ContactaPage.vue"), component: () => import("pages/ContactaPage.vue"),
}, },
{
path: "/example",
name: "Example",
component: () => import("pages/ExamplePage.vue"),
},
{ {
path: "/:catchAll(.*)*", path: "/:catchAll(.*)*",
name: "NotFound", name: "NotFound",

View File

@ -1,39 +1,42 @@
import { defineStore } from "pinia"; import { defineStore, storeToRefs } from "pinia";
import { ref } from "vue"; import { computed, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { apiBack } from "src/boot/axios"; import { apiBack } from "src/boot/axios";
import { quasarNotify } from "src/functions/quasarNotify"; import { quasarNotify } from "src/functions/quasarNotify";
import { useLocalStorage } from "src/hooks/useLocalStorage"; import { useLocalStorage } from "src/hooks/useLocalStorage";
import { useFormStore } from "./forms";
export const useCartStore = defineStore("cart", () => { export const useCartStore = defineStore("cart", () => {
const { push } = useRouter(); const { push } = useRouter();
const { addItem, getItem, removeItem } = useLocalStorage() const { addItem, getItem } = useLocalStorage();
const formStore = useFormStore();
const { availability: availabilityForm } = storeToRefs(formStore);
//! Elements //! Elements
const checkoutRef = ref(null); const checkoutRef = ref(null);
const homeSection = ref(null); const homeSection = ref(null);
const initialValues = [{ const initialValues = [
id: null, {
name: "", id: null,
price: null, name: "",
image: "", price: null,
description: "", image: "",
dateExpired: "", description: "",
isNew: null, dateExpired: "",
type: "", isNew: null,
postalCode: "", type: "",
order_position: null, postalCode: "",
recommend: null order_position: null,
}] recommend: null,
},
];
//! Variables //! Variables
const cart = ref([]); const cart = ref(getItem("cart"));
const availability = ref(getItem("availability"));
(() => {
cart.value = getItem('cart');
})()
const addCartLoadingBtn = ref(false); const addCartLoadingBtn = ref(false);
const routeId = ref(null); const routeId = ref(null);
@ -43,44 +46,8 @@ export const useCartStore = defineStore("cart", () => {
current: initialValues, current: initialValues,
next: initialValues, next: initialValues,
}); });
const featuredProducts = ref({
page: undefined,
productsPerPage: undefined,
products: [],
});
/** function transformOptionsToParams(options = {}) {
* Transforms options object into params object.
*
* @param {Object} options - The options object.
* @param {number} options.itens - The items array.
* @param {boolean} options.featured - The featured flag.
* @param {number} options.page - The page number.
* @param {string} options.type - The type name.
* @param {string} options.postalCode - The postal code.
* @param {string} options.dateExpired - The expiration date.
* @param {number} options.minPrice - The minimum price.
* @param {number} options.maxPrice - The maximum price.
* @param {number} options.bigPrice - The big price.
* @param {number} options.lowPrice - The low price.
* @param {boolean} options.isNew - The new flag.
* @returns {Object} - The params object.
*/
function transformOptionsToParams(
options = {
postalCode: undefined,
dateExpired: undefined,
type: undefined,
minPrice: undefined,
maxPrice: undefined,
bigPrice: undefined,
lowPrice: undefined,
isNew: undefined,
order_crescent: undefined,
order_descending: undefined,
recommend: undefined,
}
) {
const optionsObj = { const optionsObj = {
postalCode: options.postalCode, postalCode: options.postalCode,
dateExpired: options.dateExpired, dateExpired: options.dateExpired,
@ -105,24 +72,7 @@ export const useCartStore = defineStore("cart", () => {
return params; return params;
} }
/** const isEmpty = ref(false);
* Fetches products based on the provided options.
*
* @param {Object} options - The options for fetching products.
* @param {number} options.itens - The items to fetch.
* @param {boolean} options.featured - Whether to fetch only featured products.
* @param {number} options.page - The page number to fetch.
* @param {string} options.type - The type of products to fetch.
* @param {string} options.postalCode - The postal code for filtering products.
* @param {string} options.dateExpired - The expiration date for filtering products.
* @param {number} options.minPrice - The minimum price for filtering products.
* @param {number} options.maxPrice - The maximum price for filtering products.
* @param {number} options.bigPrice - The big price for filtering products.
* @param {number} options.lowPrice - The low price for filtering products.
* @param {boolean} options.isNew - Whether to fetch only new products.
* @param {Function} navigate - The navigation function to call after fetching products.
* @returns {Promise<void>} - A promise that resolves when the products are fetched.
*/
async function getProducts( async function getProducts(
options = { options = {
postalCode: undefined, postalCode: undefined,
@ -137,16 +87,17 @@ export const useCartStore = defineStore("cart", () => {
order_descending: undefined, order_descending: undefined,
recommend: undefined, recommend: undefined,
}, },
scrollIntoView = () => { } callback
) { ) {
const params = transformOptionsToParams(options); const params = transformOptionsToParams(options);
console.log(params);
try { try {
const { data: { data } } = await apiBack.get("products", { params }); const {
data: { data },
} = await apiBack.get("products", { params });
if (data.length === 0) { if (data.length === 0) {
isEmpty.value = true;
return quasarNotify({ return quasarNotify({
message: message:
"No hay productos disponibles para la fecha y el código postal seleccionados", "No hay productos disponibles para la fecha y el código postal seleccionados",
@ -154,10 +105,11 @@ export const useCartStore = defineStore("cart", () => {
}); });
} }
isEmpty.value = false;
products.value.data = data; products.value.data = data;
if (scrollIntoView) { if (callback) {
scrollIntoView(); callback();
} }
console.groupCollapsed("%c PRODUCTS FETCHED!", "color: green;"); console.groupCollapsed("%c PRODUCTS FETCHED!", "color: green;");
@ -182,17 +134,6 @@ export const useCartStore = defineStore("cart", () => {
} }
} }
/**
* Fetches a product by its ID and updates the cart state.
*
* @param {string} id - The ID of the product to fetch.
* @param {object} options - Additional options for the product fetch.
* @param {string} options.type - The type of the product.
* @param {string} options.postalCode - The postal code for location-based filtering.
* @param {string} options.dateExpired - The expiration date for time-based filtering.
* @param {boolean} debug - Flag indicating whether to enable debug mode.
* @returns {Promise<void>} - A promise that resolves when the product is fetched and the cart state is updated.
*/
async function getProduct( async function getProduct(
id, id,
options = { options = {
@ -220,14 +161,14 @@ export const useCartStore = defineStore("cart", () => {
}; };
return result[res.status].data[0]; return result[res.status].data[0];
}) });
products.value.prev = prev; products.value.prev = prev;
products.value.current = current; products.value.current = current;
products.value.next = next; products.value.next = next;
if (!current) { if (!current) {
push({ name: "NotFound" }) push({ name: "NotFound" });
} }
if (debug) { if (debug) {
@ -250,92 +191,51 @@ export const useCartStore = defineStore("cart", () => {
} }
} }
/** async function addToCart(product, message) {
* Retrieves featured products based on the provided options. const params = transformOptionsToParams(
* availabilityForm.value || availability.value
* @param {Object} options - The options for retrieving featured products. );
* @param {number} options.itens - The number of items to retrieve. await getProducts(params);
* @param {number} options.featured - The flag indicating if the products should be featured.
* @param {number} [options.page] - The page number for pagination.
* @param {string} [options.type] - The type of the products.
* @param {string} [options.postalCode] - The postal code for location-based filtering.
* @param {string} [options.dateExpired] - The expiration date for filtering.
* @param {number} [options.minPrice] - The minimum price for filtering.
* @param {number} [options.maxPrice] - The maximum price for filtering.
* @param {number} [options.bigPrice] - The big price for filtering.
* @param {number} [options.lowPrice] - The low price for filtering.
* @param {boolean} [options.isNew] - The flag indicating if the products are new.
* @returns {Promise<void>} - A promise that resolves when the featured products are retrieved.
*/
async function getFeaturedProducts(
options = {
postalCode: undefined,
dateExpired: undefined,
type: undefined,
minPrice: undefined,
maxPrice: undefined,
bigPrice: undefined,
lowPrice: undefined,
isNew: undefined,
order_crescent: undefined,
order_descending: undefined,
recommend: 1,
},
debug = false
) {
try {
const params = transformOptionsToParams(options);
(async () => { const hasCurrentProduct = computed(() => {
const { return cart.value.find((p) => p.id === product.id);
data: { data },
} = await apiBack.get("products", { params });
featuredProducts.value = data[0];
if (debug) {
console.groupCollapsed(
"%c FEATURED PRODUCTS FETCHED!",
"color: green;"
);
console.table(data.products);
console.groupEnd();
}
})();
} catch (err) {
new Error(`FATAL ERROR ::: ${err}`);
}
}
/**
* Adiciona um produto ao carrinho.
* @param {Object} product - O produto a ser adicionado.
* @param {string} dedication - A dedicação associada ao produto.
*/
function addToCart(product, dedication) {
const existingProduct = cart.value.find(p => p.id === product.id);
console.log(existingProduct)
if (!existingProduct) {
const arr = [...cart.value];
arr.push(product);
console.log(arr)
addItem("cart", JSON.stringify(arr));
quasarNotify({ message: 'Producto añadido al carrito.', type: 'success' })
return
}
quasarNotify({
message: "Este producto ya está en el carrito",
type: "info",
}); });
}
/** if (isEmpty.value) {
* Remove an item from the cart by its ID. push("/");
* @param {number} id - The ID of the item to be removed. return quasarNotify({
*/ message:
function removeFromCart(id) { "No hay productos disponibles para la fecha y el código postal seleccionados",
const newArrRemovedItem = cart.value.filter((p) => id !== p.id); type: "erro",
addItem("cart", JSON.stringify(newArrRemovedItem)) });
}
if (!products.value.data.some((item) => item.id === product.id)) {
push("/");
return quasarNotify({
message:
"Este producto no está disponible en su zona, intente añadir un nuevo código postal",
type: "erro",
});
}
if (hasCurrentProduct.value) {
return quasarNotify({
message: "Este producto ya está en el carrito",
type: "info",
});
}
const arr = [...cart.value];
arr.push({ ...product, message: message.value });
cart.value = arr;
addItem("cart", arr);
await push("/checkout");
quasarNotify({
message: "Producto añadido al carrito.",
type: "success",
});
} }
return { return {
@ -345,12 +245,9 @@ export const useCartStore = defineStore("cart", () => {
cart, cart,
addCartLoadingBtn, addCartLoadingBtn,
products, products,
featuredProducts,
getFeaturedProducts,
getProducts, getProducts,
addToCart, addToCart,
removeFromCart,
getProduct, getProduct,
}; };
}); });

View File

@ -2,6 +2,10 @@ import { defineStore } from "pinia";
export const useFormStore = defineStore("forms", { export const useFormStore = defineStore("forms", {
state: () => ({ state: () => ({
postalCodeValid: {
isValid: false,
dataOptions: [],
},
sortProductFilters: { sortProductFilters: {
isOpenOrderFilter: false, isOpenOrderFilter: false,
order: undefined, order: undefined,
@ -18,7 +22,7 @@ export const useFormStore = defineStore("forms", {
terms: false, terms: false,
}, },
availability: { availability: {
date: "", dateExpired: "",
postalCode: "", postalCode: "",
}, },
checkout: { checkout: {
@ -34,28 +38,23 @@ export const useFormStore = defineStore("forms", {
senderEmail: "", senderEmail: "",
senderPhone: "", senderPhone: "",
senderNotes: "", senderNotes: "",
paymentMethod: "credit", paymentMethod: "paypal",
terms: false, terms: false,
}, },
}), }),
actions: { actions: {
handleQuestionData(values) { handleQuestionData(values) {
console.log(values);
this.question = values; this.question = values;
}, },
handleAvailabilityData(values) { handleAvailabilityData(values) {
console.log(values);
this.availability = values; this.availability = values;
}, },
registerAvailability() { registerAvailability() {},
console.log(this.availability);
},
handleCheckoutData(values) { handleCheckoutData(values) {
// console.log(values);
this.checkout = values; this.checkout = values;
}, },
}, },

View File

@ -23,8 +23,7 @@ export const useModalStore = defineStore("modal", () => {
isOpenAvailability: () => "Contenido modal availability", isOpenAvailability: () => "Contenido modal availability",
isOpenFilters: () => "Contenido modal filters", isOpenFilters: () => "Contenido modal filters",
}; };
console.log(availability.value); isModal[content]()
console.log(isModal[content]());
} }
return { openModal, handleSubmit, isOpenAvailability, isOpenFilters }; return { openModal, handleSubmit, isOpenAvailability, isOpenFilters };

View File

@ -8,7 +8,6 @@ export const useRangePriceStore = defineStore("range-price", () => {
}); });
function handlePriceRange({ min, max }) { function handlePriceRange({ min, max }) {
console.log({ min, max });
rangeValue.min = min; rangeValue.min = min;
rangeValue.max = max; rangeValue.max = max;
} }

View File

@ -1,7 +1,31 @@
export function handlePhoneVal(val) { import { countryFlagObj } from "src/constants";
const regex = /[\(\) ]/g;
const valWithoutSpaceAndParenteses = val.replace(regex, "");
const valLength = valWithoutSpaceAndParenteses.length;
return valLength > 0 && valLength === 11; import * as R from "./regex";
export function getCountryCode(countryNumber) {
return (
countryFlagObj[countryNumber] || countryFlagObj["default"](countryNumber)
);
}
export function handleValidCountryCode(countryNumber) {
const validCountryCodes = ["34", "351", "33"];
const isCountryCodeValid = validCountryCodes.includes(countryNumber);
return isCountryCodeValid;
}
export function transformPhoneVal(val) {
const obj = {
length: val.replace(R.selectSpaceAndSum, "").length,
val: val.replace(R.selectParenthesesSpacesDashes, ""),
};
return obj;
}
export function handlePhoneVal(input) {
const { length, val } = input;
return length === 0;
} }

View File

@ -1,9 +1,13 @@
export const nameMessage = export const nameMessage =
"Sólo se aceptan una palabra y caracteres no numéricos"; "Sólo se aceptan una palabra y caracteres no numéricos";
export const phoneMessage = export const phoneMessage =
"El número de teléfono debe contener 11 caracteres numéricos válidos"; "El teléfono no es válido, por favor, introduzca un número de teléfono válido.";
export const onlyMinimumTwoCharacters = "Añade al menos dos caracteres"; export const onlyMinimumTwoCharacters = "Añade al menos dos caracteres";
export const onlyTextMessage = "Sólo son válidas las letras"; export const onlyTextMessage = "Sólo son válidas las letras";
export const requiredMessage = "Campo obligatorio"; export const requiredMessage = "Campo obligatorio";
export const emailMessage = export const emailMessage =
"Introduzca una dirección de correo electrónico válida."; "Introduzca una dirección de correo electrónico válida.";
export const onlyTextAndNumbersMessage =
"Sólo son válidas las letras y números";
export const onlyNumbers = "¡Sólo se aceptan números!";
export const fiveLength = "El código postal debe tener 5 dígitos";

View File

@ -1,2 +1,7 @@
export const justOneWord = /^[A-Za-z]+$/; export const justOneWord = /^[A-Za-z\u00C0-\u00FF]+$/;
export const justLetters = /^[A-Za-z ]+$/; export const justLetters = /^[A-Za-z\u00C0-\u00FF ]+$/;
export const justLettersAndNumbers = /^[A-Za-z0-9]+$/;
export const justNumbers = /^[0-9 ]+$/;
export const selectSpaceAndSum = /\s|\+/g;
export const selectValuesWithSum = /\+\d+/g;
export const selectParenthesesSpacesDashes = /[\s()-]/g;

View File

@ -1,34 +1,36 @@
import { z } from "zod"; import { z } from "zod";
import { handlePhoneVal, justLetters, justOneWord, postalCode } from ".."; import * as GP from "../globalProperties";
import * as M from "../messages"; import * as M from "../messages";
import * as R from "../regex";
const checkoutObjVal = { const checkoutObjVal = {
name: z name: z
.string({ required_error: M.requiredMessage }) .string({ required_error: M.requiredMessage })
.regex(justOneWord, M.nameMessage), .regex(R.justOneWord, M.nameMessage),
surname: z surname: z
.string({ required_error: M.requiredMessage }) .string({ required_error: M.requiredMessage })
.regex(justOneWord, M.nameMessage), .regex(R.justOneWord, M.nameMessage),
address: z.string({ required_error: M.requiredMessage }), address: z.string({ required_error: M.requiredMessage }),
postalCode, postalCode: GP.postalCode,
city: z city: z
.string({ required_error: M.requiredMessage }) .string({ required_error: M.requiredMessage })
.min(2, M.onlyMinimumTwoCharacters) .min(2, M.onlyMinimumTwoCharacters)
.regex(justLetters, M.onlyTextMessage), .regex(R.justLetters, M.onlyTextMessage),
province: z.string({ required_error: M.requiredMessage }), province: z.string({ required_error: M.requiredMessage }),
phone: z phone: z.string({ required_error: M.requiredMessage }).refine((val) => {
.string({ required_error: M.requiredMessage }) return val.length > 0;
.refine(handlePhoneVal, M.phoneMessage), }, M.requiredMessage),
senderName: z.string().regex(justOneWord, M.nameMessage), senderName: z.string().regex(R.justLetters),
senderCifNif: z senderCifNif: z
.string() .string()
.length(9, "El código postal debe tener 9 caracteres numéricos válidos"), .regex(R.justLettersAndNumbers, M.onlyTextAndNumbersMessage),
senderEmail: z.string().email(M.emailMessage), senderEmail: z.string().email(M.emailMessage),
senderPhone: z.string().refine(handlePhoneVal, M.phoneMessage), senderPhone: z.string().refine((val) => {
return val.length >= 0;
}, M.phoneMessage),
senderNotes: z.string(), senderNotes: z.string(),
paymentMethod: z.enum(["credit", "stripe"], { paymentMethod: z.enum(["redsys", "paypal"], {
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) => { terms: z.boolean().refine((val) => {

View File

@ -1,7 +1,7 @@
import { toTypedSchema } from "@vee-validate/zod"; import { toTypedSchema } from "@vee-validate/zod";
import { z } from "zod"; import { z } from "zod";
import { handlePhoneVal, justOneWord } from ".."; import { justOneWord } from "..";
import * as M from "../messages"; import * as M from "../messages";
const questionObjVal = { const questionObjVal = {
@ -14,9 +14,13 @@ const questionObjVal = {
email: z.string({ required_error: M.requiredMessage }).email(M.emailMessage), email: z.string({ required_error: M.requiredMessage }).email(M.emailMessage),
phone: z phone: z
.string({ required_error: M.requiredMessage }) .string({ required_error: M.requiredMessage })
.refine(handlePhoneVal, M.phoneMessage), .min(1, M.requiredMessage),
query: z.string({ required_error: M.requiredMessage }).min(1), query: z
message: z.string({ required_error: M.requiredMessage }).min(1), .string({ required_error: M.requiredMessage })
.min(1, M.requiredMessage),
message: z
.string({ required_error: M.requiredMessage })
.min(1, M.requiredMessage),
terms: z.boolean({ required_error: M.requiredMessage }).refine((val) => { terms: z.boolean({ required_error: M.requiredMessage }).refine((val) => {
return val === true; return val === true;
}), }),

View File

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