Compare commits

...

2 Commits

Author SHA1 Message Date
012058b998 login 2025-06-27 16:02:00 +02:00
96dbc38c3a button with image 2025-06-26 21:04:01 +02:00
27 changed files with 1428 additions and 490 deletions

10
app.vue
View File

@ -1,7 +1,5 @@
<template> <template>
<div <div class="main bg-bg-light text-text-light dark:text-text-dark dark:bg-bg-dark">
class="main bg-bg-light text-text-light dark:text-text-dark dark:bg-bg-dark"
>
<UApp> <UApp>
<NuxtLayout> <NuxtLayout>
<NuxtPage /> <NuxtPage />
@ -9,4 +7,8 @@
</UApp> </UApp>
</div> </div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
const userStore = useUserStore()
// await userStore.checkIsLogged()
</script>

View File

@ -11,10 +11,12 @@
"@pinia/nuxt": "^0.11.0", "@pinia/nuxt": "^0.11.0",
"@tailwindcss/vite": "^4.1.8", "@tailwindcss/vite": "^4.1.8",
"@vueuse/core": "^13.3.0", "@vueuse/core": "^13.3.0",
"gsap": "^3.13.0",
"nuxt": "^3.17.4", "nuxt": "^3.17.4",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.7",
"vue": "^3.5.14", "vue": "^3.5.14",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"vue3-toastify": "^0.2.8",
}, },
"devDependencies": { "devDependencies": {
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",
@ -1290,6 +1292,8 @@
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"gsap": ["gsap@3.13.0", "", {}, "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw=="],
"gzip-size": ["gzip-size@7.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA=="], "gzip-size": ["gzip-size@7.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA=="],
"h3": ["h3@1.15.3", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ=="], "h3": ["h3@1.15.3", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ=="],
@ -2360,6 +2364,8 @@
"vue-router": ["vue-router@4.5.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw=="], "vue-router": ["vue-router@4.5.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw=="],
"vue3-toastify": ["vue3-toastify@0.2.8", "", { "peerDependencies": { "vue": ">=3.2.0" }, "optionalPeers": ["vue"] }, "sha512-8jDOqsJaBZEbGpCbhWDETJc11D1lZefvgFPq/IPdM+U7+qyXoVPDvK6uq/FIgyV7qV0NcNzvGBMEzjsLQqGROw=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],

BIN
bun.lockb Executable file

Binary file not shown.

View File

@ -7,7 +7,7 @@
{{ productStore.cart.cart_items.length }}</div> {{ productStore.cart.cart_items.length }}</div>
</div> </div>
<div v-if="openCart" @click.self="openCart = !openCart" <div v-if="openCart" @click.self="openCart = !openCart"
class="absolute left-1/2 transform -translate-x-1/2 w-full px-4 sm:max-w-[768px] sm:px-[17px] md:max-w-[1000px] md:px-6 xl:max-w-[1920px] xl:px-20 right-0 z-50 flex items-center justify-end top-[90px] sm:top-[100px] md:top-[140px]"> class="absolute left-1/2 transform -translate-x-1/2 w-full px-4 sm:max-w-[768px] sm:px-[17px] md:max-w-[1000px] md:px-6 xl:max-w-[1920px] xl:px-20 right-0 z-50 flex items-center justify-end top-[100px] sm:top-[100px] md:top-[140px]">
<div class="xl:w-[55%] md:w-[90%] w-full px-4 md:px-0"> <div class="xl:w-[55%] md:w-[90%] w-full px-4 md:px-0">
<div v-if="productStore.cart.cart_items && productStore.cart.cart_items.length > 0" <div v-if="productStore.cart.cart_items && productStore.cart.cart_items.length > 0"
class="w-full p-[25px] sm:p-[50px] bg-bg-light dark:bg-bg-dark border border-button rounded-xl sm:rounded-[32px] h-full space-25-55"> class="w-full p-[25px] sm:p-[50px] bg-bg-light dark:bg-bg-dark border border-button rounded-xl sm:rounded-[32px] h-full space-25-55">
@ -36,7 +36,7 @@
class="text-accent-green-light dark:text-accent-green-dark font-inter text-[12px] sm:text-[21px] md:text-2xl leading-[150%] font-bold"> class="text-accent-green-light dark:text-accent-green-dark font-inter text-[12px] sm:text-[21px] md:text-2xl leading-[150%] font-bold">
{{ item.total_price }} {{ item.total_price }}
</p> </p>
<div class="flex items-center gap-[2px] sm:gap-4 text-xl"> <div class="flex items-center gap-[2px] sm:gap-2 text-xl">
<div <div
class="w-5 min-h-5 sm:w-11 sm:min-h-11 text-[10px] sm:text-lg flex items-center justify-center"> class="w-5 min-h-5 sm:w-11 sm:min-h-11 text-[10px] sm:text-lg flex items-center justify-center">
<i class="uil uil-minus cursor-pointer text-gray dark:text-button-disabled hover:text-gray-200 transition-all" <i class="uil uil-minus cursor-pointer text-gray dark:text-button-disabled hover:text-gray-200 transition-all"
@ -65,15 +65,18 @@
{{ productStore.cart.total_value }} {{ productStore.cart.total_value }}
</p> </p>
</div> </div>
<UiButtonArrow class="w-full" type="fill" :arrow="true" :full="true">{{ $t('to_checkout') }} <UiButtonArrow @click="menuStore.navigateToItem(menuStore.menuItems?.find((item) => item.id === 12))"
class="w-full" type="fill" :arrow="true" :full="true">{{
$t('to_checkout') }}
</UiButtonArrow> </UiButtonArrow>
</div> </div>
<div v-else <div v-else
class="w-full p-[50px] bg-bg-light dark:bg-bg-dark border border-button rounded-[32px] h-[400px] flex items-center justify-center"> class="w-full p-[25px] sm:p-[50px] bg-bg-light dark:bg-bg-dark border border-button rounded-xl sm:rounded-[32px] flex items-center justify-center">
<div <div
class="border border-block inline-flex items-center justify-center w-[30%] h-[200px] rounded-[8px]"> class="border border-block inline-flex items-center justify-center h-[140px] sm:h-[200px] text-center rounded-[8px]">
<h4 class="font-inter text-base leading-[150%] font-bold uppercase sm:text-[20px] md:text-xl"> <h4
košík je prázdný</h4> class="font-inter text-base leading-[150%] font-bold uppercase sm:text-[20px] px-4 sm:px-10 md:text-xl">
{{ $t('empty_cart') }}</h4>
</div> </div>
</div> </div>
</div> </div>
@ -83,10 +86,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onClickOutside } from "@vueuse/core"; import { onClickOutside } from "@vueuse/core";
const count = ref(1)
const productStore = useProductStore() const productStore = useProductStore()
const openCart = ref(false); const openCart = ref(false);
const menuStore = useMenuStore()
const dropdownRef = ref(null); const dropdownRef = ref(null);
onClickOutside(dropdownRef, () => { onClickOutside(dropdownRef, () => {
openCart.value = false openCart.value = false

View File

@ -18,7 +18,8 @@
</ClientOnly> </ClientOnly>
<div class="w-full flex items-center justify-between"> <div class="w-full flex items-center justify-between">
<div class="flex items-center gap-[30px]"> <div class="flex items-center gap-[30px]">
<i class="uil uil-user text-[31px] cursor-pointer"></i> <i @click="menuStore.navigateToItem(menuStore.menuItems?.find((item) => item.id === 11))"
class="uil uil-user text-[31px] cursor-pointer"></i>
<CartPopup /> <CartPopup />
</div> </div>
<div class="flex"> <div class="flex">

View File

@ -0,0 +1,6 @@
<template>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Modi perspiciatis adipisci quam odio natus odit excepturi
eveniet vitae. Fugit dicta officiis quos quia debitis perspiciatis porro ducimus earum placeat sunt?
</template>
<script lang="ts"></script>

View File

@ -1,7 +1,196 @@
<template> <template>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Veritatis dignissimos impedit eligendi quaerat. Doloribus <UiContainer>
eius nisi facere suscipit pariatur reprehenderit reiciendis tempora est accusamus adipisci hic voluptate asperiores, <form class="w-[85%] mx-auto" @submit.prevent="checkoutStore.sendForm">
voluptas enim. <div v-if="userStore.isLogged" class="space-25-55">
<div class="w-full flex items-center justify-center">
<div class="flex justify-between">
<div class="flex items-center gap-[25px] text-gray dark:text-button-disabled">
<div class="px-6 py-3 mx-auto">
{{ $t("login") }}
</div>
<div
class="cursor-pointer transition-all text-inter hover:bg-button-hover bg-button text-white font-medium rounded-xl px-6 py-3">
{{ $t("address") }}
</div>
<div class="px-6 py-3 mx-auto">
{{ $t("summary") }}
</div>
<div class="px-6 py-3 mx-auto">
{{ $t("order_placed") }}
</div>
</div>
</div>
</div>
<div class="space-y-[30px]">
<h2 class="h2-bold-bounded">
{{ $t("Account address") }}
</h2>
<div class="flex flex-col gap-[30px] xl:flex-row">
<div class="flex flex-col w-1/2 gap-[30px]">
<CheckoutInput v-model="checkoutStore.userName" :id="1" disabled>{{ $t("first_name")
}} </CheckoutInput>
<CheckoutInput v-model="checkoutStore.lastName" :id="2" disabled>{{ $t("surname")
}} </CheckoutInput>
<CheckoutInput v-model="checkoutStore.address" :id="3" disabled>{{ $t("address")
}} </CheckoutInput>
<CheckoutInput v-model="checkoutStore.postCode" :id="4" disabled>{{ $t("post_code")
}} </CheckoutInput>
</div>
<div class="flex flex-col w-1/2 gap-[30px]">
<CheckoutInput v-model="checkoutStore.city" :id="5" disabled>{{ $t("city")
}} </CheckoutInput>
<CheckoutInput v-model="checkoutStore.country" :id="6" disabled>{{ $t("country")
}} </CheckoutInput>
<CheckoutInput v-model="checkoutStore.phoneNumber" :id="7" disabled>{{ $t("phone")
}} </CheckoutInput>
</div>
</div>
</div>
<div class="space-y-[30px]">
<h2 class="h2-bold-bounded">
{{ $t("Shipping details") }}
</h2>
<div class="relative">
<input v-model="checkoutStore.accountPhoneNumber" type="text"
class="border border-block placeholder:text-bg-dark dark:placeholder:text-bg-light rounded-lg px-6 h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2 text-button" />
</div>
<div class="flex items-center gap-2">
<input @change="(event: Event) => {
const target = event.target as HTMLInputElement
target.checked ? checkoutStore.vUseAccountPhoneNumber = true : checkoutStore.vUseAccountPhoneNumber = false
}" type="checkbox" class="border border-button !bg-inherit" />
<p>{{ $t('use_account_phone') }}</p>
</div>
</div>
<div class="space-y-[30px]">
<h2 class="h2-bold-bounded">
{{ $t("Select delivery address") }}
</h2>
<div class="flex items-center justify-center gap-[25px] h-[225px]">
<div class="w-[500px] flex flex-col gap-4 h-full">
<div v-for="(item, index) in checkoutStore.addressesList" :key="index"
:class="['flex h-full flex-col py-[15px] px-[25px] gap-[15px] rounded-lg border-2', checkoutStore.activeAddress === item ? 'border-button' : 'border-block']">
<div
:class="['flex flex-col justify-between mt-1 h-full', checkoutStore.activeAddress !== item && 'text-gray dark:text-button-disabled']">
<span>{{ item.address.name }} {{ item.address.surname }}</span>
<span>{{ item.address.street }}</span>
<span>{{ item.address.postcode }} {{ item.address.city }}</span>
<span>{{ item.address.country_iso }}</span>
</div>
<div class="flex items-center gap-2 border-t pt-[15px] border-block">
<input :checked="checkoutStore.activeAddress ? true : false" @change="(event: Event) => {
const target = event.target as HTMLInputElement
target.checked ? checkoutStore.activeAddress = item : checkoutStore.activeAddress = null;
checkoutStore.isOpen = false;
}" type="checkbox" class="border border-button !bg-inherit" />
<p>{{ $t('choose_default_address') }}</p>
</div>
</div>
</div>
<p class="uppercase">{{ $t("or") }}</p>
<div @click="() => {
checkoutStore.isOpen = !checkoutStore.isOpen
checkoutStore.activeAddress = null
}"
:class="['cursor-pointer w-[500px] py-[15px] px-[25px] rounded-lg border-2 flex flex-col items-center justify-center h-full', checkoutStore.isOpen ? 'border-button text-button' : 'text-gray border-block ']">
<h4
:class="['font-inter text-base leading-[150%] uppercase text-[16px] sm:text-[20px] border-b', checkoutStore.isOpen ? 'border-button' : 'border-gray']">
{{ $t("add_new_address") }}
</h4>
</div>
</div>
<div v-if="checkoutStore.isOpen"
class="flex flex-col items-center gap-[30px] justify-center w-full">
<div class="flex flex-col gap-[30px] xl:flex-row w-full">
<div class="flex flex-col w-1/2 gap-[30px]">
<CheckoutInput v-model="checkoutStore.vNewAddressName" :placeholder="$t('first_name')"
:id="8">{{ $t("first_name") }}
</CheckoutInput>
<CheckoutInput v-model="checkoutStore.vNewAddressAddress" :placeholder="$t('address')"
:id="9">{{ $t("address") }}
</CheckoutInput>
<CheckoutInput v-model="checkoutStore.vNewAddressCity" :placeholder="$t('city')"
:id="10">{{ $t("city") }}
</CheckoutInput>
</div>
<div class="flex flex-col w-1/2 gap-[30px]">
<CheckoutInput v-model="checkoutStore.vNewAddressSurname" :placeholder="$t('surname')"
:id="11">{{ $t("surname") }}
</CheckoutInput>
<CheckoutInput v-model="checkoutStore.vNewAddressCountry" :placeholder="$t('country')"
:id="12">{{ $t("country") }}
</CheckoutInput>
<CheckoutInput v-model="checkoutStore.vNewAddressCode" :placeholder="$t('post_code')"
:id="13">{{ $t("post_code") }}
</CheckoutInput>
</div>
</div>
<form @submit.prevent="checkoutStore.uploadAddress()">
<span v-if="addressValidation === false" class="text-red"> {{
$t("Remember to select a shipping address") }}</span>
<div class="group flex cursor-pointer items-center justify-start gap-2 whitespace-nowrap">
<button type="submit"
:class="['h-[40px] cursor-pointer min-w-40 rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px] bg-button text-text-dark group-hover:bg-button-hover']">
{{ $t("add_new_address") }}
</button>
</div>
</form>
</div>
</div>
<div class="flex justify-center">
<div class="group flex cursor-pointer items-center justify-start gap-2 whitespace-nowrap">
<button
:class="['h-[40px] cursor-pointer min-w-40 rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px]', checkoutStore.activeAddress ? 'bg-button text-text-dark group-hover:bg-button-hover' : ' bg-button-disabled text-gray']">
{{ $t("continue") }}
</button>
<div
:class="['flex h-[40px] w-[40px] items-center justify-center rounded-[10px] p-2.5 transition-all sm:h-[50px] sm:w-[50px] md:h-[65px] md:w-[65px] md:rounded-[15px]', checkoutStore.activeAddress ? 'bg-button text-text-dark group-hover:bg-button-hover' : ' bg-button-disabled text-gray']">
<svg class="" width=" 26" height="26" viewBox="0 0 26 26" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M25.1274 1.87258C25.1274 1.3203 24.6797 0.872582 24.1274 0.872584L15.1274 0.872583C14.5751 0.872583 14.1274 1.3203 14.1274 1.87258C14.1274 2.42487 14.5751 2.87258 15.1274 2.87258L23.1274 2.87258L23.1274 10.8726C23.1274 11.4249 23.5751 11.8726 24.1274 11.8726C24.6797 11.8726 25.1274 11.4249 25.1274 10.8726L25.1274 1.87258ZM1.5 24.5L2.20711 25.2071L24.8345 2.57969L24.1274 1.87258L23.4203 1.16548L0.792893 23.7929L1.5 24.5Z"
fill="currentColor" />
</svg>
</div>
</div>
</div>
</div>
</form>
</UiContainer>
</template> </template>
<script lang="ts" setup></script> <script setup lang="ts">
import CheckoutInput from '../ui/CheckoutInput.vue';
const checkoutStore = useCheckoutStore();
const userStore = useUserStore()
const router = useRoute();
const addressValidation = ref<null | boolean>(null);
watch(
() => checkoutStore.vUseAccountPhoneNumber,
(newValue) => {
if (newValue == true) {
checkoutStore.accountPhoneNumber = checkoutStore.phoneNumber;
} else {
checkoutStore.accountPhoneNumber = "";
}
}
);
// fetchWithCookie(useRequestEvent(), `restricted/cart/checkout`, {
// method: "PUT",
// });
// if change needs to be applied then provide it in second argument of function below
// $setSeoMetaFromTranslation(useStore(), {});
checkoutStore.restrictedAddress()
checkoutStore.restrictedAddressOfficial()
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<UiContainer class="flex py-20 sm:py-14"> <UiContainer v-if="!userStore.vCodeVerify" class="flex py-20 sm:py-14">
<div class="hidden xl:block rounded-2xl min-w-[60%] h-[830px]" :style="{ <div class="hidden xl:block rounded-2xl min-w-[60%] h-[830px]" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`, backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
}" /> }" />
@ -9,46 +9,111 @@
<div class="space-25-55"> <div class="space-25-55">
<div class="flex flex-wrap-reverse gap-y-4 justify-between"> <div class="flex flex-wrap-reverse gap-y-4 justify-between">
<h2 class="h2-bold-bounded">{{ $t('login') }}</h2> <h2 class="h2-bold-bounded">{{ $t('login') }}</h2>
<button <button @click="menuStore.navigateToItem()"
class="h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer">{{ class="h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer">{{
$t('back_to_home') }}</button> $t('back_to_home') }}</button>
</div> </div>
<div class="space-y-[15px]"> <div class="space-y-[15px]">
<p class="pl-6">{{ $t('email') }}</p> <p class="pl-6">{{ $t('email') }}</p>
<input v-model="store.email" :placeholder="$t('email')" type="text" <input v-model="userStore.email" :placeholder="$t('email')" type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" /> class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" />
</div> </div>
<div class="space-y-[15px]"> <div class="space-y-[15px]">
<p class="pl-6">{{ $t('password') }}</p> <p class="pl-6">{{ $t('password') }}</p>
<input v-model="store.password" :placeholder="$t('placeholder_password')" type="text" <input v-model="userStore.password" :placeholder="$t('placeholder_password')" type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" /> class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" />
</div> </div>
</div> </div>
<p class="text-button hover:text-button-hover transition-all font-medium mt-[30px] cursor-pointer">{{ <p class="text-button hover:text-button-hover transition-all font-medium mt-[30px] cursor-pointer">{{
$t('forgot_password_question') $t('forgot_password_question')
}}</p> }}</p>
<div class="py-[25px] sm:py-12 border-b border-gray flex justify-center w-full"> <div class="py-[25px] sm:py-12 border-b border-gray flex justify-center w-full">
<UiButtonArrow @click="store.logIn()" type="fill" :arrow="true">{{ $t('login') }}</UiButtonArrow> <UiButtonArrow @click="userStore.logIn()" type="fill" :arrow="true">{{ $t('login') }}</UiButtonArrow>
</div> </div>
<div class="mt-[25px] sm:mt-[30px] w-full flex justify-center gap-3"> <div class="mt-[25px] sm:mt-[30px] w-full flex justify-center gap-3">
<p class="cursor-pointer hover:underline transition-all">{{ <p class="cursor-pointer hover:underline transition-all">{{
$t('no_account') $t('no_account')
}}</p> }}</p>
<p class="text-button cursor-pointer hover:text-button-hover">{{ <p class="text-button cursor-pointer hover:text-button-hover">{{
$t('sign_up_now') $t('sign_up_now')
}}</p> }}</p>
</div> </div>
</div> </div>
</UiContainer> </UiContainer>
<UiContainer v-if="!userStore.vCodeVerify" class="flex py-20 sm:py-14">
<div class="hidden xl:block rounded-2xl min-w-[50%] h-[830px]" :style="{
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<div class="w-full sm:w-[80%] mx-auto my-auto xl:w-full xl:px-12 ">
<div class="space-25-55">
<div class="space-y-[15px]">
<div class="flex flex-wrap-reverse gap-y-4 justify-between">
<h2 class="h2-bold-bounded">{{ $t('verify_login') }}</h2>
<button @click="menuStore.navigateToItem()"
class="h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer">{{
$t('back_to_home') }}</button>
</div>
<p>{{ $t('send_code') }} <span class="text-button font-semibold">test@ma-al.com</span> {{
$t('by_email') }}
</p>
</div>
<div class="space-y-[15px]">
<p class="pl-6">{{ $t('code') }}</p>
<input v-model="userStore.vCode" :placeholder="$t('code')" type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" />
</div>
</div>
<div class="py-[25px] sm:py-12 flex justify-center w-full">
<UiButtonArrow @click="userStore.logIn()" type="fill" :arrow="true">{{ $t('confirm') }}</UiButtonArrow>
</div>
</div>
</UiContainer>
<!-- v-if="store.vCodeVerify" -->
<div class="max-w-[590px] w-full">
<h3 class="mb-1 text-2xl font-bold">
{{ $t("FrontTranslations", "Verify your login") }}
</h3>
<span class="text-sm font-bold text-black">
{{ $t("FrontTranslations", "We send code to") }} <span class="text-yellow"> {{ userStore.vEmail }}</span>
{{ $t("FrontTranslations", "by") }} e-mail.</span>
<!-- <form
@submit.prevent="useUserStore().sendFormCode(`${vCode}`, vEmail, useRoute().query.to ? (useRoute().query.to as string) : undefined)">
<div class="flex flex-col w-full gap-10 mt-6">
<BaseInput v-model="vCode" :id="1"> {{ $t("FrontTranslations", "Code") }}</BaseInput>
</div>
<div class="flex flex-col">
<div class="flex">
<BaseButtonLogin id="2" type="submit">
{{ $t("FrontTranslations", "Confirm") }}
</BaseButtonLogin>
</div>
</div>
</form> -->
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ component: Component }>(); defineProps<{
type Component = { component: {
image_collection: string; id: number
section_id: string; name: string
section_img: string; img: string[]
section_lang_data: {}; component_name: string
}; is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
}
id_front_section: number
id_lang: number
}[]
}
}>();
const store = useStore() const store = useStore()
const userStore = useUserStore()
const menuStore = useMenuStore()
</script> </script>

