Cambios semana 19-12

This commit is contained in:
Jaume Solís 2023-12-27 10:56:23 +01:00
parent 0f5b2627ec
commit a7143e3048
35 changed files with 1530 additions and 442 deletions

61
package-lock.json generated
View File

@ -14,7 +14,6 @@
"axios": "^1.2.1",
"pinia": "^2.0.11",
"quasar": "^2.6.0",
"swiper": "^11.0.5",
"v-mask": "^2.3.0",
"vee-validate": "^4.12.2",
"vue": "^3.0.0",
@ -23,6 +22,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@babel/types": "^7.23.6",
"@faker-js/faker": "^8.3.1",
"@quasar/app-vite": "^1.3.0",
"@types/node": "^12.20.21",
@ -51,6 +51,24 @@
"node": ">=0.10.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
@ -62,6 +80,20 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@ -5287,24 +5319,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swiper": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.0.5.tgz",
"integrity": "sha512-rhCwupqSyRnWrtNzWzemnBLMoyYuoDgGgspAm/8iBD3jCvAWycPLH4Z3TB0O5520DHLzMx94yUMH/B9Efpa48w==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"engines": {
"node": ">= 4.7.0"
}
},
"node_modules/table": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
@ -5383,6 +5397,15 @@
"node": ">=0.6.0"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View File

