erp-city-vue-components 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/package.json +20 -0
- package/src/components/AddressForm.vue +249 -0
- package/src/components/AuthButton.vue +35 -0
- package/src/components/AuthSidebar.vue +218 -0
- package/src/components/CartButton.vue +18 -0
- package/src/components/CartDetails.vue +152 -0
- package/src/components/FavoriteProductCard.vue +35 -0
- package/src/components/FavoritesSidebar.vue +47 -0
- package/src/components/OrderConfirmation.vue +30 -0
- package/src/components/OrdersSidebar.vue +129 -0
- package/src/components/PaymentOrderConfirmation.vue +28 -0
- package/src/components/ProductCard.vue +69 -0
- package/src/components/QuantityInput.vue +44 -0
- package/src/components/Sidebar.vue +66 -0
- package/src/composables/handlesFormErrors.js +39 -0
- package/src/config.js +11 -0
- package/src/index.js +24 -0
- package/src/state/authState.js +189 -0
- package/src/state/cartState.js +106 -0
- package/src/state/favoritesState.js +94 -0
- package/src/state/ordersState.js +42 -0
- package/src/state/productsState.js +16 -0
- package/src/state/sidebarState.js +45 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onMounted, ref, watch } from "vue";
|
|
3
|
+
import { sidebarState } from "../state/sidebarState.js";
|
|
4
|
+
import { cartState } from "../state/cartState.js";
|
|
5
|
+
import { authState } from "../state/authState.js";
|
|
6
|
+
import ProductCard from "./ProductCard.vue";
|
|
7
|
+
|
|
8
|
+
onMounted(() => {
|
|
9
|
+
sidebarState.setHeader('Detalii coș', false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const loginLabel = computed(() =>
|
|
13
|
+
cartState.price ? 'Autentifică-te pentru plată' : 'Autentifică-te pentru ofertă'
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const guestLabel = computed(() =>
|
|
17
|
+
'Continuă ca invitat'
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const couponCode = ref('');
|
|
21
|
+
|
|
22
|
+
function continueAsGuest() {
|
|
23
|
+
sidebarState.open('address');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function openAuth() {
|
|
27
|
+
sidebarState.nextView = 'address';
|
|
28
|
+
sidebarState.open('auth');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function applyCoupon() {
|
|
32
|
+
await cartState.applyCoupon(couponCode.value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function clearCoupon() {
|
|
36
|
+
await cartState.clearCoupon();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
watch(
|
|
40
|
+
() => cartState.coupon,
|
|
41
|
+
(coupon) => {
|
|
42
|
+
couponCode.value = coupon?.code ?? '';
|
|
43
|
+
},
|
|
44
|
+
{ immediate: true }
|
|
45
|
+
);
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div class="flex flex-col min-h-[calc(100vh-228px)]">
|
|
50
|
+
<div class="space-y-4 divide-y divide-gray-200">
|
|
51
|
+
<product-card v-for="item in cartState.items"
|
|
52
|
+
:item="item"
|
|
53
|
+
></product-card>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div v-if="!cartState.items.length"
|
|
57
|
+
class="text-center text-lg text-gray-500 mt-20"
|
|
58
|
+
>
|
|
59
|
+
Nu ai niciun produs în coșul tău.
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div v-if="cartState.subtotal"
|
|
63
|
+
class="mt-auto flex items-center justify-between pt-6 pb-4 border-t border-gray-200"
|
|
64
|
+
>
|
|
65
|
+
<div class="text-xl lg:text-2xl font-medium">Subtotal</div>
|
|
66
|
+
<div class="text-2xl lg:text-3xl">{{ cartState.subtotal }} RON</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div
|
|
70
|
+
v-if="cartState.coupon && cartState.subtotal"
|
|
71
|
+
class="flex items-center justify-between pb-4 border-b border-gray-200"
|
|
72
|
+
>
|
|
73
|
+
<div class="text-lg lg:text-xl text-neutral-600">
|
|
74
|
+
Reducere ({{ cartState.coupon.discountPercent }}%)
|
|
75
|
+
</div>
|
|
76
|
+
<div class="text-lg lg:text-xl text-neutral-600">-{{ cartState.discountAmount }} RON</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div
|
|
80
|
+
v-if="cartState.coupon && cartState.subtotal"
|
|
81
|
+
class="flex items-center justify-between py-6"
|
|
82
|
+
>
|
|
83
|
+
<div class="text-xl lg:text-2xl font-medium">Total</div>
|
|
84
|
+
<div class="text-2xl lg:text-3xl">{{ cartState.price }} RON</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div v-if="cartState.items.length"
|
|
88
|
+
class="pb-8 flex space-x-2 items-end"
|
|
89
|
+
>
|
|
90
|
+
<v-input
|
|
91
|
+
id="coupon_code"
|
|
92
|
+
label="Cupon reducere"
|
|
93
|
+
placeholder="Introdu codul de reducere"
|
|
94
|
+
:error="cartState.couponError"
|
|
95
|
+
v-model="couponCode"
|
|
96
|
+
></v-input>
|
|
97
|
+
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="p-3 border border-primary text-primary text-sm font-medium rounded-lg"
|
|
101
|
+
:disabled="cartState.couponLoading || !couponCode"
|
|
102
|
+
@click="applyCoupon"
|
|
103
|
+
>
|
|
104
|
+
Aplică
|
|
105
|
+
</button>
|
|
106
|
+
|
|
107
|
+
<button
|
|
108
|
+
v-if="cartState.coupon"
|
|
109
|
+
type="button"
|
|
110
|
+
class="p-3 border border-red-600 text-red-600 text-sm font-medium rounded-lg"
|
|
111
|
+
@click="clearCoupon"
|
|
112
|
+
>
|
|
113
|
+
Elimină
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="mt-auto border-t-2 border-t-neutral-200 pt-10">
|
|
118
|
+
<button
|
|
119
|
+
v-if="authState.isAuthenticated"
|
|
120
|
+
type="button"
|
|
121
|
+
class="btn btn-primary btn-full mb-2 lg:mb-6"
|
|
122
|
+
:disabled="!cartState.items.length"
|
|
123
|
+
@click="sidebarState.open('address')"
|
|
124
|
+
>
|
|
125
|
+
{{ cartState.price ? 'Continuă spre plată' : 'Solicită ofertă' }}
|
|
126
|
+
</button>
|
|
127
|
+
|
|
128
|
+
<div
|
|
129
|
+
v-else
|
|
130
|
+
class="flex flex-col gap-4 text-center"
|
|
131
|
+
>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
class="btn btn-primary btn-full"
|
|
135
|
+
:disabled="!cartState.items.length"
|
|
136
|
+
@click="openAuth"
|
|
137
|
+
>
|
|
138
|
+
{{ loginLabel }}
|
|
139
|
+
</button>
|
|
140
|
+
<div class="text-sm text-neutral-500">sau</div>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
class="btn btn-secondary btn-full"
|
|
144
|
+
:disabled="!cartState.items.length"
|
|
145
|
+
@click="continueAsGuest"
|
|
146
|
+
>
|
|
147
|
+
{{ guestLabel }}
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { favoritesState } from '../state/favoritesState.js';
|
|
3
|
+
import {route} from "ziggy-js";
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
product: {
|
|
7
|
+
type: Object,
|
|
8
|
+
required: true,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
async function removeFromFavorites() {
|
|
13
|
+
await favoritesState.toggle(props.product.id);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div class="flex items-center py-4">
|
|
20
|
+
<a :href="route('shop.products.show', product.id)">
|
|
21
|
+
<img class="w-24 h-24 object-contain cursor-pointer" :src="product.icon_url || product.image" :alt="product.name">
|
|
22
|
+
</a>
|
|
23
|
+
<a class="ml-15 flex-1 cursor-pointer" :href="route('shop.products.show', product.id)">
|
|
24
|
+
<div class="font-bold text-xl lg:text-2xl mb-1.5">{{ product.name }}</div>
|
|
25
|
+
<p class="text-sm text-gray-500 line-clamp-2">{{ product.description }}</p>
|
|
26
|
+
</a>
|
|
27
|
+
<div class="ml-auto flex flex-col justify-between">
|
|
28
|
+
<div class="ml-auto hover:brightness-90 cursor-pointer" @click.stop="removeFromFavorites">
|
|
29
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
30
|
+
<path d="M8.33398 1.66667L7.50065 2.50001H4.16732C3.70732 2.50001 3.33398 2.87334 3.33398 3.33334C3.33398 3.79334 3.70732 4.16667 4.16732 4.16667H5.83398H14.1673H15.834C16.294 4.16667 16.6673 3.79334 16.6673 3.33334C16.6673 2.87334 16.294 2.50001 15.834 2.50001H12.5007L11.6673 1.66667H8.33398ZM4.16732 5.83334V16.6667C4.16732 17.5875 4.91315 18.3333 5.83398 18.3333H14.1673C15.0882 18.3333 15.834 17.5875 15.834 16.6667V5.83334H4.16732Z" fill="#C73838"/>
|
|
31
|
+
</svg>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onMounted } from 'vue';
|
|
3
|
+
import FavoriteProductCard from './FavoriteProductCard.vue';
|
|
4
|
+
import { favoritesState } from '../state/favoritesState.js';
|
|
5
|
+
import { productsState } from '../state/productsState.js';
|
|
6
|
+
import { sidebarState } from '../state/sidebarState.js';
|
|
7
|
+
|
|
8
|
+
const favoriteProducts = computed(() => favoritesState.ids
|
|
9
|
+
.map(id => productsState.getProduct(id))
|
|
10
|
+
.filter(Boolean));
|
|
11
|
+
const isLoading = computed(() => favoritesState.loading);
|
|
12
|
+
const error = computed(() => favoritesState.error);
|
|
13
|
+
|
|
14
|
+
onMounted(() => {
|
|
15
|
+
sidebarState.setHeader('Produse favorite', true);
|
|
16
|
+
|
|
17
|
+
if (! favoritesState.ids.length && ! favoritesState.loading) {
|
|
18
|
+
favoritesState.fetchFavorites();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div class="flex flex-col min-h-[calc(100vh-228px)]">
|
|
25
|
+
<h4 class="text-lg font-semibold mb-6">Produse favorite</h4>
|
|
26
|
+
|
|
27
|
+
<div v-if="isLoading" class="text-gray-500 text-sm mt-4">
|
|
28
|
+
Se încarcă favoritele...
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div v-else-if="error" class="text-red-600 text-sm mt-4">
|
|
32
|
+
{{ error }}
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div v-else-if="!favoriteProducts.length" class="text-gray-500 text-sm mt-4">
|
|
36
|
+
Nu ai adăugat încă produse la favorite.
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div v-else class="space-y-4 divide-y divide-gray-200 flex-1 overflow-y-auto">
|
|
40
|
+
<favorite-product-card
|
|
41
|
+
v-for="product in favoriteProducts"
|
|
42
|
+
:key="product.id"
|
|
43
|
+
:product="product"
|
|
44
|
+
></favorite-product-card>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {sidebarState} from "../state/sidebarState.js";
|
|
3
|
+
import {onMounted} from "vue";
|
|
4
|
+
|
|
5
|
+
onMounted(() => {
|
|
6
|
+
sidebarState.setHeader('', false);
|
|
7
|
+
});
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="flex flex-col items-center text-center pt-32">
|
|
12
|
+
<svg width="91" height="91" viewBox="0 0 91 91" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
13
|
+
<path d="M45.5 91C70.629 91 91 70.629 91 45.5C91 20.371 70.629 0 45.5 0C20.371 0 0 20.371 0 45.5C0 70.629 20.371 91 45.5 91Z" fill="#028163"/>
|
|
14
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.293 67.6904L14.6522 49.8798C12.374 47.4414 12.5047 43.5836 14.9431 41.3054C17.3815 39.0272 21.2393 39.1583 23.5175 41.5963L36.3535 55.3344L56.6713 36.3509C56.854 36.18 57.0449 36.0234 57.2423 35.8794L66.797 26.9525C69.2354 24.6742 73.0935 24.8054 75.3714 27.2437C77.6496 29.6818 77.5185 33.5399 75.0805 35.8182L45.8982 63.0841L45.867 63.0508L35.7724 72.4826L31.293 67.6904Z" fill="white"/>
|
|
15
|
+
</svg>
|
|
16
|
+
|
|
17
|
+
<h3 class="mt-6 mb-4">Solicitarea a fost trimisă cu succes!</h3>
|
|
18
|
+
<div class="text-lg lg:text-xl max-w-sm mb-10">
|
|
19
|
+
Mulțumim pentru încredere. Vom procesa solicitarea ta cât mai curând și te vom contacta pentru confirmare.
|
|
20
|
+
</div>
|
|
21
|
+
<button type="button"
|
|
22
|
+
class="btn btn-secondary btn-full"
|
|
23
|
+
@click="sidebarState.close()"
|
|
24
|
+
>
|
|
25
|
+
Închide
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onMounted } from 'vue';
|
|
3
|
+
import { ordersState } from '../state/ordersState.js';
|
|
4
|
+
import { sidebarState } from '../state/sidebarState.js';
|
|
5
|
+
|
|
6
|
+
const orders = computed(() => ordersState.orders);
|
|
7
|
+
const isLoading = computed(() => ordersState.loading);
|
|
8
|
+
const error = computed(() => ordersState.error);
|
|
9
|
+
|
|
10
|
+
function formatDate(value) {
|
|
11
|
+
if (!value) {
|
|
12
|
+
return '-';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const date = new Date(value);
|
|
16
|
+
|
|
17
|
+
if (Number.isNaN(date.getTime())) {
|
|
18
|
+
return '-';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return date.toLocaleDateString('ro-RO', {
|
|
22
|
+
day: '2-digit',
|
|
23
|
+
month: '2-digit',
|
|
24
|
+
year: 'numeric',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatCurrency(amount, currency) {
|
|
29
|
+
if (amount === null || amount === undefined) {
|
|
30
|
+
return '-';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const value = Number(amount);
|
|
34
|
+
|
|
35
|
+
if (Number.isNaN(value)) {
|
|
36
|
+
return '-';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return `${value.toFixed(2)} ${currency || 'RON'}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onMounted(() => {
|
|
43
|
+
sidebarState.setHeader('Comenzile mele', true);
|
|
44
|
+
|
|
45
|
+
if (!ordersState.orders.length && !ordersState.loading) {
|
|
46
|
+
ordersState.fetchOrders();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div class="flex flex-col min-h-[calc(100vh-228px)]">
|
|
53
|
+
<div v-if="isLoading" class="text-gray-500 text-sm mt-4">
|
|
54
|
+
Se încarcă comenzile...
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div v-else-if="error" class="text-red-600 text-sm mt-4">
|
|
58
|
+
{{ error }}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div v-else-if="!orders.length" class="text-gray-500 text-sm mt-4">
|
|
62
|
+
Nu ai plasat încă nicio comandă.
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div v-else class="space-y-4 flex-1 overflow-y-auto">
|
|
66
|
+
<div v-for="order in orders" :key="order.id" class="pt-4 border border-neutral-300 rounded-lg p-4">
|
|
67
|
+
<div class="flex items-center justify-between pb-4 border-b border-neutral-300">
|
|
68
|
+
<div class="font-medium">
|
|
69
|
+
Comanda nr. #{{ order.id }} · <span class="text-neutral-500">{{ formatDate(order.date) }}</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="font-semibold">
|
|
72
|
+
{{ formatCurrency(order.total, order.currency) }}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="text-sm text-neutral-700 mt-4">
|
|
77
|
+
<span class="font-medium">Produse: </span>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div v-if="order.items?.length" class="mt-2 text-sm text-neutral-600 space-y-1">
|
|
81
|
+
<div
|
|
82
|
+
v-for="(item, index) in order.items"
|
|
83
|
+
:key="`${order.id}-${index}`"
|
|
84
|
+
class="flex items-center justify-between"
|
|
85
|
+
>
|
|
86
|
+
<div>
|
|
87
|
+
{{ item.name || 'Produs' }}
|
|
88
|
+
<span v-if="item.quantity">({{ item.quantity }} buc)</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
{{ formatCurrency(item.price * item.quantity, item.currency || order.currency) }}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div v-else>-</div>
|
|
97
|
+
|
|
98
|
+
<div class="text-sm text-neutral-600 mt-3">
|
|
99
|
+
<span class="font-medium">Status: </span>
|
|
100
|
+
{{ order.status?.label || '-' }}
|
|
101
|
+
</div>
|
|
102
|
+
<div class="text-sm text-neutral-600 mt-2">
|
|
103
|
+
<span class="font-medium">Plată: </span>
|
|
104
|
+
{{ order.payment_type?.label || '-' }}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="mt-3 text-sm text-neutral-600 space-y-1">
|
|
108
|
+
<div class="flex items-center justify-between">
|
|
109
|
+
<span class="font-medium">Subtotal:</span>
|
|
110
|
+
<span>{{ formatCurrency(order.subtotal, order.currency) }}</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div v-if="order.discount_percent" class="text-sm text-neutral-600 mt-2">
|
|
113
|
+
<span class="font-medium">Discount:</span>
|
|
114
|
+
{{ order.discount_percent }}%
|
|
115
|
+
<span v-if="order.coupon_code">({{ order.coupon_code }})</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div v-if="order.discount_percent" class="flex items-center justify-between">
|
|
118
|
+
<span class="font-medium">Reducere:</span>
|
|
119
|
+
<span>-{{ formatCurrency(order.subtotal - order.total, order.currency) }}</span>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="flex items-center justify-between">
|
|
122
|
+
<span class="font-medium">Total final:</span>
|
|
123
|
+
<span>{{ formatCurrency(order.total, order.currency) }}</span>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { sidebarState } from "../state/sidebarState.js";
|
|
3
|
+
import { onMounted } from "vue";
|
|
4
|
+
|
|
5
|
+
onMounted(() => {
|
|
6
|
+
sidebarState.setHeader('', false);
|
|
7
|
+
});
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="flex flex-col items-center text-center pt-32">
|
|
12
|
+
<svg width="91" height="91" viewBox="0 0 91 91" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
13
|
+
<path d="M45.5 91C70.629 91 91 70.629 91 45.5C91 20.371 70.629 0 45.5 0C20.371 0 0 20.371 0 45.5C0 70.629 20.371 91 45.5 91Z" fill="#028163"/>
|
|
14
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.293 67.6904L14.6522 49.8798C12.374 47.4414 12.5047 43.5836 14.9431 41.3054C17.3815 39.0272 21.2393 39.1583 23.5175 41.5963L36.3535 55.3344L56.6713 36.3509C56.854 36.18 57.0449 36.0234 57.2423 35.8794L66.797 26.9525C69.2354 24.6742 73.0935 24.8054 75.3714 27.2437C77.6496 29.6818 77.5185 33.5399 75.0805 35.8182L45.8982 63.0841L45.867 63.0508L35.7724 72.4826L31.293 67.6904Z" fill="white"/>
|
|
15
|
+
</svg>
|
|
16
|
+
|
|
17
|
+
<h3 class="mt-6 mb-4">Comanda a fost trimisă cu succes!</h3>
|
|
18
|
+
<div class="text-lg lg:text-xl max-w-sm mb-10">
|
|
19
|
+
Mulțumim pentru încredere. Vom procesa comanda ta cât mai curând și te vom contacta pentru confirmare.
|
|
20
|
+
</div>
|
|
21
|
+
<button type="button"
|
|
22
|
+
class="btn btn-secondary btn-full"
|
|
23
|
+
@click="sidebarState.close()"
|
|
24
|
+
>
|
|
25
|
+
Închide
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {computed, onMounted, ref, watch} from "vue";
|
|
3
|
+
import {productsState} from "../state/productsState.js";
|
|
4
|
+
import {cartState} from "../state/cartState.js";
|
|
5
|
+
|
|
6
|
+
import QuantityInput from "./QuantityInput.vue";
|
|
7
|
+
|
|
8
|
+
//--- Props -----------------------------------------------------------------------------------------------------------
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
item: {
|
|
11
|
+
type: Object,
|
|
12
|
+
required: true
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
//--- Data ------------------------------------------------------------------------------------------------------------
|
|
17
|
+
let quantity = ref(null);
|
|
18
|
+
const product = computed(() => productsState.getProduct(props.item.id));
|
|
19
|
+
|
|
20
|
+
//--- Watch -----------------------------------------------------------------------------------------------------------
|
|
21
|
+
watch(quantity, (newQuantity, oldQuantity) => {
|
|
22
|
+
if (newQuantity !== oldQuantity)
|
|
23
|
+
cartState.updateQuantity(props.item.id, newQuantity, props.item.price);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
watch(() => props.item.quantity, (newQty) => {
|
|
27
|
+
quantity.value = newQty;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
//--- Mounted ---------------------------------------------------------------------------------------------------------
|
|
31
|
+
onMounted(() => {
|
|
32
|
+
quantity.value = props.item.quantity;
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div class="flex items-center pb-4">
|
|
38
|
+
<img v-if="product.icon_url"
|
|
39
|
+
class="w-24 h-24 object-contain"
|
|
40
|
+
:src="product.icon_url"
|
|
41
|
+
:alt="product.name"
|
|
42
|
+
>
|
|
43
|
+
<div v-else class="w-24 h-24">
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
<div class="ml-15">
|
|
47
|
+
<div class="font-bold text-xl lg:text-2xl mb-1.5">{{ product.name }}</div>
|
|
48
|
+
<div v-if="product.price && product.price > 0"
|
|
49
|
+
class="text-lg lg:text-xl"
|
|
50
|
+
>
|
|
51
|
+
{{ product.price }} {{ product.currency }}/buc
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="ml-auto flex flex-col justify-between">
|
|
55
|
+
<!-- Delete icon ------------------------------------------------------------------------------------->
|
|
56
|
+
<div class="ml-auto hover:brightness-90 cursor-pointer mb-8"
|
|
57
|
+
@click="cartState.removeItem(props.item.id)"
|
|
58
|
+
>
|
|
59
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
60
|
+
<path d="M8.33398 1.66667L7.50065 2.50001H4.16732C3.70732 2.50001 3.33398 2.87334 3.33398 3.33334C3.33398 3.79334 3.70732 4.16667 4.16732 4.16667H5.83398H14.1673H15.834C16.294 4.16667 16.6673 3.79334 16.6673 3.33334C16.6673 2.87334 16.294 2.50001 15.834 2.50001H12.5007L11.6673 1.66667H8.33398ZM4.16732 5.83334V16.6667C4.16732 17.5875 4.91315 18.3333 5.83398 18.3333H14.1673C15.0882 18.3333 15.834 17.5875 15.834 16.6667V5.83334H4.16732Z" fill="#C73838"/>
|
|
61
|
+
</svg>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<quantity-input v-if="product.price && product.price > 0"
|
|
65
|
+
v-model="quantity"
|
|
66
|
+
></quantity-input>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {ref, watch} from "vue";
|
|
3
|
+
|
|
4
|
+
const emits = defineEmits(['update:model-value']);
|
|
5
|
+
let quantity = ref(1);
|
|
6
|
+
|
|
7
|
+
watch(quantity, (newValue) => {
|
|
8
|
+
emits('update:model-value', newValue);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
modelValue: {
|
|
13
|
+
type: Number,
|
|
14
|
+
default: 1
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
watch(() => props.modelValue, (newValue) => {
|
|
19
|
+
quantity.value = newValue;
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div class="flex text-gray-500">
|
|
25
|
+
<button class="cursor-pointer hover:bg-gray-100 active:bg-gray-200 h-11 w-8.5 border border-gray-200 rounded-l flex items-center justify-center"
|
|
26
|
+
:disabled="quantity <= 1"
|
|
27
|
+
@click="quantity = quantity > 1 ? quantity - 1 : 1"
|
|
28
|
+
>
|
|
29
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
30
|
+
<path d="M13.3333 7.33334H2.66667C2.29867 7.33334 2 7.63201 2 8.00001C2 8.36801 2.29867 8.66668 2.66667 8.66668H13.3333C13.7013 8.66668 14 8.36801 14 8.00001C14 7.63201 13.7013 7.33334 13.3333 7.33334Z" fill="#6E7077"/>
|
|
31
|
+
</svg>
|
|
32
|
+
</button>
|
|
33
|
+
|
|
34
|
+
<input id="quantity" label="Cantitate" class="h-11 w-12 lg:w-16 flex border-t border-b border-gray-200 items-center text-center justify-center text-lg lg:text-xl" v-model="quantity">
|
|
35
|
+
|
|
36
|
+
<button class="cursor-pointer hover:bg-gray-100 active:bg-gray-200 h-11 w-8.5 border border-gray-200 rounded-r flex items-center justify-center"
|
|
37
|
+
@click="quantity++"
|
|
38
|
+
>
|
|
39
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
40
|
+
<path d="M13.3333 7.33333H8.66667V2.66667C8.66667 2.29867 8.368 2 8 2C7.632 2 7.33333 2.29867 7.33333 2.66667V7.33333H2.66667C2.29867 7.33333 2 7.632 2 8C2 8.368 2.29867 8.66667 2.66667 8.66667H7.33333V13.3333C7.33333 13.7013 7.632 14 8 14C8.368 14 8.66667 13.7013 8.66667 13.3333V8.66667H13.3333C13.7013 8.66667 14 8.368 14 8C14 7.632 13.7013 7.33333 13.3333 7.33333Z" fill="#6E7077"/>
|
|
41
|
+
</svg>
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { sidebarState } from '../state/sidebarState.js';
|
|
3
|
+
import { erpCityUiConfig } from '../config.js';
|
|
4
|
+
|
|
5
|
+
import AddressForm from "./AddressForm.vue";
|
|
6
|
+
import CartDetails from "./CartDetails.vue";
|
|
7
|
+
import OrderConfirmation from "./OrderConfirmation.vue";
|
|
8
|
+
import PaymentOrderConfirmation from "./PaymentOrderConfirmation.vue";
|
|
9
|
+
import AuthSidebar from "./AuthSidebar.vue";
|
|
10
|
+
import FavoritesSidebar from "./FavoritesSidebar.vue";
|
|
11
|
+
import OrdersSidebar from "./OrdersSidebar.vue";
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div v-if="sidebarState.isOpen"
|
|
16
|
+
class="bg-white z-30 px-6 py-6 fixed right-0 w-full max-w-[42rem] shadow-lg overflow-y-auto"
|
|
17
|
+
>
|
|
18
|
+
<!-- Header -------------------------------------------------------------------------------------------------->
|
|
19
|
+
<div class="flex items-center justify-between mb-15">
|
|
20
|
+
<!-- Back icon ------------------------------------------------------------------------------------------->
|
|
21
|
+
<div v-if="sidebarState.withBack"
|
|
22
|
+
class="cursor-pointer"
|
|
23
|
+
@click="sidebarState.goBack()"
|
|
24
|
+
>
|
|
25
|
+
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
26
|
+
<path d="M13.3324 7.1224C12.9912 7.1224 12.6497 7.25496 12.3897 7.51563L4.84803 15.0573C4.3267 15.5786 4.3267 16.4227 4.84803 16.9427L12.3897 24.4844C12.911 25.0057 13.7551 25.0057 14.2751 24.4844L14.3897 24.3698C14.911 23.8485 14.911 23.0044 14.3897 22.4844L9.23866 17.3333H26.6657C27.4017 17.3333 27.9991 16.736 27.9991 16C27.9991 15.264 27.4017 14.6667 26.6657 14.6667H9.23866L14.3897 9.51563C14.911 8.99429 14.911 8.15021 14.3897 7.63021L14.2751 7.51563C14.0144 7.25496 13.6736 7.1224 13.3324 7.1224Z" fill="black"/>
|
|
27
|
+
</svg>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Title ----------------------------------------------------------------------------------------------->
|
|
31
|
+
<h3>
|
|
32
|
+
{{ sidebarState.title }}
|
|
33
|
+
</h3>
|
|
34
|
+
|
|
35
|
+
<!-- Close icon ------------------------------------------------------------------------------------------>
|
|
36
|
+
<div class="cursor-pointer"
|
|
37
|
+
@click="sidebarState.close()"
|
|
38
|
+
>
|
|
39
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
40
|
+
<path d="M4.99105 3.99024C4.79212 3.99029 4.59772 4.04967 4.43271 4.16079C4.2677 4.2719 4.13958 4.4297 4.06472 4.61401C3.98986 4.79832 3.97167 5.00077 4.01248 5.19548C4.05328 5.39018 4.15122 5.56829 4.29379 5.70704L10.5868 12L4.29379 18.293C4.19781 18.3851 4.12119 18.4955 4.0684 18.6176C4.01562 18.7397 3.98773 18.8712 3.98638 19.0042C3.98503 19.1373 4.01023 19.2692 4.06052 19.3924C4.11081 19.5156 4.18517 19.6275 4.27925 19.7216C4.37333 19.8157 4.48523 19.89 4.60841 19.9403C4.73159 19.9906 4.86356 20.0158 4.9966 20.0144C5.12964 20.0131 5.26108 19.9852 5.38321 19.9324C5.50533 19.8796 5.6157 19.803 5.70785 19.707L12.0008 13.4141L18.2938 19.707C18.3859 19.803 18.4963 19.8796 18.6184 19.9324C18.7406 19.9852 18.872 20.0131 19.005 20.0145C19.1381 20.0158 19.27 19.9906 19.3932 19.9403C19.5164 19.89 19.6283 19.8157 19.7224 19.7216C19.8165 19.6275 19.8908 19.5156 19.9411 19.3924C19.9914 19.2692 20.0166 19.1373 20.0153 19.0042C20.0139 18.8712 19.986 18.7397 19.9332 18.6176C19.8804 18.4955 19.8038 18.3851 19.7078 18.293L13.4149 12L19.7078 5.70704C19.8523 5.56658 19.951 5.38574 19.991 5.18822C20.031 4.9907 20.0103 4.78571 19.9318 4.60013C19.8532 4.41454 19.7205 4.25701 19.5509 4.14818C19.3812 4.03935 19.1827 3.9843 18.9813 3.99024C18.7215 3.99798 18.4749 4.10656 18.2938 4.29298L12.0008 10.5859L5.70785 4.29298C5.61466 4.19718 5.50321 4.12104 5.3801 4.06904C5.25698 4.01704 5.1247 3.99025 4.99105 3.99024Z" fill="black"/>
|
|
41
|
+
</svg>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<cart-details v-if="sidebarState.currentView === 'cart'"
|
|
46
|
+
></cart-details>
|
|
47
|
+
|
|
48
|
+
<address-form v-else-if="sidebarState.currentView === 'address'"
|
|
49
|
+
></address-form>
|
|
50
|
+
|
|
51
|
+
<order-confirmation v-else-if="sidebarState.currentView === 'confirmation'"
|
|
52
|
+
></order-confirmation>
|
|
53
|
+
|
|
54
|
+
<payment-order-confirmation v-else-if="sidebarState.currentView === 'payment-confirmation'"
|
|
55
|
+
></payment-order-confirmation>
|
|
56
|
+
|
|
57
|
+
<auth-sidebar v-else-if="sidebarState.currentView === 'auth'"
|
|
58
|
+
></auth-sidebar>
|
|
59
|
+
|
|
60
|
+
<favorites-sidebar v-else-if="erpCityUiConfig.showFavorites && sidebarState.currentView === 'favorites'"
|
|
61
|
+
></favorites-sidebar>
|
|
62
|
+
|
|
63
|
+
<orders-sidebar v-else-if="erpCityUiConfig.showOrders && sidebarState.currentView === 'orders'"
|
|
64
|
+
></orders-sidebar>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {ref} from "vue";
|
|
2
|
+
|
|
3
|
+
export function useHandlesFormErrors() {
|
|
4
|
+
let errorBag = ref({});
|
|
5
|
+
let genericError = ref(null);
|
|
6
|
+
|
|
7
|
+
//--- Methods -----------------------------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function setErrorBag(error) {
|
|
10
|
+
// Validation errors
|
|
11
|
+
if (error.response.status === 422)
|
|
12
|
+
errorBag.value = error.response.data.errors;
|
|
13
|
+
|
|
14
|
+
genericError.value = error.response.data.message;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function emptyErrorBag() {
|
|
18
|
+
errorBag.value = {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function errorBagIsEmpty() {
|
|
22
|
+
return !Object.keys(errorBag.value).length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function error(fieldName) {
|
|
26
|
+
if (fieldName === 'genericError')
|
|
27
|
+
return genericError.value;
|
|
28
|
+
|
|
29
|
+
return errorBag.value[fieldName] ? errorBag.value[fieldName][0] : '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
error,
|
|
34
|
+
genericError,
|
|
35
|
+
setErrorBag,
|
|
36
|
+
emptyErrorBag,
|
|
37
|
+
errorBagIsEmpty,
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/config.js
ADDED