View File

@ -17,22 +17,12 @@
<p>{{ item.description }}</p> <p>{{ item.description }}</p>
<h4 class="h4-uppercase-bold-inter">{{ item.sub_title }}</h4> <h4 class="h4-uppercase-bold-inter">{{ item.sub_title }}</h4>
</div> </div>
<UiImgWrapper :src="`/api/public/file/${component.img[index]}_l.webp`">
<div <template #button>
class="h-[235px] sm:h-[350px] w-full rounded-[20px] bg-cover bg-center transition-transform duration-300 group-hover:scale-105 xl:block relative"
:style="{
backgroundImage: `url('/api/public/file/${component.img[index]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}">
<div
class="hidden sm:block absolute bottom-0 right-0 pt-2 pl-2 bg-bg-light dark:bg-bg-dark rounded-tl-2xl">
<UiButtonArrow :arrow="true">{{ item.title }}</UiButtonArrow> <UiButtonArrow :arrow="true">{{ item.title }}</UiButtonArrow>
</div> </template>
</div> </UiImgWrapper>
<UiButtonArrow :arrow="true" class="sm:hidden mx-auto">{{
item.title
}}</UiButtonArrow>
</div> </div>
<!-- Map block with same layout rules --> <!-- Map block with same layout rules -->

View File

@ -10,18 +10,15 @@
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`, backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
}" }" />
/> <div class="flex flex-col items-center space-y-[30px] sm:flex-row sm:items-start md:space-y-0 xl:items-center">
<div
class="flex flex-col items-center space-y-[30px] sm:flex-row sm:items-start md:space-y-0 xl:items-center"
>
<h3 class="h4-uppercase-bold-inter sm:min-w-[45%]"> <h3 class="h4-uppercase-bold-inter sm:min-w-[45%]">
{{ component.front_section_lang[0].data.title_second }} {{ component.front_section_lang[0].data.title_second }}
</h3> </h3>
<div class="flex w-full items-start justify-center sm:justify-end"> <div class="flex w-full items-start justify-center sm:justify-end">
<UiButtonArrow :arrow="true" type="fill">{{ <UiButtonArrow :arrow="true" type="fill">{{
component.front_section_lang[0].data.button component.front_section_lang[0].data.button
}}</UiButtonArrow> }}</UiButtonArrow>
</div> </div>
</div> </div>
</div> </div>
@ -29,22 +26,25 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ component: { defineProps<{
id: number component: {
name: string id: number
img: string[] name: string
component_name: string img: string[]
is_no_lang: boolean component_name: string
page_name: string is_no_lang: boolean
front_section_lang: { page_name: string
data: { front_section_lang: {
title: string data: {
button: string title: string
title_second: string button: string
} title_second: string
id_front_section: number }
id_lang: number id_front_section: number
}[] id_lang: number
} }>(); }[]
}
}>();
</script> </script>

