Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
@@ -5,11 +5,18 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<NuxtRouteAnnouncer />
|
<NuxtRouteAnnouncer />
|
||||||
|
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
|
<ToastProvider>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
<div dir="ltr">
|
<div dir="ltr">
|
||||||
<VueQueryDevtools dir="ltr" buttonPosition="bottom-left" />
|
<VueQueryDevtools dir="ltr" buttonPosition="bottom-left" />
|
||||||
</div>
|
</div>
|
||||||
|
<ToastContainer />
|
||||||
|
<ToastViewport
|
||||||
|
class="[--viewport-padding:_25px] fixed bottom-0 right-0 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[2147483647] outline-none"
|
||||||
|
/>
|
||||||
|
</ToastProvider>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -36,10 +36,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
@apply bg-slate-50 text-slate-300;
|
@apply bg-slate-100 text-slate-400;
|
||||||
|
|
||||||
svg[class~=iconify] path {
|
svg[class~=iconify] path {
|
||||||
@apply stroke-slate-300;
|
@apply stroke-slate-400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,10 +57,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
@apply bg-slate-100 text-slate-300;
|
@apply bg-slate-100 text-slate-400;
|
||||||
|
|
||||||
svg[class~=iconify] path {
|
svg[class~=iconify] path {
|
||||||
@apply stroke-slate-300;
|
@apply stroke-slate-400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,10 +103,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
@apply text-slate-300;
|
@apply text-slate-400;
|
||||||
|
|
||||||
svg[class~=iconify] path {
|
svg[class~=iconify] path {
|
||||||
@apply stroke-slate-300;
|
@apply stroke-slate-400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,16 +120,15 @@
|
|||||||
--breakpoint-xs: 480px;
|
--breakpoint-xs: 480px;
|
||||||
|
|
||||||
/* ANIMATIONS */
|
/* ANIMATIONS */
|
||||||
<<<<<<< HEAD
|
|
||||||
--animate-marquee: marquee 3s linear infinite;
|
--animate-marquee: marquee 3s linear infinite;
|
||||||
--animate-slide-down: slideDown 300ms ease-out;
|
--animate-slide-down: slideDown 300ms ease-out;
|
||||||
--animate-slide-up: slideUp 300ms ease-out;
|
--animate-slide-up: slideUp 300ms ease-out;
|
||||||
--animate-overlay-show: overlayShow 150ms ease-in;
|
--animate-overlay-show: overlayShow 150ms ease-in;
|
||||||
--animate-content-show: contentShow 150ms ease-in;
|
--animate-content-show: contentShow 150ms ease-in;
|
||||||
=======
|
|
||||||
--animate-marquee: marquee 25s linear infinite;
|
--animate-toast-hide: toastHide 100ms ease-in;
|
||||||
--animate-marquee-reverse: marquee 25s linear infinite reverse;
|
--animate-toast-in: toastSlideIn 600ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
>>>>>>> be4fa509843c81855f5ffc118150196c94a7b17b
|
--animate-toast-out: toastSlideOut 200ms ease-out;
|
||||||
|
|
||||||
@keyframes marquee {
|
@keyframes marquee {
|
||||||
to {
|
to {
|
||||||
@@ -174,6 +173,33 @@
|
|||||||
transform: translate(-50%, -50%) scale(1);
|
transform: translate(-50%, -50%) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes toastHide {
|
||||||
|
from { opacity: 1 }
|
||||||
|
to { opacity: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(calc(100% + var(--viewport-padding)))
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastSlideOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(var(--reka-toast-swipe-end-x))
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(calc(100% + var(--viewport-padding)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CONTAINER */
|
/* CONTAINER */
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ type Props = {
|
|||||||
size?: "xl" | "lg" | "md";
|
size?: "xl" | "lg" | "md";
|
||||||
startIcon?: string;
|
startIcon?: string;
|
||||||
endIcon?: string;
|
endIcon?: string;
|
||||||
|
loading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// props
|
// props
|
||||||
@@ -35,8 +36,9 @@ const classes = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button :class="classes">
|
<button :class="classes">
|
||||||
<Icon v-if="startIcon" :name="startIcon" />
|
<Icon v-if="!loading && startIcon" :name="startIcon" />
|
||||||
<slot />
|
<slot v-if="!loading" />
|
||||||
<Icon v-if="endIcon" :name="endIcon" />
|
<Icon v-if="!loading && endIcon" :name="endIcon" />
|
||||||
|
<Icon v-if="loading" name="svg-spinners:3-dots-fade" class="my-0.5" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,51 +1,66 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// types
|
|
||||||
import Tooltip from "~/components/ui/Tooltip.vue";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
variant?: "solid" | "outlined";
|
variant?: "solid" | "outlined";
|
||||||
startIcon?: string;
|
|
||||||
endIcon?: string;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
modelValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// props
|
// props
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
variant: "solid",
|
variant: "solid"
|
||||||
});
|
});
|
||||||
const { variant, message, error, disabled } = toRefs(props);
|
const { variant, message, error, disabled } = toRefs(props);
|
||||||
|
|
||||||
|
// emits
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const inputRef = ref<HTMLInputElement | null>(null);
|
const inputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
|
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
return [
|
return [
|
||||||
"flex items-center cursor-text transition-all border-[1.5px] gap-3 typo-label-md p-4 rounded-100",
|
"flex items-center cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3 rounded-100",
|
||||||
{
|
{
|
||||||
"input-solid": variant.value === "solid",
|
"input-solid": variant.value === "solid",
|
||||||
"input-outlined": variant.value === "outlined",
|
"input-outlined": variant.value === "outlined",
|
||||||
"input-effects": !error.value,
|
"input-effects": !error.value,
|
||||||
[variant.value === "solid"
|
[variant.value === "solid"
|
||||||
? "input-solid-error"
|
? "input-solid-error"
|
||||||
: "input-outlined-error"]: error.value,
|
: "input-outlined-error"]: error.value
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const onInput = (e: any) => {
|
||||||
|
emit("update:modelValue", e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-bind="$attrs" :class="classes" @click="inputRef?.focus()">
|
<div v-bind="$attrs" :class="classes" @click="inputRef?.focus()">
|
||||||
<Icon v-if="startIcon" :name="startIcon" class="ms-0" size="24px" />
|
<slot name="startItem" />
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
:value="modelValue"
|
||||||
|
@input="onInput"
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
class="outline-none w-max"
|
class="outline-none w-max"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
/>
|
/>
|
||||||
<Icon v-if="endIcon" :name="endIcon" class="me-0" size="24px" />
|
|
||||||
|
<slot name="endItem" />
|
||||||
</div>
|
</div>
|
||||||
<!-- <Tooltip :title="message" class="w-full">
|
<!-- <Tooltip :title="message" class="w-full">
|
||||||
</Tooltip> -->
|
</Tooltip> -->
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
status?: "success" | "error" | "idle";
|
||||||
|
modelValue: never[];
|
||||||
|
autofocus?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// props
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
status: "idle"
|
||||||
|
});
|
||||||
|
const { modelValue, disabled, status } = toRefs(props);
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $gsap: gsap } = useNuxtApp();
|
||||||
|
|
||||||
|
// emit
|
||||||
|
|
||||||
|
const emit = defineEmits(["complete", "update:modelValue"]);
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const currentOtpCode = ref([]);
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
emit("update:modelValue", currentOtpCode.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
emit("update:modelValue", currentOtpCode.value);
|
||||||
|
emit("complete", currentOtpCode.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const playStatusAnimation = () => {
|
||||||
|
|
||||||
|
const inputCount = 6;
|
||||||
|
const duration = 0.100;
|
||||||
|
|
||||||
|
let statusColor = {
|
||||||
|
border : "",
|
||||||
|
bg : ""
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status.value === "success") {
|
||||||
|
statusColor.border = "var(--color-success-500)";
|
||||||
|
statusColor.bg = "var(--color-success-50)";
|
||||||
|
} else if (status.value === "error") {
|
||||||
|
statusColor.border = "var(--color-danger-500)";
|
||||||
|
statusColor.bg = "var(--color-danger-50)";
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
const animate = (index: number) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
gsap.to(`#otp-input-${index}`, {
|
||||||
|
borderColor: statusColor.border,
|
||||||
|
backgroundColor: statusColor.bg,
|
||||||
|
scale: 1.2,
|
||||||
|
duration: duration / 2
|
||||||
|
});
|
||||||
|
|
||||||
|
gsap.to(`#otp-input-${index}`, {
|
||||||
|
scale: 1,
|
||||||
|
duration: duration / 2,
|
||||||
|
delay: duration
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
gsap.to(`#otp-input-${index}`, {
|
||||||
|
borderColor: "black",
|
||||||
|
backgroundColor: "var(--color-slate-50)"
|
||||||
|
});
|
||||||
|
}, (inputCount + 1) * duration * 3000);
|
||||||
|
|
||||||
|
}, index * duration * 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
while (index < 6) {
|
||||||
|
animate(index);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// watch
|
||||||
|
|
||||||
|
watch(() => modelValue.value, (value) => {
|
||||||
|
currentOtpCode.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => disabled.value, (value) => {
|
||||||
|
if (!value) {
|
||||||
|
const otpInputFirst = document.querySelector("#otp-input-0") as HTMLInputElement;
|
||||||
|
setTimeout(() => {
|
||||||
|
otpInputFirst.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => status.value, (value) => {
|
||||||
|
if (value !== "idle") {
|
||||||
|
playStatusAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PinInputRoot
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="$attrs"
|
||||||
|
type="number"
|
||||||
|
v-model="currentOtpCode"
|
||||||
|
placeholder="_"
|
||||||
|
class="flex gap-4 items-center justify-center mt-1"
|
||||||
|
@change="handleChange"
|
||||||
|
@complete="handleComplete"
|
||||||
|
otp
|
||||||
|
>
|
||||||
|
<PinInputInput
|
||||||
|
v-for="(id, index) in 6"
|
||||||
|
:id="`otp-input-${index}`"
|
||||||
|
:key="id"
|
||||||
|
:index="index"
|
||||||
|
:autofocus="autofocus ? index === 0 ? true : 'off' : 'off'"
|
||||||
|
class="disabled:text-slate-400 focus-within:border-black transition-all size-16 bg-slate-50 typo-label-lg rounded-lg text-center border-[1.5px] border-slate-200 outline-none"
|
||||||
|
/>
|
||||||
|
</PinInputRoot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
// type
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: number;
|
||||||
|
message: string,
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// props
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const { id } = toRefs(props);
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { destroyToast } = useToast();
|
||||||
|
|
||||||
|
const open = ref(true);
|
||||||
|
|
||||||
|
// method
|
||||||
|
|
||||||
|
const onSwipeEnd = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
destroyToast(id);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// watch
|
||||||
|
|
||||||
|
watch(() => open.value, (value) => {
|
||||||
|
if (!value) onSwipeEnd()
|
||||||
|
});
|
||||||
|
|
||||||
|
// lifecycle
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
open.value = false;
|
||||||
|
}, 4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToastRoot
|
||||||
|
@swipeEnd="onSwipeEnd"
|
||||||
|
v-model:open="open"
|
||||||
|
class="bg-white rounded-lg shadow-xl shadow-black/5 border-[0.5px] border-black p-[15px] grid [grid-template-areas:_'title_action'_'description_action'] grid-cols-[auto_max-content] gap-x-[15px] items-center data-[state=open]:animate-toast-in data-[state=closed]:animate-toast-hide data-[swipe=move]:translate-x-[var(--reka-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out] data-[swipe=end]:animate-toast-out"
|
||||||
|
>
|
||||||
|
<ToastTitle class="[grid-area:_title] mb-[5px] font-medium text-black text-sm">
|
||||||
|
{{ message }}
|
||||||
|
</ToastTitle>
|
||||||
|
<ToastDescription v-if="description" as-child>
|
||||||
|
<div
|
||||||
|
class="[grid-area:_description] m-0 text-black leading-[1.3]"
|
||||||
|
>
|
||||||
|
{{ description }}
|
||||||
|
</div>
|
||||||
|
</ToastDescription>
|
||||||
|
</ToastRoot>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
import ToastBox from "~/components/ui/ToastContainer/ToastBox.vue";
|
||||||
|
|
||||||
|
const { toasts } = useToast();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToastBox
|
||||||
|
v-for="toast in toasts"
|
||||||
|
:key="toast.id"
|
||||||
|
:id="toast.id"
|
||||||
|
:message="toast.message"
|
||||||
|
:description="toast.description"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/vue-query";
|
||||||
|
import axios from "~/configs/axios.config";
|
||||||
|
import { API_ENDPOINTS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type OtpRequest = {
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
export const handleOtp = async (variables: OtpRequest) => {
|
||||||
|
const { data } = await axios.post<OtpRequest>(`${API_ENDPOINTS.account.send_otp}`, variables);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// composable
|
||||||
|
|
||||||
|
const useOtp = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: OtpRequest) => handleOtp(variables)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useOtp;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/vue-query";
|
||||||
|
import axios from "~/configs/axios.config";
|
||||||
|
import { API_ENDPOINTS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type SignInRequest = {
|
||||||
|
otp: string;
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
export const handleSignIn = async (variables: SignInRequest) => {
|
||||||
|
const { data } = await axios.post<SignInRequest>(`${API_ENDPOINTS.auth.signin}/`, variables);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// composable
|
||||||
|
|
||||||
|
const useSignIn = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: SignInRequest) => handleSignIn(variables)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSignIn;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
type Props = {
|
||||||
|
duration: number;
|
||||||
|
callback?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTimer({ duration, callback }: Props) {
|
||||||
|
const timeout = ref<NodeJS.Timeout | null>(null);
|
||||||
|
const interval = ref<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const isPending = ref(false);
|
||||||
|
const timer = ref(duration / 1000);
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
if (timeout.value) clearTimeout(timeout.value);
|
||||||
|
if (interval.value) clearInterval(interval.value);
|
||||||
|
|
||||||
|
isPending.value = false;
|
||||||
|
timer.value = duration / 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
isPending.value = true;
|
||||||
|
|
||||||
|
timeout.value = setTimeout(() => {
|
||||||
|
if (interval.value) clearInterval(interval.value);
|
||||||
|
if (callback) callback();
|
||||||
|
isPending.value = false;
|
||||||
|
timer.value = duration / 1000;
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
interval.value = setInterval(() => {
|
||||||
|
timer.value -= 1;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPending,
|
||||||
|
timer,
|
||||||
|
reset,
|
||||||
|
start
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
type Toast = {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
description?: string;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
message: string,
|
||||||
|
description?: string,
|
||||||
|
options?: Omit<Toast, "id" | "message">
|
||||||
|
}
|
||||||
|
|
||||||
|
const toasts = ref<Toast[]>([]);
|
||||||
|
|
||||||
|
export function useToast() {
|
||||||
|
const addToast = ({ message, description, options = {} }: Props) => {
|
||||||
|
const id = Date.now();
|
||||||
|
|
||||||
|
toasts.value.push({ id, message, description, ...options });
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroyToast = (id: number) => {
|
||||||
|
toasts.value = toasts.value.filter(toast => toast.id !== id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
toasts,
|
||||||
|
addToast,
|
||||||
|
destroyToast
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
export const API_ENDPOINTS = {
|
export const API_ENDPOINTS = {
|
||||||
|
account : {
|
||||||
|
send_otp : "/accounts/send_otp",
|
||||||
|
},
|
||||||
auth: {
|
auth: {
|
||||||
login: "/token",
|
signin: "/token",
|
||||||
logout: "/accounts/logout",
|
logout: "/accounts/logout",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,4 +44,10 @@ export default defineNuxtConfig({
|
|||||||
"@nuxt/icon",
|
"@nuxt/icon",
|
||||||
"reka-ui/nuxt",
|
"reka-ui/nuxt",
|
||||||
],
|
],
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
API_BASE_URL: "http://38.60.202.91:8000",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+91
-11
@@ -11,6 +11,8 @@
|
|||||||
"@nuxtjs/google-fonts": "^3.2.0",
|
"@nuxtjs/google-fonts": "^3.2.0",
|
||||||
"@tanstack/vue-query": "^5.62.2",
|
"@tanstack/vue-query": "^5.62.2",
|
||||||
"@tanstack/vue-query-devtools": "^5.62.3",
|
"@tanstack/vue-query-devtools": "^5.62.3",
|
||||||
|
"@vuelidate/core": "^2.0.3",
|
||||||
|
"@vuelidate/validators": "^2.0.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"gsap": "^3.12.5",
|
"gsap": "^3.12.5",
|
||||||
"nuxt": "^3.14.1592",
|
"nuxt": "^3.14.1592",
|
||||||
@@ -18,8 +20,7 @@
|
|||||||
"swiper": "^11.1.15",
|
"swiper": "^11.1.15",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-router": "latest",
|
"vue-router": "latest",
|
||||||
"vue-scrollto": "^2.20.0",
|
"vue-scrollto": "^2.20.0"
|
||||||
"vue-toastification": "^2.0.0-rc.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.0.0-beta.5",
|
"@tailwindcss/postcss": "^4.0.0-beta.5",
|
||||||
@@ -3309,6 +3310,94 @@
|
|||||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@vuelidate/core": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vuelidate/core/-/core-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-AN6l7KF7+mEfyWG0doT96z+47ljwPpZfi9/JrNMkOGLFv27XVZvKzRLXlmDPQjPl/wOB1GNnHuc54jlCLRNqGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": "^0.13.11"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^2.0.0 || >=3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vuelidate/core/node_modules/vue-demi": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vuelidate/validators": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vuelidate/validators/-/validators-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-odTxtUZ2JpwwiQ10t0QWYJkkYrfd0SyFYhdHH44QQ1jDatlZgTh/KRzrWVmn/ib9Gq7H4hFD4e8ahoo5YlUlDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": "^0.13.11"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^2.0.0 || >=3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vuelidate/validators/node_modules/vue-demi": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vueuse/core": {
|
"node_modules/@vueuse/core": {
|
||||||
"version": "12.0.0",
|
"version": "12.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz",
|
||||||
@@ -10451,15 +10540,6 @@
|
|||||||
"bezier-easing": "2.1.0"
|
"bezier-easing": "2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-toastification": {
|
|
||||||
"version": "2.0.0-rc.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
|
|
||||||
"integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "node .output/server/index.mjs",
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"dev-o": "nuxt dev -- -o",
|
"dev-o": "nuxt dev -- -o",
|
||||||
@@ -15,6 +16,8 @@
|
|||||||
"@nuxtjs/google-fonts": "^3.2.0",
|
"@nuxtjs/google-fonts": "^3.2.0",
|
||||||
"@tanstack/vue-query": "^5.62.2",
|
"@tanstack/vue-query": "^5.62.2",
|
||||||
"@tanstack/vue-query-devtools": "^5.62.3",
|
"@tanstack/vue-query-devtools": "^5.62.3",
|
||||||
|
"@vuelidate/core": "^2.0.3",
|
||||||
|
"@vuelidate/validators": "^2.0.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"gsap": "^3.12.5",
|
"gsap": "^3.12.5",
|
||||||
"nuxt": "^3.14.1592",
|
"nuxt": "^3.14.1592",
|
||||||
@@ -22,8 +25,7 @@
|
|||||||
"swiper": "^11.1.15",
|
"swiper": "^11.1.15",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-router": "latest",
|
"vue-router": "latest",
|
||||||
"vue-scrollto": "^2.20.0",
|
"vue-scrollto": "^2.20.0"
|
||||||
"vue-toastification": "^2.0.0-rc.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.0.0-beta.5",
|
"@tailwindcss/postcss": "^4.0.0-beta.5",
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
// import
|
||||||
|
|
||||||
|
import { helpers, required } from "@vuelidate/validators";
|
||||||
|
import { useVuelidate } from "@vuelidate/core";
|
||||||
|
import useOtp from "~/composables/api/auth/useOtp";
|
||||||
|
import { useTimer } from "~/composables/useTimer";
|
||||||
|
import useSignIn from "~/composables/api/auth/useSignIn";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
type LoginInfo = {
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { addToast } = useToast();
|
||||||
|
|
||||||
|
const showOtp = ref(false);
|
||||||
|
const otpCode = ref([]);
|
||||||
|
|
||||||
|
const formRules = computed(() => {
|
||||||
|
return {
|
||||||
|
phone: {
|
||||||
|
required: helpers.withMessage("Phone is required", required),
|
||||||
|
phoneValidator: helpers.regex(/^[1-9][0-9]{9}$/)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginInfo = ref<LoginInfo>({
|
||||||
|
phone: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const formValidator$ = useVuelidate(formRules, loginInfo);
|
||||||
|
|
||||||
|
const {
|
||||||
|
timer: otpBlockerTimePassed,
|
||||||
|
start: startOtpBlocker,
|
||||||
|
reset: resetOtpBlocker,
|
||||||
|
isPending: isResendOtpBlocked
|
||||||
|
} = useTimer({
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: sendOtp, isPending: sendOtpIsPending } = useOtp();
|
||||||
|
const { mutateAsync: signIn, isPending: signInIsPending, status: signInStatus } = useSignIn();
|
||||||
|
|
||||||
|
// computed
|
||||||
|
|
||||||
|
const sendOtpHandler = async () => {
|
||||||
|
if (!sendOtpIsPending.value) {
|
||||||
|
try {
|
||||||
|
await sendOtp({
|
||||||
|
phone: `0${loginInfo.value.phone}`
|
||||||
|
});
|
||||||
|
|
||||||
|
addToast({ message: "کد برای شما ارسال شد" });
|
||||||
|
|
||||||
|
showOtp.value = true;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
addToast({ message: "مشکلی پیش آمده" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!showOtp.value) {
|
||||||
|
await formValidator$.value.$validate();
|
||||||
|
|
||||||
|
if (!formValidator$.value.$errors.length) {
|
||||||
|
await sendOtpHandler();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await signIn({
|
||||||
|
otp: otpCode.value.join(""),
|
||||||
|
phone: `0${loginInfo.value.phone}`
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
otpCode.value = [];
|
||||||
|
addToast({ message: "مشکلی پیش آمده" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resendOtp = async () => {
|
||||||
|
try {
|
||||||
|
await sendOtpHandler();
|
||||||
|
resetOtpBlocker();
|
||||||
|
startOtpBlocker();
|
||||||
|
} catch (e) {
|
||||||
|
resetOtpBlocker();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
loginInfo.value.phone = "";
|
||||||
|
formValidator$.value.$reset();
|
||||||
|
otpCode.value = [];
|
||||||
|
showOtp.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container min-h-[700px] flex flex-col items-center justify-center">
|
||||||
|
<h1 class="typo-hero-2">
|
||||||
|
فرم ورود
|
||||||
|
</h1>
|
||||||
|
<form
|
||||||
|
@submit.prevent
|
||||||
|
class="max-w-[500px] w-full mt-12"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!showOtp"
|
||||||
|
class="flex items-center gap-2 w-full"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
class="w-full"
|
||||||
|
v-model="loginInfo.phone"
|
||||||
|
placeholder="9380123456"
|
||||||
|
dir="ltr"
|
||||||
|
:error="formValidator$.phone.$error"
|
||||||
|
>
|
||||||
|
<template #startItem>
|
||||||
|
<span class="text-slate-500">
|
||||||
|
+98
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Input>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Icon class="translate-y-[-1px]" name="twemoji:flag-iran" size="24" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OtpInput
|
||||||
|
v-else
|
||||||
|
v-model="otpCode"
|
||||||
|
:status="signInStatus === 'success' ? 'success' : signInStatus === 'error' ? 'error' : 'idle'"
|
||||||
|
:disabled="signInIsPending || sendOtpIsPending"
|
||||||
|
:autofocus="true"
|
||||||
|
@complete="handleLogin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
v-if="!showOtp"
|
||||||
|
class="rounded-full w-full mt-4"
|
||||||
|
type="submit"
|
||||||
|
@click="handleLogin"
|
||||||
|
:loading="sendOtpIsPending"
|
||||||
|
:disabled="sendOtpIsPending"
|
||||||
|
>
|
||||||
|
ارسال کد
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex items-center w-full gap-4 mt-4"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="rounded-full w-full mt-4"
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
@click="resetForm"
|
||||||
|
:disabled="signInIsPending || sendOtpIsPending"
|
||||||
|
>
|
||||||
|
تغییر شماره
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="rounded-full w-full mt-4"
|
||||||
|
type="submit"
|
||||||
|
@click="resendOtp"
|
||||||
|
:loading="signInIsPending || sendOtpIsPending"
|
||||||
|
:disabled="signInIsPending || isResendOtpBlocked || sendOtpIsPending"
|
||||||
|
>
|
||||||
|
ارسال مجدد کد
|
||||||
|
{{ isResendOtpBlocked ? otpBlockerTimePassed : "" }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 justify-center mt-6">
|
||||||
|
<span>
|
||||||
|
بازگشت به فروشگاه
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
name="ci:left-rotation"
|
||||||
|
size="24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import Toast, { useToast } from "vue-toastification";
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
nuxtApp.vueApp.use(Toast, {
|
|
||||||
position: "top-center",
|
|
||||||
hideProgressBar: true,
|
|
||||||
transition: "Vue-Toastification__fade",
|
|
||||||
maxToasts: 3,
|
|
||||||
closeButton: false,
|
|
||||||
timeout: 1800,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
provide: {
|
|
||||||
toast: useToast(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user