@ -11,7 +11,8 @@
"dev": "quasar dev -m ssr",
"backend": "json-server -p 5000 -d 3000 -w src/services/json-server/db.json",
"build": "quasar build -m ssr",
"start:build": "npm run build && cd dist/ssr && npm i && npm run start"
"start:build": "npm run build && cd dist/ssr && npm i && npm run start",
"typecheck": "tsc --project tsconfig.json --noEmit"
},
"dependencies": {
"@quasar/extras": "^1.16.4",
@ -20,7 +21,6 @@
"axios": "^1.2.1",
"pinia": "^2.0.11",
"quasar": "^2.6.0",
"swiper": "^11.0.5",
"v-mask": "^2.3.0",
"vee-validate": "^4.12.2",
"vue": "^3.0.0",
@ -29,6 +29,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@babel/types": "^7.23.6",
"@faker-js/faker": "^8.3.1",
"@quasar/app-vite": "^1.3.0",
"@types/node": "^12.20.21",

View File

@ -50,9 +50,9 @@
import { toTypedSchema } from '@vee-validate/zod';
import { storeToRefs } from 'pinia';
import { useFormStore } from 'src/stores/forms';
import { availabilitySchema } from 'src/utils/zod/schemas/availabilitySchema';
import { useForm } from 'vee-validate';
import { defineComponent, ref } from 'vue';
import { z } from 'zod';
import IconCalendar from '../icons/IconCalendar.vue';
export default defineComponent({
@ -70,34 +70,7 @@ export default defineComponent({
const proxyDate = ref(fullCurrentDate);
const validationSchema = toTypedSchema(
z.object({
date: z.string().refine((val) => {
const [day, month, year] = val.split('/');
console.log({ day, month, year });
const regex = /\//g;
const valWithoutSlash = val.replace(regex, '');
/* const daysOnMonth = (month: number, year: number) => {
const data = new Date(year, month, 0);
return data.getDate();
};
const daysOnMonthValue = daysOnMonth(+month, +year); */
const data = new Date(`${year}-${month}-${day}`);
const today = new Date();
return (
valWithoutSlash.length === 8 &&
/* +year >= currentYear &&
+month >= currentMonth &&
+day >= +currentDay && */
data >= today /* &&
+month > 0 &&
+month <= 12 &&
+day > 0 &&
+day <= daysOnMonthValue */
);
}, 'La fecha no puede ser inferior al día de hoy!'),
})
availabilitySchema.pick({ date: true })
);
const { errors, defineField } = useForm({
validationSchema,

View File

@ -26,9 +26,9 @@
import { toTypedSchema } from '@vee-validate/zod';
import { storeToRefs } from 'pinia';
import { useFormStore } from 'src/stores/forms';
import { availabilitySchema } from 'src/utils/zod/schemas/availabilitySchema';
import { useForm } from 'vee-validate';
import { defineComponent } from 'vue';
import { z } from 'zod';
import IconPostalCode from '../icons/IconPostalCode.vue';
export default defineComponent({
@ -38,19 +38,7 @@ export default defineComponent({
const formStore = useFormStore();
const { availability } = storeToRefs(formStore);
const validationSchema = toTypedSchema(
z
.object({
postalCode: z.string().refine((val) => {
const valWithoutHifen = val.replace('-', '');
const valLength = valWithoutHifen.length;
const regex = /^[0-9]+$/;
return (
regex.test(valWithoutHifen) && valLength === 8 && valLength > 0
);
}, 'El código postal no puede contener letras y debe constar de 8 caracteres!'),
})
.partial()
availabilitySchema.pick({ postalCode: true }).partial()
);
const { errors, defineField } = useForm({
validationSchema,

View File

@ -0,0 +1,82 @@
<template>
<div class="order-values" role="select">
<div
role="option"
class="order-values-option"
@click="handleOrder('lowest-price')"
>
<p class="filter-paragraph">menor precio</p>
</div>
<div
role="option"
class="order-values-option"
@click="handleOrder('highest-price')"
>
<p class="filter-paragraph">mayor precio</p>
</div>
<div
role="option"
class="order-values-option"
@click="handleOrder('latest')"
>
<p class="filter-paragraph">más recientes</p>
</div>
<div
role="option"
class="order-values-option"
@click="handleOrder('recommended')"
>
<p class="filter-paragraph">recomendados</p>
</div>
</div>
</template>
<script lang="ts">
import { storeToRefs } from 'pinia';
import { Order, useFormStore } from 'src/stores/forms';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'SortSelect',
components: {},
setup() {
const formStore = useFormStore();
const { sortProductFilters } = storeToRefs(formStore);
function handleOrder(order: Order) {
sortProductFilters.value.order = order;
sortProductFilters.value.isOpenOrderFilter = false;
}
return { sortProductFilters, handleOrder };
},
});
</script>
<style lang="scss" scoped>
.order-values {
display: flex;
flex-direction: column;
gap: 4px;
position: absolute;
top: 22px;
right: -10px;
left: -20px;
z-index: 10;
background-color: $secondary-10;
padding: 0px 10px 10px;
padding-right: 10px;
border-radius: 0px 0px 10px 10px;
&-option {
cursor: pointer;
& .filter-paragraph {
color: inherit;
font-size: $font-14;
}
}
}
</style>

View File

@ -1,64 +0,0 @@
<template>
<div>
<p>{{ title }}</p>
<ul>
<li v-for="todo in todos" :key="todo.id" @click="increment">
{{ todo.id }} - {{ todo.content }}
</li>
</ul>
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
<p>Active: {{ active ? 'yes' : 'no' }}</p>
<p>Clicks on todos: {{ clickCount }}</p>
</div>
</template>
<script lang="ts">
import {
defineComponent,
PropType,
computed,
ref,
toRef,
Ref,
} from 'vue';
import { Todo, Meta } from './models';
function useClickCount() {
const clickCount = ref(0);
function increment() {
clickCount.value += 1
return clickCount.value;
}
return { clickCount, increment };
}
function useDisplayTodo(todos: Ref<Todo[]>) {
const todoCount = computed(() => todos.value.length);
return { todoCount };
}
export default defineComponent({
name: 'ExampleComponent',
props: {
title: {
type: String,
required: true
},
todos: {
type: Array as PropType<Todo[]>,
default: () => []
},
meta: {
type: Object as PropType<Meta>,
required: true
},
active: {
type: Boolean
}
},
setup (props) {
return { ...useClickCount(), ...useDisplayTodo(toRef(props, 'todos')) };
},
});
</script>

View File

@ -0,0 +1,54 @@
<template>
<svg
width="29"
height="19"
viewBox="0 0 29 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_79_1742)">
<rect width="29" height="19" rx="3" fill="#1D71B9" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.4941 15.4503V9.1377L29.0158 9.14802V10.8917L27.684 12.2791L29.0158 13.6799V15.4603H26.8897L25.7598 14.2445L24.6377 15.4652L17.4941 15.4503Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.2637 14.7579V9.83228H22.5468V10.967H19.6508V11.7373H22.4778V12.8533H19.6508V13.6098H22.5468V14.7579H18.2637Z"
fill="#1D71B9"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.5254 14.7578L24.895 12.2923L22.5254 9.83252H24.3594L25.8076 11.3935L27.2598 9.83252H29.0154V9.87113L26.696 12.2923L29.0154 14.6882V14.7578H27.2422L25.7684 13.1811L24.3096 14.7578H22.5254Z"
fill="#1D71B9"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.0319 3.53491H20.8092L21.7846 5.69494V3.53491H25.2135L25.8047 5.15321L26.3979 3.53491H29.016V9.84716H15.1797L18.0319 3.53491Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.5578 4.2207L16.3164 9.14212H17.8536L18.2763 8.15654H20.5674L20.9901 9.14212H22.5653L20.3334 4.2207H18.5578ZM18.7503 7.05206L19.4222 5.48537L20.0938 7.05206H18.7503Z"
fill="#1D71B9"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.5449 9.14139V4.21997L24.7055 4.22723L25.8174 7.2528L26.9367 4.21997H29.0161V9.14139L27.6781 9.15286V5.77213L26.4149 9.14139H25.1932L23.9038 5.76066V9.14139H22.5449Z"
fill="#1D71B9"
/>
</g>
<defs>
<clipPath id="clip0_79_1742">
<rect width="29" height="19" rx="3" fill="white" />
</clipPath>
</defs>
</svg>
</template>

View File

@ -0,0 +1,42 @@
<template>
<span>
<svg
width="21"
height="13"
viewBox="0 0 21 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="logo">
<g id="icon">
<path
id="Vector"
d="M13.1039 1.81055H7.75195V11.1896H13.1039V1.81055Z"
fill="#FF5F00"
/>
<path
id="&#60;Path&#62;"
d="M8.091 6.50114C8.09001 5.59775 8.29983 4.70597 8.70458 3.89335C9.10933 3.08073 9.69839 2.36856 10.4272 1.81079C9.52482 1.11928 8.44119 0.689275 7.3001 0.569914C6.15901 0.450553 5.0065 0.646652 3.97429 1.1358C2.94208 1.62495 2.07182 2.38741 1.46298 3.33604C0.854126 4.28468 0.53125 5.38121 0.53125 6.50032C0.53125 7.61942 0.854126 8.71596 1.46298 9.66459C2.07182 10.6132 2.94208 11.3757 3.97429 11.8648C5.0065 12.354 6.15901 12.5501 7.3001 12.4307C8.44119 12.3114 9.52482 11.8814 10.4272 11.1898C9.69861 10.6322 9.10969 9.92035 8.70495 9.10803C8.30022 8.29571 8.09027 7.40426 8.091 6.50114Z"
fill="#EB001B"
/>
<path
id="Vector_2"
d="M20.3245 6.50086C20.3245 7.62005 20.0016 8.71665 19.3926 9.66531C18.7836 10.614 17.9133 11.3764 16.8809 11.8655C15.8486 12.3546 14.696 12.5506 13.5548 12.431C12.4136 12.3115 11.33 11.8813 10.4277 11.1896C11.156 10.6315 11.7447 9.91937 12.1496 9.10701C12.5544 8.29465 12.7648 7.40323 12.7648 6.50003C12.7648 5.59682 12.5544 4.70541 12.1496 3.89305C11.7447 3.08069 11.156 2.3686 10.4277 1.8105C11.33 1.11878 12.4136 0.688563 13.5548 0.569031C14.696 0.449498 15.8486 0.645471 16.8809 1.13455C17.9133 1.62363 18.7836 2.38608 19.3926 3.33474C20.0016 4.28341 20.3245 5.38001 20.3245 6.4992V6.50086Z"
fill="#F79E1B"
/>
</g>
</g>
</svg>
</span>
</template>
<style lang="scss" scoped>
span {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 3.5px 4.5px;
border-radius: 3px;
background-color: #01326f;
}
</style>

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="23" height="22" viewBox="0 0 23 22" fill="none">
<ellipse cx="11.5" cy="11" rx="11.5" ry="11" transform="rotate(-180 11.5 11)" fill="#117564" />
<path d="M13.6904 16.2383L9.61959 12.1516C8.84245 11.3714 8.84217 10.1098 9.61895 9.32931L13.6904 5.23828"
stroke="#CDEBD2" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="23" height="22" viewBox="0 0 23 22" fill="none">
<ellipse cx="11.5" cy="11" rx="11.5" ry="11" fill="#117564" />
<path d="M9.30957 5.76172L13.3804 9.8484C14.1575 10.6286 14.1578 11.8902 13.381 12.6707L9.30957 16.7617"
fill="#117564" />
<path d="M9.30957 5.76172L13.3804 9.8484C14.1575 10.6286 14.1578 11.8902 13.381 12.6707L9.30957 16.7617"
stroke="#CDEBD2" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 493 B

View File

@ -1,8 +0,0 @@
export interface Todo {
id: number;
content: string;
}
export interface Meta {
totalCount: number;
}

View File

@ -1,29 +1,87 @@
<template>
<swiper
:navigation="true"
<Swiper
:space-between="screenWidth > 1024 ? 56 : 25"
:slides-per-view="'auto'"
:centered-slides="true"
:modules="modules"
class="cards-carousel-container"
:grabCursor="true"
:loop="true"
:pagination="{
dynamicBullets: true,
clickable: true,
}"
:keyboard="{
enabled: true,
}"
@swiper="onSwiper"
>
<slot></slot>
</swiper>
</Swiper>
</template>
<script lang="ts">
import Swiper from 'swiper';
import { Navigation } from 'swiper/modules';
import { storeToRefs } from 'pinia';
import { Autoplay, Keyboard, Navigation, Pagination } from 'swiper/modules';
import type { Swiper as SwiperTypes } from 'swiper/types';
import { Swiper } from 'swiper/vue';
import { defineComponent } from 'vue';
import 'swiper/css';
import 'swiper/css/navigation';
import { useMobileStore } from 'src/stores/mobileNav';
import { useSwiperStore } from 'src/stores/swiper';
export default defineComponent({
name: 'horizontal-carousel',
props: {},
components: {
Swiper,
},
setup() {
const swiperStore = useSwiperStore();
const { swiperCtx } = storeToRefs(swiperStore);
function onSwiper(swiper: SwiperTypes) {
swiperCtx.value = swiper;
}
const mobileStore = useMobileStore();
const { screenWidth } = storeToRefs(mobileStore);
return {
modules: [Navigation],
onSwiper,
screenWidth,
modules: [Navigation, Pagination, Keyboard, Autoplay],
};
},
components: { Swiper },
});
</script>
<style lang="scss">
.swiper {
height: 100%;
width: 100%;
& .swiper-wrapper .swiper-slide {
width: 360px !important;
}
& .swiper-button-prev,
& .swiper-button-next {
background-repeat: no-repeat;
background-position: center;
&::after,
&::before {
display: none;
}
}
& .swiper-button-prev {
background-image: url('../../icons/svg/ArrowCircleFilledLeft.svg');
}
& .swiper-button-next {
background-image: url('../../icons/svg/ArrowCircleFilledRight.svg');
}
& .swiper-pagination {
& .swiper-pagination-bullet.swiper-pagination-bullet-active {
background-color: $primary;
}
}
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<q-btn
title="previous button"
class="swiper-btn prev"
color="primary"
size="sm"
@click="handlePrev"
round
flat
>
<IconArrowCircleFilledLeft />
</q-btn>
<swiper-container
class="swiper"
:space-between="screenWidth > 1024 ? 56 : 25"
:slides-per-view="'auto'"
:centered-slides="true"
:grabCursor="true"
:navigation="true"
:loop="true"
:pagination="{
dynamicBullets: true,
clickable: true,
}"
:keyboard="{
enabled: true,
}"
>
<slot></slot>
</swiper-container>
<q-btn
title="next button"
class="swiper-btn next"
color="primary"
size="sm"
@click="handleNext"
round
flat
>
<IconArrowCircleFilledRight />
</q-btn>
</template>
<script>
import { storeToRefs } from 'pinia';
import { defineComponent, onMounted, ref } from 'vue';
import { useMobileStore } from 'src/stores/mobileNav';
import IconArrowCircleFilledLeft from '../icons/IconArrowCircleFilledLeft.vue';
import IconArrowCircleFilledRight from '../icons/IconArrowCircleFilledRight.vue';
export default defineComponent({
name: 'SwiperComponent',
components: { IconArrowCircleFilledLeft, IconArrowCircleFilledRight },
setup() {
const mobileStore = useMobileStore();
const { screenWidth } = storeToRefs(mobileStore);
const prevBtn = ref(null);
const nextBtn = ref(null);
const swiperContainer = ref(null);
const prevSwiperBtn = ref(null);
const nextSwiperBtn = ref(null);
onMounted(() => {
// console.log('Montado!');
swiperContainer.value =
document.querySelector('swiper-container').shadowRoot;
prevSwiperBtn.value = swiperContainer.value.querySelector(
'.swiper-button-prev'
);
nextSwiperBtn.value = swiperContainer.value.querySelector(
'.swiper-button-next'
);
const swiperDisplay = 'none';
nextSwiperBtn.value.style.display = swiperDisplay;
prevSwiperBtn.value.style.display = swiperDisplay;
nextBtn.value = document.querySelector('.swiper-btn.next');
prevBtn.value = document.querySelector('.swiper-btn.prev');
});
/* onUpdated(() => {
console.log('Atualizado!');
console.groupCollapsed('%c Custom', 'color: tomato;');
console.log({ prevBtn: prevBtn.value, nextBtn: nextBtn.value });
console.groupEnd();
console.groupCollapsed('%c Swiper', 'color: hotpink;');
console.log(prevSwiperBtn.value);
console.groupEnd();
}); */
return {
screenWidth,
handlePrev() {
// console.log('Prev click');
prevSwiperBtn.value.click();
},
handleNext() {
// console.log('Next click');
nextSwiperBtn.value.click();
},
};
},
});
</script>
<style lang="scss">
.swiper {
height: 100%;
width: 100%;
&::after {
content: '';
position: absolute;
right: 0;
top: 0;
height: 409px;
width: 150px;
background: rgb(0, 0, 0);
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 25%,
rgba(255, 255, 255, 1) 100%
);
z-index: 4;
}
& .swiper-slide {
width: 360px !important;
}
& .swiper-button-prev,
& .swiper-button-next {
background-repeat: no-repeat;
background-position: center;
&::after,
&::before {
display: none;
}
}
& .swiper-button-prev {
background-image: url('../../icons/svg/ArrowCircleFilledLeft.svg');
}
& .swiper-button-next {
background-image: url('../../icons/svg/ArrowCircleFilledRight.svg');
}
& .swiper-pagination {
& .swiper-pagination-bullet.swiper-pagination-bullet-active {
background-color: $primary;
}
}
@media only screen and (max-width: $med-lg) {
&::after {
display: none;
}
& .swiper-slide {
width: 166px !important;
}
}
}
.swiper-btn {
position: absolute;
z-index: 5;
top: calc(50% - 40px);
transform: translateY(-50%);
&.prev {
left: 40px;
}
&.next {
right: 125px;
}
@media only screen and (max-width: $med-lg) {
top: -45px;
transform: initial;
&.prev {
left: 13px;
}
&.next {
right: 13px;
}
}
}
</style>

View File

@ -139,6 +139,9 @@ export default defineComponent({
border-radius: 15px;
overflow: hidden;
position: relative;
&:focus-visible {
outline: 2px solid $primary-light;
}
&:hover {
& .tags {
@ -170,6 +173,7 @@ export default defineComponent({
font-family: $font-lora;
user-select: none;
font-weight: 600;
font-size: $font-12;
&.new {
color: $white;
@ -180,6 +184,10 @@ export default defineComponent({
background: $primary-light;
color: $primary-dark;
}
@media only screen and (max-width: $med-lg) {
font-size: $font-10;
}
}
}

View File

@ -1,11 +1,19 @@
<template>
<div class="mobile-nav-container" :class="!isOpenNav && 'hide'">
<header class="mobile-nav-links">
<RouterLink class="mobile-link" to="/">Ramos</RouterLink>
<RouterLink class="mobile-link" to="/">Plantas</RouterLink>
<RouterLink class="mobile-link" to="/">Floranet</RouterLink>
<RouterLink class="mobile-link" to="/">FAQs</RouterLink>
<RouterLink class="mobile-link" to="/">Contacta</RouterLink>
<RouterLink @click="closeNav" class="mobile-link" to="/categoria/ramos"
>Ramos</RouterLink
>
<RouterLink @click="closeNav" class="mobile-link" to="/categoria/plantas">
Plantas
</RouterLink>
<RouterLink @click="closeNav" class="mobile-link" to="/"
>Floranet</RouterLink
>
<RouterLink @click="closeNav" class="mobile-link" to="/">FAQs</RouterLink>
<RouterLink @click="closeNav" class="mobile-link" to="/"
>Contacta</RouterLink
>
</header>
<div class="mobile-nav-lang">
@ -37,9 +45,14 @@ export default defineComponent({
const mobileStore = useMobileStore();
const { isOpenNav } = storeToRefs(mobileStore);
const setBodyStyle = (overflow: 'hidden' | 'visible') => {
function setBodyStyle(overflow: 'hidden' | 'visible') {
document.body.style.overflow = overflow;
};
}
function closeNav() {
isOpenNav.value = false;
console.log('foi click');
}
watch(isOpenNav, (newValue) => {
if (newValue) {
@ -50,7 +63,7 @@ export default defineComponent({
setBodyStyle('visible');
});
return { isOpenNav };
return { isOpenNav, closeNav };
},
});
</script>

View File

@ -73,7 +73,6 @@ export default defineComponent({
IconSearch,
IconCloseModal,
PriceRange,
Calendar,
PostalCode,
},

View File

@ -4,8 +4,8 @@
<q-input
v-model="firstName"
v-bind="firstNameAttrs"
:error-message="errors.fist_name"
:error="!!errors.fist_name"
:error-message="errors.name"
:error="!!errors.name"
bg-color="white"
label="Nombre"
class="name"
@ -14,8 +14,8 @@
<q-input
v-model="secondName"
v-bind="secondNameAttrs"
:error-message="errors.second_name"
:error="!!errors.second_name"
:error-message="errors.surname"
:error="!!errors.surname"
bg-color="white"
label="Apellidos"
class="nickname"
@ -33,10 +33,10 @@
standout
/>
<q-input
v-model="telephone"
v-bind="telephoneAttrs"
:error-message="errors.telephone"
:error="!!errors.telephone"
v-model="phone"
v-bind="phoneAttrs"
:error-message="errors.phone"
:error="!!errors.phone"
bg-color="white"
type="tel"
label="Teléfono"
@ -83,7 +83,7 @@
type="submit"
class="question-submit-btn btn rounded"
flat
:disable="!terms"
:disable="!meta.valid"
>
Enviar solicitud <IconArrowRightOne />
</q-btn>
@ -91,14 +91,13 @@
</template>
<script lang="ts">
import { toTypedSchema } from '@vee-validate/zod';
import { useQuasar } from 'quasar';
import { useFormStore } from 'src/stores/forms';
import { useForm } from 'vee-validate';
import { defineComponent } from 'vue';
import { z } from 'zod';
import { useQuasar } from 'quasar';
import IconArrowRightOne from 'src/components/icons/IconArrowRightOne.vue';
import { questionSchema } from 'src/utils/zod/schemas/questionSchema';
export default defineComponent({
name: 'QuestionForm',
@ -107,35 +106,20 @@ export default defineComponent({
const $q = useQuasar();
const formStore = useFormStore();
const { handleQuestionData } = formStore;
const nameMessage = 'Sólo se aceptan una palabra y caracteres no numéricos';
const requiredMessage = 'Campo obligatorio';
const validationSchema = toTypedSchema(
z
.object({
fist_name: z
.string({ required_error: requiredMessage })
.regex(/^[A-Za-z]+$/, nameMessage),
second_name: z
.string({ required_error: requiredMessage })
.regex(/^[A-Za-z]+$/, nameMessage),
email: z.string({ required_error: requiredMessage }).email(),
telephone: z.string({ required_error: requiredMessage }),
query: z.string({ required_error: requiredMessage }),
message: z.string({ required_error: requiredMessage }),
terms: z.boolean({ required_error: requiredMessage }),
})
.required({})
);
const { errors, meta, defineField, handleSubmit, handleReset } = useForm({
validationSchema,
validationSchema: questionSchema,
initialValues: {
terms: false,
},
});
const [firstName, firstNameAttrs] = defineField('fist_name', {});
const [secondName, secondNameAttrs] = defineField('second_name', {});
const [email, emailAttrs] = defineField('email', {});
const [telephone, telephoneAttrs] = defineField('telephone', {});
const [query, queryAttrs] = defineField('query', {});
const [message, messageAttrs] = defineField('message', {});
const [terms, termsAttrs] = defineField('terms', {});
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);
@ -163,8 +147,8 @@ export default defineComponent({
secondNameAttrs,
email,
emailAttrs,
telephone,
telephoneAttrs,
phone,
phoneAttrs,
query,
queryAttrs,
message,
@ -225,6 +209,9 @@ export default defineComponent({
& .question-submit-btn {
align-self: flex-start;
@media only screen and (max-width: $med-lg) {
align-self: initial;
}
}
}

View File

@ -7,6 +7,11 @@
@import './pages/faq.scss';
@import './components/calendar-postalcode.scss';
:root {
--swiper-theme-color: #117564;
--swiper-pagination-color: #117564;
}
* {
margin: 0;
padding: 0;
@ -39,6 +44,13 @@ html {
}
}
.checkout-padding {
padding-top: 149px !important;
@media only screen and (max-width: $med-md) {
padding-top: 73px !important;
}
}
.hide {
display: none;
visibility: hidden;
@ -86,7 +98,7 @@ html {
&:focus,
&:focus-visible {
outline: 2px solid $primary-light;
outline: 2px solid $primary-light !important;
}
&.outlined {
@ -232,7 +244,11 @@ ul {
a {
font-family: $font-questrial;
text-decoration: none;
font-size: font-xxxsm;
font-size: $font-xxxsm;
// display: inline-flex;
&:focus-visible {
outline: 2px solid $primary-light;
}
}
p,
@ -260,7 +276,12 @@ p,
.header-title {
margin-block: 90px 50px;
.pege-title {
&.success {
width: min(100%, 676px);
margin: 90px auto 64px;
}
& .pege-title {
font-family: $font-lora;
font-size: 1.875rem;
line-height: 1.1;
@ -269,10 +290,14 @@ p,
margin-bottom: 25px;
color: $title-default;
}
.pege-subtitle {
& .pege-subtitle {
font-family: $font-questrial;
font-size: 1rem;
text-align: center;
&.checkout {
line-height: 28px;
}
}
}

View File

@ -26,6 +26,7 @@ $positive: #21ba45;
$negative: #c10015;
$info: #31ccec;
$warning: #f2c037;
$blue: #00709f;
//! Media queries
$med-hg: 1441px;
@ -54,12 +55,14 @@ $primary-light: #cdebd2;
//! Secondary pallete
$white: #ffffff;
$secondary-orange: #ff9900;
$secondary-orange-light: #e3e3e3;
$secondary-100: #141414;
$secondary-80: #54544f;
$secondary-60: #979797;
$secondary-40: #ededed;
$secondary-20: #f8f8f8;
$secondary-10: #f9f9f9;
$secondary-5: #fafafa;
//! Font color pallete
$title-default: #117564;
@ -135,6 +138,7 @@ $font-18: 1.125rem;
$font-16: 1rem;
$font-14: 0.875rem;
$font-12: 0.75rem;
$font-10: 0.625rem;
//! Button
$btn-black-bg: $text-title-100;

View File

@ -5,7 +5,7 @@
</q-no-ssr>
<mobile-nav />
<q-page-container class="no-padding padding-top more">
<q-page-container class="no-padding padding-top checkout-padding">
<router-view />
</q-page-container>

View File

@ -1,13 +1,16 @@
import { fakerES } from '@faker-js/faker';
export const cardMock = Array.from({ length: 10 }, (_, i) => ({
imgSrc: `assets/flowers/flower-${i + 1}.png`,
discount: i % 2 === 0 ? '10' : '',
title: 'Nombre del producto',
isNew: i % 2 === 0,
value: '25,90',
export const cardMock = Array.from({ length: 8 }, (_, i) => ({
id: i + 1,
attributes: [''],
imgSrc: `assets/flowers/flower-${i + 1}.png`,
title: fakerES.commerce.productName(),
discount: fakerES.commerce.price({ min: 5, max: 15, dec: 0 }),
isNew: fakerES.datatype.boolean(),
value: fakerES.commerce.price({ min: 20, max: 150 }),
// title: 'Nombre del producto',
// discount: i % 2 === 0 ? '10' : '',
// isNew: i % 2 === 0,
// value: '25,90',
}));
interface GenerateFlowersParams {

View File

@ -48,13 +48,27 @@
<IconFilter />
</q-btn>
<div class="filter-item order-filter">
<div
class="filter-item order-filter"
:class="sortProductFilters.isOpenOrderFilter && 'active'"
>
<div class="order-filters">
<p class="filter-paragraph">
Ordenar por:
<span class="green-text">precio</span>
<span class="green-text">{{
orderText[sortProductFilters.order as Order] || ''
}}</span>
</p>
<q-btn flat type="button" class="btn filter-btn price-order">
<SortSelect v-if="sortProductFilters.isOpenOrderFilter" />
</div>
<q-btn
flat
@click="openOrderFilter"
type="button"
class="btn filter-btn price-order"
>
<IconArrowDownWhite />
</q-btn>
</div>
@ -96,18 +110,23 @@
<script lang="ts">
import { fakerES } from '@faker-js/faker';
import { storeToRefs } from 'pinia';
import { defineComponent, ref } from 'vue';
import {
defineAsyncComponent,
defineComponent,
onMounted,
onUpdated,
reactive,
ref,
} from 'vue';
import { useRoute } from 'vue-router';
import SortSelect from 'src/components/@inputs/SortSelect.vue';
import IconArrowCircleFilledRight from 'src/components/icons/IconArrowCircleFilledRight.vue';
import IconArrowDownWhite from 'src/components/icons/IconArrowDownWhite.vue';
import IconFilter from 'src/components/icons/IconFilter.vue';
import IconPencil from 'src/components/icons/IconPencil.vue';
import DudasSection from 'src/components/sections/DudasSection.vue';
import Card from 'src/components/ui/Card.vue';
import Container from 'src/components/ui/Container.vue';
import Modal from 'src/components/ui/Modal.vue';
import { useFormStore } from 'src/stores/forms';
import { Category, Order, useFormStore } from 'src/stores/forms';
import { useModalStore } from 'src/stores/modalStore';
type MonthES =
@ -132,15 +151,18 @@ export default defineComponent({
IconPencil,
IconFilter,
Container,
Modal,
Card,
DudasSection,
DudasSection: defineAsyncComponent(
() => import('src/components/sections/DudasSection.vue')
),
Modal: defineAsyncComponent(() => import('src/components/ui/Modal.vue')),
Card: defineAsyncComponent(() => import('src/components/ui/Card.vue')),
SortSelect,
},
setup() {
const modalStore = useModalStore();
const formStore = useFormStore();
const { availability } = storeToRefs(formStore);
const monthES: Record<number, MonthES> = {
const { availability, sortProductFilters } = storeToRefs(formStore);
const monthES: Record<number, MonthES> = reactive({
0: 'Enero',
1: 'Febrero',
2: 'Marzo',
@ -153,13 +175,10 @@ export default defineComponent({
9: 'Octubre',
10: 'Noviembre',
11: 'Diciembre',
};
// monthES[] || console.error('Invalid date');
});
const isOpenOrder = ref(false);
const category = ref('');
const { path } = useRoute();
const [_a, _b, categoryValue] = path.split(/\//);
const route = useRoute();
const cardsMock = Array.from({ length: 8 }, (_, i) => ({
id: i + 1,
imgSrc: `../assets/flowers/flower-${i + 1}.png`,
@ -168,8 +187,37 @@ export default defineComponent({
title: fakerES.commerce.product(),
value: fakerES.commerce.price({ min: 30, max: 100 }),
}));
const orderText: Record<Order, string> = {
'lowest-price': 'menor precio',
'highest-price': 'mayor precio',
recommended: 'recomendados',
latest: 'más recientes',
};
return { modalStore, availability, cardsMock, category };
onMounted(() => {
sortProductFilters.value.category = route.path.split('/')[3] as Category;
});
onUpdated(() => {
console.log('Atualizado!');
console.log(sortProductFilters.value);
});
function openOrderFilter() {
sortProductFilters.value.isOpenOrderFilter =
!sortProductFilters.value.isOpenOrderFilter;
}
return {
sortProductFilters,
openOrderFilter,
availability,
isOpenOrder,
modalStore,
orderText,
cardsMock,
category,
};
},
});
</script>
@ -216,7 +264,16 @@ export default defineComponent({
padding: 4px 30px 4px 14px;
}
&.order-filter {
padding: 4px 30px 4px 14px;
padding: 4px 10px 4px 20px;
text-align: end;
border-radius: 10px 0px 0px 10px;
&.active {
border-radius: 10px 0px 0px 0px;
}
& .order-filters {
position: relative;
}
}
&.filters {
@ -230,6 +287,7 @@ export default defineComponent({
& .filter-btn {
padding: 8px;
border-radius: 0 30px 30px 0;
&.availability,
&.price-order {
position: absolute;
@ -243,6 +301,7 @@ export default defineComponent({
&.price-order {
right: -33px;
padding: 9.5px;
top: 0;
}
}
@ -250,6 +309,7 @@ export default defineComponent({
display: flex;
gap: 40px;
margin-right: 33px;
align-items: flex-start;
}
@media only screen and (max-width: $med-md) {

View File

@ -1,75 +1,86 @@
<template>
<q-page class="checkout-page">
<Container tag="section">
<header class="header-title">
<h1 class="pege-title">¿A quién y dónde lo entregamos?</h1>
<header class="header-title" :class="!checkoutBlock && 'success'">
<h1 class="pege-title" v-if="checkoutBlock">
¿A quién y dónde lo entregamos?
</h1>
<h1 class="pege-title" v-if="!checkoutBlock">¡Muchas gracias Jerom!</h1>
<p class="pege-subtitle">
<p class="pege-subtitle checkout" v-if="checkoutBlock">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
<p class="pege-subtitle checkout" v-if="!checkoutBlock">
¡Tu pedido se ha realizado con éxito! Gracias por confiar en nosotros,
en breves recibirás un correo con la confirmación de tu pedido.
</p>
</header>
<div class="checkout-container">
<div class="checkout-steps">
<div
v-for="({ active, description, name, value }, i) in stepsFormated()"
class="step-item-container"
v-for="(step, index) in stepsFormated()"
:key="index"
:key="i"
>
<div class="step-item">
<div class="circle-step-container">
<span class="border-step" :class="[i == 0 && 'transparent']" />
<div
:class="['border-step', index == 0 && 'transparent']"
></div>
<div
:class="['circle-step', step.active && 'active']"
v-on:click="handleClickStep(step.value)"
>
<span class="step-value">{{ step.value }}</span>
</div>
<div
class="circle-step"
:class="[
'border-step',
index == stepList['data'].length - 1 && 'transparent',
(active || (meta.valid && i == 1) || !checkoutBlock) &&
'active',
]"
></div>
>
<span class="step-value">{{ value }}</span>
</div>
<span
class="border-step"
:class="[i == stepList['data'].length - 1 && 'transparent']"
/>
</div>
<div class="step-content">
<div class="title">
<h4>{{ step.name }}</h4>
<h4>{{ name }}</h4>
</div>
<div class="description">
<p>{{ step.description }}</p>
<p>{{ description }}</p>
</div>
</div>
</div>
</div>
</div>
<template v-if="checkoutBlock">
<div class="checkout-content">
<div class="checkout-form-container">
<header class="checkout-header-form">
<h3>Instrucciones para la entrega</h3>
</header>
<div class="checkout-form-wrapper">
<div class="checkout-form">
<q-form
action=""
method="post"
@submit.prevent="onSubmitPersonalData"
id="checkout-form"
@submit.prevent="onSubmit"
>
<div class="form-fields-container delivery">
<header class="checkout-header-form">
<h3>Instrucciones para la entrega</h3>
</header>
<div class="checkout-fields">
<div class="field-control field-input">
<q-input
placeholder="Nombre*"
name="name"
type="text"
v-model="formPersonalData.data.name"
:rules="[
(val) => val.length > 0 || 'Campo obligatorio',
]"
v-model="name"
v-bind:="nameAttrs"
:error="!!errors.name"
:error-message="errors.name"
outlined
/>
</div>
@ -79,10 +90,10 @@
placeholder="Apellidos*"
name="surname"
type="text"
v-model="formPersonalData.data.surname"
:rules="[
(val) => val.length > 0 || 'Campo obligatorio',
]"
v-model="surname"
v-bind:="surnameAttrs"
:error="!!errors.surname"
:error-message="errors.surname"
outlined
/>
</div>
@ -92,10 +103,10 @@
placeholder="Dirección*"
name="address"
type="text"
v-model="formPersonalData.data.address"
:rules="[
(val) => val.length > 0 || 'Campo obligatorio',
]"
v-model="address"
v-bind:="addressAttrs"
:error="!!errors.address"
:error-message="errors.address"
outlined
/>
</div>
@ -105,18 +116,22 @@
placeholder="Código postal*"
name="postalCode"
type="text"
v-model="formPersonalData.data.postalCode"
:rules="[
(val) => val.length > 0 || 'Campo obligatorio',
]"
mask="#####-###"
v-model="postalCode"
v-bind:="postalCodeAttrs"
:error="!!errors.postalCode"
:error-message="errors.postalCode"
outlined
/>
</div>
<div class="field-control field-select">
<q-select
outlined
v-model="formPersonalData.data.city"
name="city"
v-model="city"
v-bind:="cityAttrs"
:error="!!errors.city"
:error-message="errors.city"
:options="optionsCity.data"
:label="
formPersonalData.data.city.length
@ -124,13 +139,17 @@
: 'Ciudad*'
"
stack-label
outlined
/>
</div>
<div class="field-control field-select">
<q-select
outlined
v-model="formPersonalData.data.province"
name="province"
v-model="province"
v-bind:="provinceAttrs"
:error="!!errors.province"
:error-message="errors.province"
:options="optionsProvince.data"
:label="
formPersonalData.data.province
@ -138,115 +157,126 @@
: 'Provincia*'
"
stack-label
outlined
/>
</div>
<div class="field-control field-input">
<div class="field-control field-input telephone">
<q-input
outlined
placeholder="Teléfono*"
name="phone"
type="text"
v-model="formPersonalData.data.phone"
:rules="[
(val) => val.length > 0 || 'Campo obligatorio',
]"
mask="(##) ##### ####"
v-model="phone"
v-bind:="phoneAttrs"
:error="!!errors.phone"
:error-message="errors.phone"
outlined
/>
</div>
</div>
</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*"
name="name"
placeholder="Nombre y apellidos o nombre de empresa"
name="senderName"
type="text"
v-model="formPersonalData.data.name"
:rules="[
(val) => val.length > 0 || 'Campo obligatorio',
]"
v-model="senderName"
v-bind:="senderNameAttrs"
:error="!!errors.senderName"
:error-message="errors.senderName"
outlined
/>
</div>
<div class="field-control field-input">
<q-input
placeholder="Nombre*"
name="name"
placeholder="CIF / NIF"
name="senderCifNif"
type="text"
v-model="formPersonalData.data.name"
:rules="[
(val) => val.length > 0 || 'Campo obligatorio',
]"
mask="#########"
v-model="senderCifNif"
v-bind:="senderCifNifAttrs"
:error="!!errors.senderCifNif"
:error-message="errors.senderCifNif"
outlined
/>
</div>
<div class="field-control field-input">
<q-input
placeholder="Nombre*"
name="name"
type="text"
v-model="formPersonalData.data.name"
:rules="[
(val) => val.length > 0 || 'Campo obligatorio',
]"
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="Nombre*"
name="name"
placeholder="Teléfono"
name="senderPhone"
type="text"
v-model="formPersonalData.data.name"
:rules="[
(val) => val.length > 0 || 'Campo obligatorio',
]"
mask="(##) ##### ####"
v-model="senderPhone"
v-bind:="senderPhoneAttrs"
:error="!!errors.senderPhone"
:error-message="errors.senderPhone"
outlined
/>
</div>
<div class="field-control field-input">
<q-input
v-model="formPersonalData.data.name"
:rules="[
(val) => val.length > 0 || 'Campo obligatorio',
]"
bg-color="white"
label="Mensaje"
class="message"
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 class="form-button-control">
<q-btn color="primary" type="submit">Continuar</q-btn>
</div>
</q-form>
</div>
<aside class="checkout-aside">
<div class="checkout-delivery-date" v-if="true">
<header class="checkout-aside-header">
<strong> Fecha de entrega </strong>
<header class="checkout-aside-header green-text">
<strong class="checkout-aside-title">
Fecha de entrega
</strong>
</header>
<div class="checkout-delivery-body">
<p>13 de julio - De 11h - 12 h</p>
<p class="green-text">13 de julio - De 11h - 12 h</p>
</div>
</div>
<div class="checkout-summary">
<header class="checkout-aside-header">
<strong> Resumen del pedido </strong>
<header class="checkout-aside-header gray-bg">
<strong class="checkout-aside-title">
Resumen del pedido
</strong>
</header>
<div class="checkout-summary-body">
<div class="checkout-summary-body gray-bg">
<ul class="checkout-summary-list">
<li class="checkout-summary-item">
<p>Ramo Lucena <span>30,00</span></p>
@ -261,35 +291,101 @@
<footer class="checkout-summary-footer">
<p class="checkout-summary-paragraph">Total</p>
<p class="checkout-summary-price">67</p>
<p class="checkout-summary-paragraph summary-price">67</p>
</footer>
</div>
<div class="checkout-payment-methods">
<div class="checkout-payment-methods gray-bg">
<header class="checkout-aside-header">
<strong>Método de pago</strong>
<strong class="checkout-aside-title">Método de pago</strong>
</header>
<div class="checkout-payment-body">
<q-radio v-model="priceMethod" val="credit" color="primary">
<p>a</p>
<q-radio
v-model="paymentMethod"
v-bind="paymentMethodAttrs"
val="credit"
color="primary"
>
<p>
Tarjeta
<span class="card-flags">
<IconMaster /><IconVisa /> <IconAny /> <IconExpress />
</span>
</p>
</q-radio>
<q-radio v-model="priceMethod" val="stripe" color="primary">
<p>b</p>
<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" class="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">CONTINUAR</q-btn>
<q-btn flat class="btn" type="submit" form="checkout-form">
PROCEDER AL PAGO
</q-btn>
</div>
</aside>
</div>
</template>
<div v-if="!checkoutBlock" 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 class="checkout-success-item">
<div class="checkout-item-content">
<div class="checkout-product-details">
<img
src="../assets/checkout-flower.png"
alt="product"
class="checkout-product-img"
/>
<p class="checkout-product-title">Ramo Lucena</p>
</div>
<p class="checkout-product-price">30.00</p>
</div>
</li>
<li class="checkout-success-item">
<div class="checkout-item-content">
<div class="checkout-product-details">
<img
src="../assets/checkout-flower.png"
alt="product"
class="checkout-product-img"
/>
<p class="checkout-product-title">Ramo Lucena</p>
</div>
<p class="checkout-product-price">30.00</p>
</div>
</li>
</ul>
</div>
<footer class="checkout-success-footer">
<p class="checkout-success-paragraph">Total</p>
<p class="checkout-success-paragraph">67.00</p>
</footer>
</div>
</div>
</div>
@ -298,8 +394,17 @@
</template>
<script lang="ts">
import { useForm } from 'vee-validate';
import { defineComponent, reactive, ref } from 'vue';
import { toTypedSchema } from '@vee-validate/zod';
import IconAny from 'src/components/icons/credit-flags/IconAny.vue';
import IconExpress from 'src/components/icons/credit-flags/IconExpress.vue';
import IconMaster from 'src/components/icons/credit-flags/IconMaster.vue';
import IconVisa from 'src/components/icons/credit-flags/IconVisa.vue';
import Container from 'src/components/ui/Container.vue';
import { defineComponent, reactive, ref, watch } from 'vue';
import { useFormStore } from 'src/stores/forms';
import { checkoutSchema } from 'src/utils/zod/schemas/checkoutSchema';
interface StepsProps {
value: number;
@ -320,19 +425,39 @@ interface FormPersonalData {
export default defineComponent({
name: 'CheckoutPage',
components: { Container },
components: {
Container,
IconAny,
IconVisa,
IconExpress,
IconMaster,
// StepByStep,
},
setup() {
const formPersonalData = reactive<{ data: FormPersonalData }>({
data: {
name: '',
surname: '',
address: '',
postalCode: '',
phone: '',
city: '',
province: '',
const formStore = useFormStore();
const { handleCheckoutData } = formStore;
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
validationSchema: toTypedSchema(checkoutSchema),
initialValues: {
paymentMethod: 'credit',
terms: false,
},
});
const [name, nameAttrs] = defineField('name');
const [surname, surnameAttrs] = defineField('surname');
const [address, addressAttrs] = defineField('address');
const [postalCode, postalCodeAttrs] = defineField('postalCode');
const [phone, phoneAttrs] = defineField('phone');
const [city, cityAttrs] = defineField('city');
const [province, provinceAttrs] = defineField('province');
const [senderName, senderNameAttrs] = defineField('senderName');
const [senderCifNif, senderCifNifAttrs] = defineField('senderCifNif');
const [senderEmail, senderEmailAttrs] = defineField('senderEmail');
const [senderPhone, senderPhoneAttrs] = defineField('senderPhone');
const [senderNotes, senderNotesAttrs] = defineField('senderNotes');
const [paymentMethod, paymentMethodAttrs] = defineField('paymentMethod');
const [terms, termsAttrs] = defineField('terms');
const stepActive = reactive({ data: 1 });
const stepList = reactive<{ data: StepsProps[] }>({
data: [
@ -340,7 +465,7 @@ export default defineComponent({
value: 1,
name: 'Paso 1',
description: 'Datos de facturación',
active: false,
active: true,
},
{
value: 2,
@ -356,6 +481,16 @@ export default defineComponent({
},
],
});
const checkoutBlock = ref(true);
// const successblock = document.querySelector('#success-block');
const onSubmit = handleSubmit((values) => {
handleCheckoutData(values);
stepList.data[2].active = true;
checkoutBlock.value = false;
// successblock?.scrollIntoView({ behavior: 'smooth' });
resetForm();
});
const handleClickStep = (value: number) => {
stepActive['data'] = value;
@ -378,7 +513,18 @@ export default defineComponent({
data: ['Complete la dirección, código postal y seleccione la ciudad'],
});
watch(
const formPersonalData = reactive<{ data: FormPersonalData }>({
data: {
name: '',
surname: '',
address: '',
postalCode: '',
phone: '',
city: '',
province: '',
},
});
/* watch(
() => formPersonalData.data.postalCode,
() => {
if (
@ -423,21 +569,49 @@ export default defineComponent({
function onSubmitPersonalData(_e: Event) {
console.log(formPersonalData.data);
}
const priceMethod = ref('credit');
const terms = ref(false);
} */
return {
handleClickStep,
stepsFormated,
onSubmitPersonalData,
formPersonalData,
optionsProvince,
optionsCity,
stepList,
priceMethod,
step: ref(1),
checkoutBlock,
meta,
errors,
onSubmit,
name,
nameAttrs,
surname,
surnameAttrs,
address,
addressAttrs,
postalCode,
postalCodeAttrs,
phone,
phoneAttrs,
city,
cityAttrs,
province,
provinceAttrs,
senderName,
senderNameAttrs,
senderCifNif,
senderCifNifAttrs,
senderEmail,
senderEmailAttrs,
senderPhone,
senderPhoneAttrs,
senderNotes,
senderNotesAttrs,
terms,
termsAttrs,
paymentMethod,
paymentMethodAttrs,
};
},
});
@ -450,20 +624,24 @@ export default defineComponent({
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;
@ -484,6 +662,7 @@ export default defineComponent({
}
}
}
.step-content {
display: flex;
flex-direction: column;
@ -503,40 +682,275 @@ export default defineComponent({
font-family: $font-lora;
}
}
.checkout-content {
margin: 50px 0 150px;
& .checkout-content {
width: min(100%, 1144px);
margin: 50px auto 0;
display: flex;
flex-wrap: wrap;
gap: 20px;
.checkout-header-form {
background-color: $grey-700;
padding: 12px 30px;
width: 100%;
margin-bottom: 20px;
margin-bottom: 21px;
border-radius: 5px;
h3 {
color: $text-default;
font-size: 0.875rem;
line-height: 1.5;
}
}
& .checkout-form-wrapper {
display: flex;
flex-wrap: wrap;
gap: 20px;
@media only screen and (max-width: $med-lg) {
margin-bottom: 11px;
}
}
& .checkout-form {
flex: 1 0 min(100%, 795px);
}
& .checkout-aside {
flex: 1 0 min(100%, 329px);
& .checkout-delivery-date,
& .checkout-summary,
& .checkout-payment-methods {
border-radius: 5px;
overflow: hidden;
}
& .gray-bg {
background-color: $secondary-10;
}
& .checkout-delivery-date {
display: flex;
flex-direction: column;
gap: 18px;
padding: 16px 23px 18px;
background-color: $primary-light;
margin-bottom: 21px;
}
& .checkout-summary {
margin-bottom: 33px;
& .checkout-aside-header,
& .checkout-summary-body,
& .checkout-summary-footer {
padding-inline: 22px 19px;
}
& .checkout-aside-header {
padding-block: 16px 17px;
& .checkout-aside-title {
font-family: $font-lora;
font-weight: 600;
line-height: 21px;
letter-spacing: 0.32px;
}
}
& .checkout-summary-body {
padding-bottom: 23px;
& p {
font-size: $font-12;
line-height: 21px;
letter-spacing: 0.24px;
color: $text-default;
}
& .checkout-summary-list {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 4px;
& .checkout-summary-item p {
display: flex;
justify-content: space-between;
}
}
}
& .checkout-summary-footer {
display: flex;
justify-content: space-between;
background-color: $secondary-orange-light;
padding: 19px 19px 20px 23px;
& .checkout-summary-paragraph {
font-family: $font-lora;
line-height: 21px;
letter-spacing: 0.32px;
color: $text-default;
}
}
}
& .checkout-payment-methods {
padding: 9px 16px 20px 21px;
margin-bottom: 21px;
& .checkout-aside-header {
margin-bottom: 14px;
}
& .checkout-payment-body {
display: flex;
flex-direction: column;
gap: 12px;
& p {
width: min(100%, 200px);
display: inline-flex;
justify-content: space-between;
gap: 31px;
font-size: $font-12;
& .card-flags {
display: inline-flex;
align-items: center;
gap: 4px;
}
& a {
margin-left: 4px;
font-family: $font-questrial;
color: $blue;
font-size: $font-12;
line-height: 21px;
letter-spacing: 0.24px;
text-decoration: underline;
}
}
}
}
& .checkout-terms {
display: flex;
flex-direction: column;
gap: 23px;
& .terms p {
margin-left: 10px;
font-size: $font-12;
line-height: 17px;
letter-spacing: 0.24px;
color: $text-muted-one;
}
& .erro {
font-family: $font-lora;
font-size: $font-12;
color: #ff0000;
text-align: center;
}
}
}
@media only screen and (max-width: $med-lg) {
gap: 47px;
}
}
& .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;
}
}
}
}
.form-fields-container {
display: flex;
flex-wrap: wrap;
gap: 10px 15px;
&.delivery {
}
&.sender {
margin-top: 12px;
}
& .checkout-fields {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 12px 9px;
@media only screen and (max-width: $med-lg) {
gap: 15px;
}
}
.field-control {
flex-basis: calc(50% - 8px);
flex: 1 0 min(100%, 390px);
&.telephone {
flex: 0 0 calc(50% - 5px);
@media only screen and (max-width: $med-lg) {
flex: 1 0 min(100%, 390px);
}
}
&.field-input {
label {
@ -592,6 +1006,11 @@ export default defineComponent({
}
}
}
@media only screen and (max-width: $med-lg) {
&.sender {
margin-top: 45px;
}
}
}
}
</style>

View File

@ -4,11 +4,6 @@
<VerticalCarouselImgs :imgsArr="slidesContent" class="home-carousel" />
</q-no-ssr>
<!-- <p v-if="isCarouselVisible">Está visível</p>
<p v-if="!isCarouselVisible">Não está visível</p>
<p v-if="isOpenNav">Hamburg ativo</p>
<p v-if="!isOpenNav">Hamburg não ativo</p> -->
<section class="products-section">
<header class="products-section-header section-header">
<h3 class="products-header-title subtitle">
@ -44,7 +39,7 @@
</div>
</section>
<section class="products-selection">
<section class="products-selection-section">
<header class="products-selection-header section-header">
<h3 class="products-selection-title subtitle">
Nuestra selección de plantas para el verano
@ -58,7 +53,42 @@
</header>
<div class="products-selection-body">
<!-- <Container> </Container> -->
<!-- <HorizontalCarousel>
<SwiperSlideOne
v-for="{ id, discount, isNew, value, title, imgSrc } in cardMock"
:key="id"
>
<Card
:id="id"
:key="id"
:productValue="value"
:productName="title"
:discount="discount"
:imgSrc="imgSrc"
:isNew="isNew"
/>
</SwiperSlideOne>
</HorizontalCarousel> -->
<q-no-ssr>
<Swiper>
<swiper-slide
v-for="{ id, discount, isNew, value, title, imgSrc } in cardMock"
:key="id"
class="swiper-slide"
>
<Card
:id="id"
:key="id"
:productValue="value"
:productName="title"
:discount="discount"
:imgSrc="imgSrc"
:isNew="isNew"
/>
</swiper-slide>
</Swiper>
</q-no-ssr>
</div>
<footer class="products-selection-footer">
@ -71,11 +101,9 @@
</template>
<script lang="ts">
import { storeToRefs } from 'pinia';
import { defineAsyncComponent, defineComponent, ref } from 'vue';
import { storeToRefs } from 'pinia';
import IconArrowCircleFilledRight from 'src/components/icons/IconArrowCircleFilledRight.vue';
import VerticalCarouselImgs from 'src/components/quasar-components/carousel/VerticalCarouselImgs.vue';
import Container from 'src/components/ui/Container.vue';
import { cardMock } from 'src/mock/cards';
import { useMobileStore } from 'src/stores/mobileNav';
@ -83,21 +111,23 @@ import { useMobileStore } from 'src/stores/mobileNav';
export default defineComponent({
name: 'HomePage',
components: {
IconArrowCircleFilledRight,
VerticalCarouselImgs,
// HorizontalCarousel,
// ButtonComponent,
// SwiperSlide,
VerticalCarouselImgs: defineAsyncComponent(
() =>
import(
'src/components/quasar-components/carousel/VerticalCarouselImgs.vue'
)
),
Swiper: defineAsyncComponent(
() => import('src/components/swiper/Swiper.vue')
),
Card: defineAsyncComponent(() => import('src/components/ui/Card.vue')),
Container,
Card: defineAsyncComponent({
loader: () => import('src/components/ui/Card.vue'),
loadingComponent: { template: '<p>loading</p>' },
}),
},
setup() {
const mobileStore = useMobileStore();
const { isCarouselVisible, isOpenNav, screenWidth } =
storeToRefs(mobileStore);
const slidesContent = [
'assets/1.jpg',
'assets/2.jpg',
@ -108,11 +138,11 @@ export default defineComponent({
const data = ref(null);
return {
slidesContent,
cardMock,
isCarouselVisible,
isOpenNav,
slidesContent,
screenWidth,
isOpenNav,
cardMock,
data,
};
},
@ -145,7 +175,7 @@ export default defineComponent({
@media only screen and (max-width: $med-xmd) {
padding-inline: 16px;
gap: 22px;
margin-bottom: 33px;
margin-bottom: 64px;
}
}
@ -175,7 +205,7 @@ export default defineComponent({
}
}
.products-selection {
.products-selection-section {
display: flex;
flex-direction: column;
justify-content: center;
@ -183,6 +213,23 @@ export default defineComponent({
background-color: $secondary-10;
padding-block: 104px 73px;
& .products-selection-body {
width: min(100%, 1440px);
margin: 0 auto 92px;
padding-inline: 76px;
height: 490px;
position: relative;
@media only screen and (max-width: $med-lg) {
height: 300px;
margin-bottom: 48px;
}
@media only screen and (max-width: $med-md) {
padding-inline: 16px;
}
}
@media only screen and (max-width: $med-xmd) {
padding-block: 48px 41px;
}

View File

@ -428,7 +428,7 @@ export default defineComponent({
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
gap: 84px;
& .product-pag-item {
margin-top: 76px;
&::before {

View File

@ -1,12 +1,35 @@
import { defineStore } from 'pinia';
import { AvailabilityForm } from 'src/utils/zod/schemas/availabilitySchema';
import type { CheckoutForm } from 'src/utils/zod/schemas/checkoutSchema';
import type { QuestionForm } from 'src/utils/zod/schemas/questionSchema';
export type Order = 'lowest-price' | 'highest-price' | 'latest' | 'recommended';
export type Category = 'ramos' | 'plantas';
interface UseFormStoreState {
sortProductFilters: {
isOpenOrderFilter: boolean;
order?: Order;
price?: number;
category: Category;
};
question: QuestionForm;
availability: AvailabilityForm;
checkout: CheckoutForm;
}
export const useFormStore = defineStore('forms', {
state: () => ({
state: (): UseFormStoreState => ({
sortProductFilters: {
isOpenOrderFilter: false,
order: undefined,
price: undefined,
category: 'ramos',
},
question: {
fist_name: '',
second_name: '',
name: '',
surname: '',
email: '',
telephone: '',
phone: '',
query: '',
message: '',
terms: false,
@ -15,10 +38,26 @@ export const useFormStore = defineStore('forms', {
date: '',
postalCode: '',
},
checkout: {
name: '',
surname: '',
address: '',
postalCode: '',
city: '',
province: '',
phone: '',
senderName: '',
senderCifNif: '',
senderEmail: '',
senderPhone: '',
senderNotes: '',
paymentMethod: 'credit',
terms: false,
},
}),
actions: {
handleQuestionData(values: typeof this.question) {
handleQuestionData(values: QuestionForm) {
console.log(values);
this.question = values;
},
@ -27,5 +66,10 @@ export const useFormStore = defineStore('forms', {
console.log(values);
this.availability = values;
},
handleCheckoutData(values: CheckoutForm) {
// console.log(values);
this.checkout = values;
},
},
});

1
src/types/global.ts Normal file
View File

@ -0,0 +1 @@
export type Modify<T, R> = Omit<T, keyof R> & R;

View File

@ -0,0 +1,7 @@
export function handlePhoneVal(val: string) {
const regex = /[\(\) ]/g;
const valWithoutSpaceAndParenteses = val.replace(regex, '');
const valLength = valWithoutSpaceAndParenteses.length;
return valLength > 0 && valLength === 11;
}

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
import * as M from './messages';
export const postalCode = z
.string({ required_error: M.requiredMessage })
.refine((val) => {
const valWithoutHifen = val.replaceAll('-', '');
const valLength = valWithoutHifen.length;
return valLength === 8 && valLength >= 1;
}, 'El código postal debe tener 8 caracteres numéricos válidos');

View File

@ -0,0 +1,7 @@
export const nameMessage =
'Sólo se aceptan una palabra y caracteres no numéricos';
export const phoneMessage =
'El número de teléfono debe contener 11 caracteres numéricos válidos';
export const requiredMessage = 'Campo obligatorio';
export const emailMessage =
'Introduzca una dirección de correo electrónico válida.';

1
src/utils/zod/regex.ts Normal file
View File

@ -0,0 +1 @@
export const justOneWord = /^[A-Za-z]+$/;

View File

@ -0,0 +1,18 @@
import { z } from 'zod';
import { postalCode } from './../globalProperties';
const availabilityObj = {
date: z.string().refine((val) => {
const [day, month, year] = val.split('/');
const regex = /\//g;
const valWithoutSlash = val.replace(regex, '');
const data = new Date(`${year}-${month}-${day}`);
const today = new Date();
return valWithoutSlash.length === 8 && data >= today;
}, 'La fecha no puede ser inferior al día de hoy!'),
postalCode,
};
export const availabilitySchema = z.object(availabilityObj);
export type AvailabilityForm = z.infer<typeof availabilitySchema>;

View File

@ -0,0 +1,45 @@
import { z } from 'zod';
import { handlePhoneVal } from '../functions';
import { postalCode } from '../globalProperties';
import { justOneWord } from '../regex';
import * as M from '../messages';
const checkoutObjVal = {
name: z
.string({ required_error: M.requiredMessage })
.regex(justOneWord, M.nameMessage),
surname: z
.string({ required_error: M.requiredMessage })
.regex(justOneWord, M.nameMessage),
address: z.string({ required_error: M.requiredMessage }),
postalCode,
city: z.string({ required_error: M.requiredMessage }).min(3),
province: z.string({ required_error: M.requiredMessage }).min(3),
phone: z
.string({ required_error: M.requiredMessage })
.refine(handlePhoneVal, M.phoneMessage),
senderName: z.string().regex(justOneWord, M.nameMessage),
senderCifNif: z
.string()
.length(9, 'El código postal debe tener 9 caracteres numéricos válidos'),
senderEmail: z.string().email(M.emailMessage),
senderPhone: z.string().refine(handlePhoneVal, M.phoneMessage),
senderNotes: z.string(),
paymentMethod: z.enum(['credit', 'stripe'], {
required_error: 'Seleccione uno de los métodos de pago',
}),
terms: z.boolean().refine((val) => {
return val === true;
}, 'Acepte las condiciones antes de continuar con la compra'),
};
export const checkoutSchema = z.object(checkoutObjVal).partial({
senderName: true,
senderCifNif: true,
senderEmail: true,
senderPhone: true,
senderNotes: true,
});
export type CheckoutForm = z.infer<typeof checkoutSchema>;

View File

@ -0,0 +1,29 @@
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';
import { handlePhoneVal } from '../functions';
import * as M from '../messages';
import { justOneWord } from '../regex';
const questionObjVal = {
name: z
.string({ required_error: M.requiredMessage })
.regex(justOneWord, M.nameMessage),
surname: z
.string({ required_error: M.requiredMessage })
.regex(justOneWord, M.nameMessage),
email: z.string({ required_error: M.requiredMessage }).email(M.emailMessage),
phone: z
.string({ required_error: M.requiredMessage })
.refine(handlePhoneVal, M.phoneMessage),
query: z.string({ required_error: M.requiredMessage }),
message: z.string({ required_error: M.requiredMessage }),
terms: z.boolean({ required_error: M.requiredMessage }).refine((val) => {
return val === true;
}),
};
const questionType = z.object(questionObjVal);
export type QuestionForm = z.infer<typeof questionType>;
export const questionSchema = toTypedSchema(z.object(questionObjVal));

View File

@ -2,5 +2,7 @@
"extends": "@quasar/app-vite/tsconfig-preset",
"compilerOptions": {
"baseUrl": "."
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}