View File

@ -2,7 +2,7 @@
<div <div
class="w-[150px] sm:w-[260px] md:w-[330px] px-2 py-3 sm:py-5 sm:px-[15px] bg-block rounded-2xl flex flex-col items-center gap-[15px] sm:gap-[50px]"> class="w-[150px] sm:w-[260px] md:w-[330px] px-2 py-3 sm:py-5 sm:px-[15px] bg-block rounded-2xl flex flex-col items-center gap-[15px] sm:gap-[50px]">
<img :src="`https://www.yourgold.cz/api/public/file/${props.product?.cover_picture_uuid}.webp`" alt="Product Image" <img :src="`https://www.yourgold.cz/api/public/file/${props.product?.cover_picture_uuid}.webp`" alt="Product Image"
class="max-h-[95px] sm:max-h-[180px] md:max-h-[205px] rounded-[5px]" class="h-[95px] sm:h-[180px] md:h-[205px] rounded-[5px]"
onerror="this.onerror=null; this.src='/photo.svg';" /> onerror="this.onerror=null; this.src='/photo.svg';" />
<div class="flex flex-col justify-between h-full w-full gap-[7px] sSm:gap-[15px]"> <div class="flex flex-col justify-between h-full w-full gap-[7px] sSm:gap-[15px]">

View File

@ -1,91 +1,135 @@
<template> <template>
<SectionShopPageCurrencyRatesBar class="mb-[25px] sm:mb-[55px] xl:mb-[75px]" /> <SectionShopPageCurrencyRatesBar
<UiContainer> class="mb-[25px] sm:mb-[55px] xl:mb-[75px]"
<div class="flex flex-col gap-[25px] sm:gap-10 xl:flex-row"> />
<!-- button to open categories --> <UiContainer>
<div class="xl:hidden flex items-center w-full"> <div class="flex flex-col gap-[25px] sm:gap-10 xl:flex-row">
<button @click="openCategories = !openCategories" <!-- button to open categories -->
class="h-[40px] w-full cursor-pointer rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px] bg-button text-text-dark group-hover:bg-button-hover"> <div class="xl:hidden flex items-center w-full">
Otevřené kategorie a filtry <button
</button> class="h-[40px] w-full cursor-pointer rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px] bg-button text-text-dark group-hover:bg-button-hover"
</div> @click="openCategories = !openCategories"
>
Otevřené kategorie a filtry
</button>
</div>
<Transition> <Transition>
<div v-if="openCategories" class="min-w-[250px] px-5 sm:p-0 xl:hidden"> <div v-if="openCategories" class="min-w-[250px] px-5 sm:p-0 xl:hidden">
<h1 class="font-bounded leading-[140%] font-bold text-[24px] mb-[25px]"> <h1
{{ $t("category") }} class="font-bounded leading-[140%] font-bold text-[24px] mb-[25px]"
</h1> >
<div class="flex flex-col gap-[25px]"> {{ $t("category") }}
<div> </h1>
<div v-if="categoriesList && categoriesList.length < 1" class="animate-pulse"> <div class="flex flex-col gap-[25px]">
<div <div>
class="flex items-center justify-between mt-4 text-white rounded-lg cursor-pointer xl:pr-24"> <div
<div class="w-32 h-4 bg-gray-200 rounded"></div> v-if="categoriesList && categoriesList.length < 1"
<div class="w-4 h-4 bg-gray-200 rounded-full"></div> class="animate-pulse"
</div> >
</div> <div
<CategoryTree :data="categoriesList" @change-category="changeCategory($event)" class="flex items-center justify-between mt-4 text-white rounded-lg cursor-pointer xl:pr-24"
:active="categoryId" /> >
</div> <div class="w-32 h-4 bg-gray-200 rounded" />
<div> <div class="w-4 h-4 bg-gray-200 rounded-full" />
<p class="mb-[25px] text-lg font-extrabold text-black dark:text-white">
{{ $t("filtered_by") }}
</p>
<div v-for="(item, itemIndex) in filters" :key="itemIndex"
:class="['mb-[30px]', visibleFeatures[item.feature] && 'border-b border-block pb-2']">
<span
class="flex justify-between items-center font-bold cursor-pointer mb-[25px] text-base"
@click="toggleFeature(item.feature)">
{{ item.feature }}
<span :class="[visibleFeatures[item.feature] && 'rotate-180', 'transition-all']"><i
class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto"></i></span>
</span>
<ul v-show="visibleFeatures[item.feature]" class="flex flex-col gap-5">
<li v-for="filter in item.feature_values" :key="filter.value_id"
class="flex items-center gap-[10px] cursor-pointer">
<input :id="`${filter.value_id}`" :value="`${filter.parent}.${filter.value_id}`"
v-model="selectedFilters" type="checkbox"
class="border-button !bg-inherit" />
<label :for="`${filter.value_id}`"
class="cursor-pointer flex items-center justify-between w-full text-base">
<span>{{ filter.value }}</span>
<span>12</span>
</label>
</li>
</ul>
</div>
</div>
</div>
</div> </div>
</Transition> </div>
<CategoryTree
:data="categoriesList"
:active="categoryId"
@change-category="changeCategory($event)"
/>
</div>
<div>
<p
class="mb-[25px] text-lg font-extrabold text-black dark:text-white"
>
{{ $t("filtered_by") }}
</p>
<!-- categories --> <div
<div class="min-w-[250px] hidden xl:block"> v-for="(item, itemIndex) in filters"
<h1 class="font-bounded leading-[140%] font-bold text-[40px] mb-[55px]"> :key="itemIndex"
{{ $t("category") }} :class="[
</h1> 'mb-[30px]',
<div class="flex flex-col gap-12"> visibleFeatures[item.feature] && 'border-b border-block pb-2',
<div> ]"
>
<span
class="flex justify-between items-center font-bold cursor-pointer mb-[25px] text-base"
@click="toggleFeature(item.feature)"
>
{{ item.feature }}
<span
:class="[
visibleFeatures[item.feature] && 'rotate-180',
'transition-all',
]"
><i
class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto"
/></span>
</span>
<ul
v-show="visibleFeatures[item.feature]"
class="flex flex-col gap-5"
>
<li
v-for="filter in item.feature_values"
:key="filter.value_id"
class="flex items-center gap-[10px] cursor-pointer"
>
<input
:id="`${filter.value_id}`"
v-model="selectedFilters"
:value="`${filter.parent}.${filter.value_id}`"
type="checkbox"
class="border-button !bg-inherit"
/>
<label
:for="`${filter.value_id}`"
class="cursor-pointer flex items-center justify-between w-full text-base"
>
<span>{{ filter.value }}</span>
<span>12</span>
</label>
</li>
</ul>
</div>
</div>
</div>
</div>
</Transition>
<div v-if="categoriesList && categoriesList.length < 1" class="animate-pulse"> <!-- categories -->
<div <div class="min-w-[250px] hidden xl:block">
class="flex items-center justify-between mt-4 text-white rounded-lg cursor-pointer xl:pr-24"> <h1 class="font-bounded leading-[140%] font-bold text-[40px] mb-[55px]">
<div class="w-32 h-4 bg-gray-200 rounded"></div> {{ $t("category") }}
<div class="w-4 h-4 bg-gray-200 rounded-full"></div> </h1>
</div> <div class="flex flex-col gap-12">
</div> <div>
<CategoryTree :data="categoriesList" @change-category="changeCategory($event)" <div
:active="categoryId" /> v-if="categoriesList && categoriesList.length < 1"
</div> class="animate-pulse"
<div> >
<p class="mb-10 text-2xl font-extrabold text-black dark:text-white"> <div
{{ $t("filtered_by") }} class="flex items-center justify-between mt-4 text-white rounded-lg cursor-pointer xl:pr-24"
</p> >
<div class="w-32 h-4 bg-gray-200 rounded" />
<div class="w-4 h-4 bg-gray-200 rounded-full" />
</div>
</div>
<CategoryTree
:data="categoriesList"
:active="categoryId"
@change-category="changeCategory($event)"
/>
</div>
<div>
<p class="mb-10 text-2xl font-extrabold text-black dark:text-white">
{{ $t("filtered_by") }}
</p>
<!-- <div v-if="filters.length < 1" class="mb-8 text-white animate-pulse"> <!-- <div v-if="filters.length < 1" class="mb-8 text-white animate-pulse">
<div v-for="i in 5" <div v-for="i in 5"
class="mt-10 flex justify-between font-bold text-black cursor-pointer 2xl:pr-24 dark:text-gray"> class="mt-10 flex justify-between font-bold text-black cursor-pointer 2xl:pr-24 dark:text-gray">
<div class="w-32 h-4 bg-gray-200 rounded"></div> <div class="w-32 h-4 bg-gray-200 rounded"></div>
@ -93,110 +137,162 @@
</div> </div>
</div> --> </div> -->
<div v-for="(item, itemIndex) in filters" :key="itemIndex" <div
:class="['mb-[30px]', visibleFeatures[item.feature] && 'border-b border-block pb-2']"> v-for="(item, itemIndex) in filters"
<span class="flex justify-between items-center font-bold cursor-pointer mb-[25px]" :key="itemIndex"
@click="toggleFeature(item.feature)"> :class="[
{{ item.feature }} 'mb-[30px]',
<span :class="[visibleFeatures[item.feature] && 'rotate-180', 'transition-all']"><i visibleFeatures[item.feature] && 'border-b border-block pb-2',
class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto"></i></span> ]"
>
<span
</span> class="flex justify-between items-center font-bold cursor-pointer mb-[25px]"
<ul v-show="visibleFeatures[item.feature]" class="flex flex-col gap-5"> @click="toggleFeature(item.feature)"
<li v-for="filter in item.feature_values" :key="filter.value_id" >
class="flex items-center gap-[10px] cursor-pointer"> {{ item.feature }}
<!-- <input :id="`${filter.value_id}`" :value="`${filter.parent}.${filter.value_id}`" <span
:class="[
visibleFeatures[item.feature] && 'rotate-180',
'transition-all',
]"
><i
class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto"
/></span>
</span>
<ul
v-show="visibleFeatures[item.feature]"
class="flex flex-col gap-5"
>
<li
v-for="filter in item.feature_values"
:key="filter.value_id"
class="flex items-center gap-[10px] cursor-pointer"
>
<!-- <input :id="`${filter.value_id}`" :value="`${filter.parent}.${filter.value_id}`"
v-model="selectedFilters" type="checkbox" class="border-button !bg-inherit" /> v-model="selectedFilters" type="checkbox" class="border-button !bg-inherit" />
<label :for="`${filter.value_id}`" class="cursor-pointer">{{ filter.value }}</label> --> <label :for="`${filter.value_id}`" class="cursor-pointer">{{ filter.value }}</label> -->
<input
<input :id="`${filter.value_id}`" :value="`${filter.parent}.${filter.value_id}`" :id="`${filter.value_id}`"
v-model="selectedFilters" type="checkbox" class="border-button !bg-inherit" /> v-model="selectedFilters"
<label :for="`${filter.value_id}`" :value="`${filter.parent}.${filter.value_id}`"
class="cursor-pointer flex items-center justify-between w-full"> type="checkbox"
<span>{{ filter.value }}</span> class="border-button !bg-inherit"
<span>12</span> />
</label> <label
:for="`${filter.value_id}`"
</li> class="cursor-pointer flex items-center justify-between w-full"
</ul> >
</div> <span>{{ filter.value }}</span>
</div> <span>12</span>
</div> </label>
</div> </li>
</ul>
<div class="w-full space-y-10">
<!-- pop-up -->
<div v-if="isInfo"
class="w-full xl:w-[70%] mx-auto border-y border-block py-[15px] sm:p-[30px] flex gap-[55px] relative">
<UButton @click="closeElement()" size="xl" icon="i-lucide-x" variant="ghost"
class="p-0 absolute right-0 top-2 sm:right-2 sm:top-2 cursor-pointer text-button font-light hover:bg-inherit hover:text-button-hover" />
<div class="flex flex-col sm:flex-row gap-[25px]">
<div class="flex flex-col justify-between gap-[25px]">
<h4 class="font-inter text-lg sm:text-[24px] leading-[150%] md:leading-[120%] font-bold">
{{ component.front_section_lang[0].data.title }}
</h4>
<p>{{ component.front_section_lang[0].data.description }}</p>
</div>
<img class="max-w-[150px] mx-auto" :src="`/api/public/file/${component.img[0]}_m.webp')`" />
</div>
</div>
<div v-if="products.length < 1" class="grid gap-12 pt-32 pb-16 md:grid-cols-2 2xl:grid-cols-3">
<TheProductSkeleton v-for="index in 6"></TheProductSkeleton>
</div>
<!-- products -->
<div v-else ref="loadingElement" class="flex flex-wrap justify-center gap-5 sm:gap-10">
<Product v-for="product in products" :key="product.id" :product="product" />
</div>
<div v-if="reachedEnd" class="w-full flex justify-center">
<p>
{{ $t("FrontTranslations", "You reached end of the list.") }}
</p>
</div>
</div> </div>
</div>
</div> </div>
</UiContainer> </div>
<div class="w-full space-y-10">
<!-- pop-up -->
<div
v-if="isInfo"
class="w-full xl:w-[70%] mx-auto border-y border-block py-[15px] sm:p-[30px] flex gap-[55px] relative"
>
<UButton
variant="ghost"
class="p-0 absolute right-0 top-2 sm:right-2 sm:top-2 cursor-pointer text-button font-light hover:bg-inherit hover:text-button-hover"
size="xl"
icon="i-lucide-x"
@click="closeElement()"
/>
<div class="flex flex-col sm:flex-row gap-[25px]">
<div class="flex flex-col justify-between gap-[25px]">
<h4
class="font-inter text-lg sm:text-[24px] leading-[150%] md:leading-[120%] font-bold"
>
{{ component.front_section_lang[0].data.title }}
</h4>
<p>{{ component.front_section_lang[0].data.description }}</p>
</div>
<img
class="max-w-[150px] mx-auto"
:src="`/api/public/file/${component.img[0]}_m.webp')`"
/>
</div>
</div>
<div
v-if="products.length < 1"
class="grid gap-12 pt-32 pb-16 md:grid-cols-2 2xl:grid-cols-3"
>
<TheProductSkeleton v-for="index in 6" :key="index" />
</div>
<!-- products -->
<div
v-else
ref="loadingElement"
class="flex flex-wrap justify-center gap-5 sm:gap-10"
>
<Product
v-for="product in products"
:key="product.id"
:product="product"
/>
</div>
<div v-if="reachedEnd" class="w-full flex justify-center">
<p>
{{ $t("FrontTranslations", "You reached end of the list.") }}
</p>
</div>
</div>
</div>
</UiContainer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import Product from "./Product.vue"; import Product from "./Product.vue";
import type { Feature, GenericResponse, GenericResponseChildren, GenericResponseItems, ProductType } from "~/types"; import type {
Feature,
GenericResponse,
GenericResponseChildren,
GenericResponseItems,
ProductType,
} from "~/types";
import CategoryTree from "./CategoryTree.vue"; import CategoryTree from "./CategoryTree.vue";
const { $session } = useNuxtApp(); const { $session } = useNuxtApp();
watch(
watch(() => $session.cookieData, async () => await getProducts(), { deep: true }); () => $session.cookieData,
async () => await getProducts(),
const props = defineProps<{ { deep: true }
component: { );
id: number defineProps<{
name: string component: {
img: string[] id: number;
component_name: string name: string;
is_no_lang: boolean img: string[];
page_name: string component_name: string;
front_section_lang: { is_no_lang: boolean;
data: { page_name: string;
title: string; front_section_lang: {
description: string data: {
} title: string;
id_front_section: number description: string;
id_lang: number };
}[] id_front_section: number;
} id_lang: number;
}[];
};
}>(); }>();
const openCategories = ref(false); const openCategories = ref(false);
const isInfo = ref<boolean>(true); const isInfo = ref<boolean>(true);
const selectedFilters = ref<any>([]); const selectedFilters = ref<any>([]);
const categoryId = ref<number>(1); const categoryId = ref<number>(1);
const itemsCount = ref(0);
const loading = ref(false); const loading = ref(false);
const reachedEnd = ref(false); const reachedEnd = ref(false);
@ -209,249 +305,253 @@ const maxElements = ref(0);
const products = ref([] as ProductType[]); const products = ref([] as ProductType[]);
async function getProducts() { async function getProducts() {
try { try {
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>( const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
`/api/public/products/category/${categoryId.value}?p=${page.value}&elems=${elems.value}`, `/api/public/products/category/${categoryId.value}?p=${page.value}&elems=${elems.value}`,
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
onErrorOccured: (_, status) => { onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`); throw new Error(`HTTP error: ${status}`);
}, },
} }
); );
products.value = data.items;
products.value = data.items; maxElements.value = data.items_count + 1;
maxElements.value = data.items_count + 1; } catch (error) {
console.error("getProducts error:", error);
} catch (error) { }
console.error("getProducts error:", error);
}
} }
const filters = ref([] as Feature[]); const filters = ref([] as Feature[]);
async function getCategory() { async function getCategory() {
try { try {
const { data } = await useMyFetch<GenericResponse<object>>( const { data } = await useMyFetch<GenericResponse<object>>(
`/api/public/products/category/1/classification`, `/api/public/products/category/1/classification`,
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
onErrorOccured: (_, status) => { onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`); throw new Error(`HTTP error: ${status}`);
}, },
} }
); );
filters.value = data as Feature[];
filters.value = data as Feature[]; filters.value.forEach((el: Feature) => {
filters.value.forEach((el) => { const parentId = el.feature_id;
const parentId = el.feature_id; el.feature_values.forEach((el) => {
el.feature_values.forEach((el) => { el.parent = parentId;
el.parent = parentId; });
}); });
}); } catch (error) {
console.error("getCategory error:", error);
} catch (error) { }
console.error("getCategory error:", error);
}
} }
const categoriesList = ref(); const categoriesList = ref();
async function getCategoryTree() { async function getCategoryTree() {
try { try {
const { data } = await useMyFetch<GenericResponseChildren<ProductType[]>>( const { data } = await useMyFetch<GenericResponseChildren<ProductType[]>>(
`/api/public/categories/tree`, `/api/public/categories/tree`,
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
onErrorOccured: (_, status) => { onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`); throw new Error(`HTTP error: ${status}`);
}, },
} }
); );
categoriesList.value = data.children; categoriesList.value = data.children;
} catch (error) {
} catch (error) { console.error("getCategory error:", error);
console.error("getCategory error:", error); }
}
} }
getProducts();
getProducts() getCategory();
getCategory() getCategoryTree();
getCategoryTree()
const closeElement = () => { const closeElement = () => {
isInfo.value = false; isInfo.value = false;
}; };
onMounted(() => { onMounted(() => {
window.addEventListener("scroll", scrollEvent); window.addEventListener("scroll", scrollEvent);
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("scroll", scrollEvent); window.removeEventListener("scroll", scrollEvent);
}); });
async function scrollEvent(e: Event) { async function scrollEvent() {
let maxScrollY = window.scrollY || document.documentElement.scrollHeight - document.documentElement.clientHeight; const maxScrollY =
window.scrollY ||
document.documentElement.scrollHeight -
document.documentElement.clientHeight;
if (window.scrollY >= maxScrollY - 500 && !reachedEnd.value && !loading.value) { if (
loading.value = true; window.scrollY >= maxScrollY - 500 &&
await loadMoreProducts(); !reachedEnd.value &&
loading.value = false; !loading.value
} ) {
loading.value = true;
await loadMoreProducts();
loading.value = false;
}
} }
filters.value.forEach((item) => { filters.value.forEach((item) => {
visibleFeatures[item.feature] = false; visibleFeatures[item.feature] = false;
}); });
const visibleFeatures = reactive<any>({}); const visibleFeatures = reactive<Record<string, boolean>>({});
function toggleFeature(feature: any) { function toggleFeature(feature: string) {
if (visibleFeatures.hasOwnProperty(feature)) { if (feature in visibleFeatures) {
visibleFeatures[feature] = !visibleFeatures[feature]; visibleFeatures[feature] = !visibleFeatures[feature];
} else { } else {
visibleFeatures[feature] = true; visibleFeatures[feature] = true;
} }
} }
class FilteredQueryString extends URLSearchParams { class FilteredQueryString extends URLSearchParams {
override append(name: string, value: string): void { override append(name: string, value: string): void {
if (value == null) { if (value == null) {
return; return;
}
super.append(name, value);
} }
super.append(name, value);
}
} }
async function loadMoreProducts() { async function loadMoreProducts() {
let qParams = new FilteredQueryString(); const qParams = new FilteredQueryString();
page.value = page.value + 1; page.value = page.value + 1;
qParams.append("p", `${page.value}`); qParams.append("p", `${page.value}`);
qParams.append("elems", `${elems.value}`); qParams.append("elems", `${elems.value}`);
qParams.append("features", selectedFilters.value.length > 0 ? selectedFilters.value : null); qParams.append(
"features",
selectedFilters.value.length > 0 ? selectedFilters.value : null
);
try { try {
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>( const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
`/api/public/products/category/${categoryId.value}?${qParams.toString()}`, `/api/public/products/category/${categoryId.value}?${qParams.toString()}`,
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
onErrorOccured: (_, status) => { onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`); throw new Error(`HTTP error: ${status}`);
}, },
} }
); );
maxElements.value = data.items_count; maxElements.value = data.items_count;
if (data.items) { if (data.items) {
products.value.push(...(data.items as ProductType[])); products.value.push(...(data.items as ProductType[]));
} else { } else {
reachedEnd.value = true; reachedEnd.value = true;
}
if (products.value.length >= maxElements.value) {
reachedEnd.value = true;
}
} catch (error) {
console.error("getCategory error:", error);
} }
if (products.value.length >= maxElements.value) {
reachedEnd.value = true;
}
} catch (error) {
console.error("getCategory error:", error);
}
} }
const changeCategory = (item: any) => { const changeCategory = (item: any) => {
categoryId.value = item.id; categoryId.value = item.id;
}; };
watch(selectedFilters, async (newQuestion) => { watch(selectedFilters, async (newQuestion: string) => {
if (newQuestion) { if (newQuestion) {
page.value = 1; page.value = 1;
reachedEnd.value = false; reachedEnd.value = false;
loadingElement.value?.scrollIntoView(); loadingElement.value?.scrollIntoView();
let qParams = new FilteredQueryString(); const qParams = new FilteredQueryString();
qParams.append("p", `${page.value}`); qParams.append("p", `${page.value}`);
qParams.append("elems", `${elems.value}`); qParams.append("elems", `${elems.value}`);
qParams.append("features", selectedFilters.value.length > 0 ? selectedFilters.value : null); qParams.append(
"features",
selectedFilters.value.length > 0 ? selectedFilters.value : null
);
try { try {
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>( const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
`/api/public/products/category/1?${qParams.toString()}`, `/api/public/products/category/1?${qParams.toString()}`,
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
onErrorOccured: (_, status) => { onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`); throw new Error(`HTTP error: ${status}`);
}, },
}
);
products.value = data.items;
maxElements.value = data.items_count;
} catch (error) {
console.error("selectedFilters error:", error);
} }
);
products.value = data.items;
maxElements.value = data.items_count;
} catch (error) {
console.error("selectedFilters error:", error);
} }
}
}); });
watch(categoryId, async (newQuestion) => { watch(categoryId, async (newCategoryId) => {
if (newQuestion) { if (newCategoryId) {
page.value = 1; page.value = 1;
reachedEnd.value = false; reachedEnd.value = false;
loadingElement.value?.scrollIntoView(); loadingElement.value?.scrollIntoView();
let qParams = new FilteredQueryString(); const qParams = new FilteredQueryString();
qParams.append("p", `${page.value}`);
qParams.append("elems", `${elems.value}`);
qParams.append(
"features",
selectedFilters.value.length > 0 ? selectedFilters.value : null
);
qParams.append("p", `${page.value}`); try {
qParams.append("elems", `${elems.value}`); const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
qParams.append("features", selectedFilters.value.length > 0 ? selectedFilters.value : null); `api/public/products/category/${newCategoryId}?${qParams.toString()}`,
{
try { headers: { "Content-Type": "application/json" },
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>( onErrorOccured: (_, status) => {
`api/public/products/category/${categoryId.value}?${qParams.toString()}`, throw new Error(`HTTP error: ${status}`);
{ },
headers: {
"Content-Type": "application/json",
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`);
},
}
);
products.value = data.items;
maxElements.value = data.items_count;
} catch (error) {
console.error("getCategory error:", error);
} }
);
products.value = data.items;
maxElements.value = data.items_count;
} catch (error) {
console.error("getCategory error:", error);
} }
}
}); });
</script> </script>
<style scoped> <style scoped>
.v-enter-active, .v-enter-active,
.v-leave-active { .v-leave-active {
transition: opacity 0.5s ease; transition: opacity 0.5s ease;
} }
.v-enter-from, .v-enter-from,
.v-leave-to { .v-leave-to {
opacity: 0; opacity: 0;
} }
</style> </style>

View File

@ -1,41 +1,26 @@
<template> <template>
<div <div class="group flex cursor-pointer items-center justify-start gap-2 whitespace-nowrap">
class="group flex cursor-pointer items-center justify-start gap-2 whitespace-nowrap" <button :class="[
> 'h-[40px] cursor-pointer min-w-40 rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px]',
<button type === 'fill'
:class="[ ? 'bg-button text-text-dark group-hover:bg-button-hover'
'h-[40px] cursor-pointer rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px]', : type === 'border'
type === 'fill'
? 'bg-button text-text-dark group-hover:bg-button-hover'
: type === 'border'
? 'border-button text-button group-hover:border-button-hover group-hover:text-button-hover border' ? 'border-button text-button group-hover:border-button-hover group-hover:text-button-hover border'
: 'border-button text-button dark:border-block dark:text-block group-hover:border-button-hover group-hover:text-button-hover border', : 'border-button text-button dark:border-block dark:text-block group-hover:border-button-hover group-hover:text-button-hover border',
full && 'w-full' full && 'w-full'
]" ]">
>
<slot /> <slot />
</button> </button>
<div <div v-if="arrow" :class="[
v-if="arrow" 'flex h-[40px] w-[40px] items-center justify-center rounded-[10px] p-2.5 transition-all sm:h-[50px] sm:w-[50px] md:h-[65px] md:w-[65px] md:rounded-[15px]',
:class="[ type === 'fill'
'flex h-[40px] w-[40px] items-center justify-center rounded-[10px] p-2.5 transition-all sm:h-[50px] sm:w-[50px] md:h-[65px] md:w-[65px] md:rounded-[15px]', ? 'bg-button text-text-dark group-hover:bg-button-hover'
type === 'fill' : 'border-button text-button dark:border-block dark:text-block group-hover:border-button-hover group-hover:text-button-hover border',
? 'bg-button text-text-dark group-hover:bg-button-hover' ]">
: 'border-button text-button dark:border-block dark:text-block group-hover:border-button-hover group-hover:text-button-hover border', <svg class="" width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
]"
>
<svg
class=""
width="26"
height="26"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M25.1274 1.87258C25.1274 1.3203 24.6797 0.872582 24.1274 0.872584L15.1274 0.872583C14.5751 0.872583 14.1274 1.3203 14.1274 1.87258C14.1274 2.42487 14.5751 2.87258 15.1274 2.87258L23.1274 2.87258L23.1274 10.8726C23.1274 11.4249 23.5751 11.8726 24.1274 11.8726C24.6797 11.8726 25.1274 11.4249 25.1274 10.8726L25.1274 1.87258ZM1.5 24.5L2.20711 25.2071L24.8345 2.57969L24.1274 1.87258L23.4203 1.16548L0.792893 23.7929L1.5 24.5Z" d="M25.1274 1.87258C25.1274 1.3203 24.6797 0.872582 24.1274 0.872584L15.1274 0.872583C14.5751 0.872583 14.1274 1.3203 14.1274 1.87258C14.1274 2.42487 14.5751 2.87258 15.1274 2.87258L23.1274 2.87258L23.1274 10.8726C23.1274 11.4249 23.5751 11.8726 24.1274 11.8726C24.6797 11.8726 25.1274 11.4249 25.1274 10.8726L25.1274 1.87258ZM1.5 24.5L2.20711 25.2071L24.8345 2.57969L24.1274 1.87258L23.4203 1.16548L0.792893 23.7929L1.5 24.5Z"
fill="currentColor" fill="currentColor" />
/>
</svg> </svg>
</div> </div>
</div> </div>

View File

@ -0,0 +1,57 @@
<template>
<div class="space-y-[15px]">
<p :for="`base-input-${id}`" class="pl-6">
<slot />
</p>
<div class="flex flex-col">
<div class="flex relative">
<input :id="`base-input-${id}`" :value="modelValue" :type="!isPasswordVisible ? type : 'text'"
:placeholder="placeholder" :disabled="disabled"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
@focus="$emit('focus')" @blur="$emit('blur')"
class="border border-block placeholder:text-gray dark:placeholder:text-button-disabled rounded-lg px-6 h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2 text-button" />
<i v-if="disabled"
class="uil uil-lock-alt text-[22px] absolute right-6 top-1/2 -translate-y-1/2 text-gray" />
<div v-if="type === 'password'" class="order-2 ml-1.5 cursor-pointer" :title="!isPasswordVisible ? $t('Panel.Component.InputDefault', 'Show password') : $t('Panel.Component.InputDefault', 'Hide password')
" @click="isPasswordVisible = !isPasswordVisible">
<FaceObserver class="ml-4 text-xl leading-6" :isPasswordVisible="isPasswordVisible" />
</div>
</div>
<!-- <p class="mt-2 text-xs text-red-600">{{ validationText }}</p> -->
<!-- <p v-if="!validation && validation != null" class="mt-2 text-xs text-red-600">
{{ validationText }}
</p> -->
</div>
</div>
</template>
<script setup lang="ts">
import FaceObserver from './FaceObserver.vue';
defineEmits(["update:modelValue", "focus", "blur"]);
defineProps<{
modelValue?: string | any;
modelModifiers?: object;
id: number;
type?: string;
disabled?: boolean;
placeholder?: string;
validation?: boolean | null;
validationText?: string;
}>();
const isPasswordVisible = ref(false);
</script>
<style>
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
transition: background-color 5000s ease-in-out 0s;
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<svg v-if="isPasswordVisible" xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 -960 960 960" width="16px" fill="#000000"><path d="m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 -960 960 960" width="16px" fill="#000000"><path d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z"/></svg>
</template>
<script>
export default {
props: {
isPasswordVisible: {
type: Boolean,
required: true
}
}
};
</script>

View File

@ -0,0 +1,29 @@
<template>
<div class="hidden md:block">
<svg width="100%" height="100%" viewBox="0 0 870 350" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="customClip">
<path
d="M20 0.5H847.666C858.366 0.5 867.067 9.12193 867.165 19.8213L869.315 254.821C869.415 265.66 860.656 274.5 849.816 274.5H653C641.678 274.5 632.5 283.678 632.5 295V330C632.5 340.77 623.77 349.5 613 349.5H20C9.23045 349.5 0.5 340.77 0.5 330V20C0.5 9.23045 9.23045 0.5 20 0.5Z" />
</clipPath>
</defs>
<image :href="src" clip-path="url(#customClip)" preserveAspectRatio="xMidYMid slice" width="100%"
height="100%" />
<foreignObject x="640" y="285" width="calc(100% - 640px - 1px)" height="calc(100% - 285px)">
<slot name="button" />
</foreignObject>
</svg>
</div>
<div class="block md:hidden">
<img :src="src" width="100%" height="100%" class="object-contain rounded-2xl my-4" />
<div class="flex justify-center">
<slot name="button" />
</div>
</div>
</template>
<script lang="ts" setup>
defineProps<{ src: string }>()
</script>

View File

@ -53,6 +53,7 @@ export default defineNuxtConfig({
ignored: ["**/backend/pb_data/**"], ignored: ["**/backend/pb_data/**"],
}, },
hmr: { hmr: {
host: "127.0.0.1",
clientPort: 3000, // useful if proxying clientPort: 3000, // useful if proxying
}, },
}, },

View File

@ -17,6 +17,7 @@
"@pinia/nuxt": "^0.11.0", "@pinia/nuxt": "^0.11.0",
"@tailwindcss/vite": "^4.1.8", "@tailwindcss/vite": "^4.1.8",
"@vueuse/core": "^13.3.0", "@vueuse/core": "^13.3.0",
"gsap": "^3.13.0",
"nuxt": "^3.17.4", "nuxt": "^3.17.4",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.7",
"vue": "^3.5.14", "vue": "^3.5.14",

View File

@ -6,6 +6,62 @@
</template> </template>
<script setup> <script setup>
import { gsap } from 'gsap'
import ScrollTrigger from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
watch(useColorMode(), (color) => {
console.log(color);
})
onMounted(() => {
const anim = gsap.fromTo(
'h1',
{
opacity: 0,
zoom: 0.95
},
{
opacity: 1,
duration: 1,
zoom: 1,
ease: 'power2.out',
}
)
ScrollTrigger.create({
trigger: 'h1',
start: 'top 80%',
onEnter: () => anim.restart(), // play when scrolling down
onEnterBack: () => anim.restart(), // play again when scrolling up
})
const animh2 = gsap.fromTo(
'h2',
{
// opacity: 0,
// color: 'var(--color-accent-green-light)',
},
{
// opacity: 1,
// duration: 1,
ease: 'power2.out',
}
)
ScrollTrigger.create({
trigger: 'h2',
start: 'top 80%',
onEnter: () => animh2.restart(), // play when scrolling down
onEnterBack: () => animh2.restart(), // play again when scrolling up
})
})
const route = useRoute(); const route = useRoute();
const store = useStore(); const store = useStore();
const menuStore = useMenuStore(); const menuStore = useMenuStore();

255
stores/checkoutStore.ts Normal file
View File

@ -0,0 +1,255 @@
import type { GenericResponse } from "~/types";
import type { AddressesList } from "~/types/checkout";
import { validation } from "../utils/validation";
import { REGEX_PHONE } from "../utils/regex";
export const useCheckoutStore = defineStore("checkoutStore", () => {
const { $toast } = useNuxtApp();
const addressesList = ref<AddressesList[]>();
const activeAddress = ref<AddressesList | null>();
async function restrictedAddress() {
try {
// const { data } = await useMyFetch<GenericResponse<object>>(
// `/api/restricted/user/addresses`,
// {
// headers: {
// "Content-Type": "application/json",
// },
// onErrorOccured: async (_, status) => {
// throw createError({
// statusCode: status,
// statusMessage: `HTTP error: ${status}`,
// });
// },
// }
// );
const data = [
{
address: {
city: "Bochnia",
country_iso: "pl",
name: "John",
postcode: "32-700",
street: "Karosek",
surname: "Kornelsky",
},
address_id: 2,
alias: "home",
customer_id: 4,
is_default: true,
is_official: false,
},
];
addressesList.value = data;
activeAddress.value = addressesList.value[0];
} catch (error) {
console.error("restrictedAddress error:", error);
}
}
const userName = ref("");
const lastName = ref("");
const address = ref("");
const postCode = ref("");
const city = ref("");
const country = ref("");
const phoneNumber = ref("");
const accountPhoneNumber = ref("");
async function restrictedAddressOfficial() {
try {
// const { data } = await useMyFetch<GenericResponse<object>>(
// `/api/restricted/user/address/official`,
// {
// headers: {
// "Content-Type": "application/json",
// },
// onErrorOccured: async (_, status) => {
// throw createError({
// statusCode: status,
// statusMessage: `HTTP error: ${status}`,
// });
// },
// }
// );
const data = {
address: {
city: "Bochnia",
country_iso: "pl",
name: "John",
postcode: "32-700",
street: "Karosek",
surname: "Kornelsky",
},
address_id: 2,
alias: "home",
customer_id: 4,
is_default: true,
is_official: false,
};
userName.value = data.address.name;
lastName.value = data.address.surname;
address.value = data.address.street;
postCode.value = data.address.postcode;
city.value = data.address.city;
country.value = data.address.country_iso;
// resolve this
// accountPhoneNumber.value = useUserStore().fullUserData.phone_number;
phoneNumber.value = "+36 789 3773 737";
} catch (error) {
console.error("restrictedAddressOfficial error:", error);
}
}
const vNewAddressName = ref("");
const vNewAddressSurname = ref("");
const vNewAddressAddress = ref("");
const vNewAddressCode = ref("");
const vNewAddressCity = ref("");
const vNewAddressCountry = ref("");
const vUseAccountPhoneNumber = ref(false);
const isOpen = ref<boolean>(false);
async function uploadAddress() {
try {
const res = await useMyFetch<GenericResponse<object>>(
`/api/restricted/user/address/official`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
address: {
city: vNewAddressCity.value,
// country_iso: vNewAddressCountry.value?.iso_code,
name: vNewAddressName.value,
postcode: vNewAddressCode.value,
street: vNewAddressAddress.value,
surname: vNewAddressSurname.value,
},
}),
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
});
},
}
);
if (res.status === 200) {
$toast.success("Address successfully added", {
autoClose: 5000,
dangerouslyHTMLString: true,
});
isOpen.value = false;
restrictedAddress();
} else {
$toast.error("Failed to add address. Please try again.", {
autoClose: 5000,
dangerouslyHTMLString: true,
});
}
} catch (error) {
console.error("uploadAddress error:", error);
}
}
const currentPrefix = ref<string | number>("+43");
const changePrefix = (item: any) => {
currentPrefix.value = item;
};
const phoneValidation = ref<boolean | null>(null);
async function sendForm() {
let phoneNum = `${currentPrefix.value}${phoneNumber.value}`
.replaceAll(" ", "")
.trim();
// if (vUseAccountPhoneNumber.value) {
// phoneNum = phoneNumber.value;
// }
phoneValidation.value = validation(phoneNum, 1, 49, REGEX_PHONE);
try {
const res = await useMyFetch<GenericResponse<object>>(
`restricted/cart/checkout/delivery`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
address: {
city: activeAddress.value?.address.city,
country_iso: activeAddress.value?.address.country_iso,
name: activeAddress.value?.address.name,
postcode: activeAddress.value?.address.postcode,
street: activeAddress.value?.address.street,
surname: activeAddress.value?.address.surname,
},
phone_number: phoneNum,
// email: useUserStore().fullUserData.email,
}),
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
});
},
}
);
if (res.status === 200) {
$toast.success("Form successfully sent", {
autoClose: 5000,
dangerouslyHTMLString: true,
});
// redirectToSummary();
} else {
$toast.error("Failed to send form. Please try again.", {
autoClose: 5000,
dangerouslyHTMLString: true,
});
}
} catch (error) {
console.error("uploadAddress error:", error);
}
}
const changeActive = (item: any) => {
activeAddress.value = item;
};
return {
addressesList,
activeAddress,
isOpen,
userName,
lastName,
address,
postCode,
city,
country,
phoneNumber,
accountPhoneNumber,
vUseAccountPhoneNumber,
currentPrefix,
vNewAddressName,
vNewAddressSurname,
vNewAddressAddress,
vNewAddressCode,
vNewAddressCity,
vNewAddressCountry,
restrictedAddress,
restrictedAddressOfficial,
changeActive,
uploadAddress,
sendForm,
};
});

View File

@ -56,7 +56,6 @@ export const useMenuStore = defineStore("menuStore", () => {
const currencies = ref([] as Currency[]); const currencies = ref([] as Currency[]);
const languages = ref([] as Language[]); const languages = ref([] as Language[]);
const getLocales = async () => { const getLocales = async () => {
const { data: countriesList } = await useMyFetch<GenericResponse<Country[]>>(`/api/public/country/list`); const { data: countriesList } = await useMyFetch<GenericResponse<Country[]>>(`/api/public/country/list`);
countries.value = countriesList; countries.value = countriesList;
@ -96,7 +95,6 @@ export const useMenuStore = defineStore("menuStore", () => {
console.warn("Root menu item not found"); console.warn("Root menu item not found");
menu.value = []; menu.value = [];
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@ -8,7 +8,7 @@ import type { FrontPageSection } from "~/types/frontSection";
export const useStore = defineStore("store", () => { export const useStore = defineStore("store", () => {
const currentPageID = ref(""); const currentPageID = ref("");
const { $toast } = useNuxtApp();
// calculator // calculator
const monthlySavings = ref(137); const monthlySavings = ref(137);
@ -16,19 +16,13 @@ export const useStore = defineStore("store", () => {
const totalInvestment: Ref<number> = ref(0); const totalInvestment: Ref<number> = ref(0);
const minValue = ref(); const minValue = ref();
// login
const email = ref();
const password = ref();
const components = ref({} as FrontPageSection[]); const components = ref({} as FrontPageSection[]);
const getSections = async (id: string) => { const getSections = async (id: string) => {
const { data } = await useMyFetch<GenericResponse<FrontPageSection[]>>( const { data } = await useMyFetch<GenericResponse<FrontPageSection[]>>(
`/api/public/front/sections/${id}` `/api/public/front/sections/${id}`
) );
components.value = data components.value = data;
}; };
async function getComponents(): Promise<componentsListType[]> { async function getComponents(): Promise<componentsListType[]> {
@ -56,7 +50,6 @@ export const useStore = defineStore("store", () => {
name: componentName, name: componentName,
component: child.front_section, component: child.front_section,
componentInstance: nonReactiveComponent, componentInstance: nonReactiveComponent,
}); });
} catch (error) { } catch (error) {
console.error(`Failed to load component ${componentName}`, error); console.error(`Failed to load component ${componentName}`, error);
@ -103,39 +96,12 @@ export const useStore = defineStore("store", () => {
} }
); );
minValue.value = data; minValue.value = data;
} catch (error) { } catch (error) {
console.error("getList error:", error); console.error("getList error:", error);
} }
} }
async function logIn() {
try {
const { data } = await useMyFetch<GenericResponse<object>>(
`/api/public/user/session/start`,
{
method: "POST",
body: JSON.stringify({
mail: email.value,
password: password.value,
}),
headers: {
"Content-Type": "application/json",
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`);
},
}
);
minValue.value = data;
} catch (error) {
console.error("getList error:", error);
}
}
return { return {
currentPageID, currentPageID,
components, components,
@ -143,9 +109,6 @@ export const useStore = defineStore("store", () => {
monthlySavings, monthlySavings,
storagePeriod, storagePeriod,
minValue, minValue,
email,
password,
logIn,
getCalculator, getCalculator,
getComponents, getComponents,
getSections, getSections,

105
stores/userStore.ts Normal file
View File

@ -0,0 +1,105 @@
import type { GenericResponse } from "~/types";
import type { Customer } from "~/types/user";
export const useUserStore = defineStore("userStore", () => {
const store = useStore();
const fullUserData = ref<Customer | null>(null);
const isLogged = ref<boolean>(true);
const user = ref<string | null>(null);
async function checkIsLogged() {
try {
const { data } = await useMyFetch<
GenericResponse<{ loggedin: boolean } | Customer>
>(`/api/public/user`, {
headers: {
"Content-Type": "application/json",
},
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
});
},
});
if ("loggedin" in data && data.loggedin === true) {
isLogged.value = true;
user.value = null;
fullUserData.value = null;
} else if ("first_name" in data && "last_name" in data) {
isLogged.value = true;
user.value = `${data.first_name} ${data.last_name}`;
fullUserData.value = data as Customer;
} else {
isLogged.value = false;
user.value = null;
fullUserData.value = null;
}
} catch (error) {
console.error("checkIsLogged error:", error);
}
}
// login
const email = ref();
const password = ref();
const vLogin = ref<boolean>(true);
const vCodeVerify = ref<boolean>(false);
const vCode = ref<number | null>(null);
const vEmail = ref<string>("");
async function logIn() {
try {
const data = await useMyFetch<GenericResponse<object>>(
`/api/public/user/session/start`,
{
method: "POST",
body: JSON.stringify({
mail: email.value,
password: password.value,
}),
headers: {
"Content-Type": "application/json",
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`);
},
}
);
if (data.status === 200 || data.status === 201) {
console.log(vCodeVerify.value);
// $toast.success("Address successfully added", {
// autoClose: 5000,
// dangerouslyHTMLString: true,
// });
vLogin.value = false;
vCodeVerify.value = true;
}
// else {
// $toast.error("Failed to add address. Please try again.", {
// autoClose: 5000,
// dangerouslyHTMLString: true,
// });
// }
store.minValue = data;
} catch (error) {
console.error("getList error:", error);
}
}
return {
isLogged,
user,
fullUserData,
vCodeVerify,
vCode,
vEmail,
email,
password,
logIn,
checkIsLogged,
};
});

15
types/checkout.ts Normal file
View File

@ -0,0 +1,15 @@
export interface AddressesList {
address: {
city: string;
country_iso: string;
name: string;
postcode: string;
street: string;
surname: string;
};
address_id: number;
alias: string;
customer_id: number;
is_default: boolean;
is_official: boolean;
}

49
types/user.ts Normal file
View File

@ -0,0 +1,49 @@
export interface Customer {
active: boolean;
agreed_for_newsletter: boolean;
bank_accounts: {
bank_currency_iso: string;
bank_name: string;
customer_id: number;
iban: string;
swift: string;
verified: boolean;
}[];
birthday_date: string;
communication_languag_id: number;
document_verified: boolean;
documents: {
file: string;
id: number;
name: string;
size: number;
typ: string;
}[];
email: string;
entity: {
city: string;
country_iso: string;
customer_id: number;
extra_enitity_id: string;
name: string;
national_court_register_number: string;
postcode: string;
statistical_number: string;
street: string;
vat_number: string;
web_pages_list: string;
}[];
first_name: string;
is_entity: boolean;
is_partner: boolean;
is_root: boolean;
last_name: string;
metadata: {
id: number;
metadata: string;
type: string;
}[];
partner_code: string;
phone_number: string;
taxes_country_iso: string;
}

34
utils/regex.js Normal file
View File

@ -0,0 +1,34 @@
const REGEX_EMAIL = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/i,
// Minimum eight characters, at least one uppercase letter, one lowercase letter, one number and one special character
// REGEX_PASSWORD = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
// REGEX_PASSWORD = /^(?=.*[A-Za-z])(?=.*\d)(?=.*\W)[A-Za-z\d^\S]{8,}$/,
REGEX_PASSWORD = new RegExp(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*\W)[A-Za-z\d^\S]{8,}$/),
REGEX_CODE = /.{6}/,
// REGEX_PHONE = /^\+?[1-9][0-9]{7,14}$/,
REGEX_PHONE = new RegExp(/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/im),
REGEX_DATE = /^[+-]?\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/,
// Only numbers
REGEX_ONLYNUMBERS = /^[0-9]*$/,
// Number (price)
REGEX_NUMBER = /^(?!0*[.,]0*$|[.,]0*$|0*$)\d+[,.]?\d{0,2}$/,
REGEX_NUMBER_WITH_ZERO = /^[0-9]{1,10}([.][0-9]{1,2})?$/,
REGEX_URL = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/
// URL
// REGEX_URL = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/
export {
REGEX_EMAIL,
REGEX_PASSWORD,
REGEX_CODE,
REGEX_PHONE,
REGEX_ONLYNUMBERS,
REGEX_NUMBER,
REGEX_NUMBER_WITH_ZERO,
REGEX_DATE,
REGEX_URL,
}

12
utils/validation.ts Normal file
View File

@ -0,0 +1,12 @@
const validation = function (item:string, min:number, max:number, regEx = /.*/) {
if (
item == undefined ||
item.length < min ||
item.length > max ||
!regEx.test(item)
) return false;
else return true;
};
export { validation };