diff --git a/frontend/app.vue b/frontend/app.vue index 2cf361f..7cd4dbe 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -5,11 +5,18 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools"; + - - - - + + + + + + + + diff --git a/frontend/assets/css/button.comp.css b/frontend/assets/css/button.comp.css index 05a12d5..db43161 100644 --- a/frontend/assets/css/button.comp.css +++ b/frontend/assets/css/button.comp.css @@ -36,10 +36,10 @@ } &:disabled { - @apply bg-slate-50 text-slate-300; + @apply bg-slate-100 text-slate-400; svg[class~=iconify] path { - @apply stroke-slate-300; + @apply stroke-slate-400; } } } @@ -57,10 +57,10 @@ } &:disabled { - @apply bg-slate-100 text-slate-300; + @apply bg-slate-100 text-slate-400; svg[class~=iconify] path { - @apply stroke-slate-300; + @apply stroke-slate-400; } } } @@ -103,10 +103,10 @@ } &:disabled { - @apply text-slate-300; + @apply text-slate-400; svg[class~=iconify] path { - @apply stroke-slate-300; + @apply stroke-slate-400; } } } diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css index 4e59175..1ce8cae 100644 --- a/frontend/assets/css/tailwind.css +++ b/frontend/assets/css/tailwind.css @@ -120,16 +120,15 @@ --breakpoint-xs: 480px; /* ANIMATIONS */ -<<<<<<< HEAD --animate-marquee: marquee 3s linear infinite; --animate-slide-down: slideDown 300ms ease-out; --animate-slide-up: slideUp 300ms ease-out; --animate-overlay-show: overlayShow 150ms ease-in; --animate-content-show: contentShow 150ms ease-in; -======= - --animate-marquee: marquee 25s linear infinite; - --animate-marquee-reverse: marquee 25s linear infinite reverse; ->>>>>>> be4fa509843c81855f5ffc118150196c94a7b17b + + --animate-toast-hide: toastHide 100ms ease-in; + --animate-toast-in: toastSlideIn 600ms cubic-bezier(0.16, 1, 0.3, 1); + --animate-toast-out: toastSlideOut 200ms ease-out; @keyframes marquee { to { @@ -174,6 +173,33 @@ 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 */ diff --git a/frontend/components/ui/Button.vue b/frontend/components/ui/Button.vue index 46ea375..4624e10 100644 --- a/frontend/components/ui/Button.vue +++ b/frontend/components/ui/Button.vue @@ -5,6 +5,7 @@ type Props = { size?: "xl" | "lg" | "md"; startIcon?: string; endIcon?: string; + loading?: boolean; }; // props @@ -35,8 +36,9 @@ const classes = computed(() => { - - - + + + + diff --git a/frontend/components/ui/Input.vue b/frontend/components/ui/Input.vue index 6a69c0f..9054835 100644 --- a/frontend/components/ui/Input.vue +++ b/frontend/components/ui/Input.vue @@ -1,51 +1,66 @@ - + + - + + diff --git a/frontend/components/ui/OtpInput.vue b/frontend/components/ui/OtpInput.vue new file mode 100644 index 0000000..590fedd --- /dev/null +++ b/frontend/components/ui/OtpInput.vue @@ -0,0 +1,138 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/components/ui/ToastContainer/ToastBox.vue b/frontend/components/ui/ToastContainer/ToastBox.vue new file mode 100644 index 0000000..152a043 --- /dev/null +++ b/frontend/components/ui/ToastContainer/ToastBox.vue @@ -0,0 +1,63 @@ + + + + + + {{ message }} + + + + {{ description }} + + + + \ No newline at end of file diff --git a/frontend/components/ui/ToastContainer/index.vue b/frontend/components/ui/ToastContainer/index.vue new file mode 100644 index 0000000..266d557 --- /dev/null +++ b/frontend/components/ui/ToastContainer/index.vue @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/frontend/composables/api/auth/useOtp.ts b/frontend/composables/api/auth/useOtp.ts new file mode 100644 index 0000000..7909ccc --- /dev/null +++ b/frontend/composables/api/auth/useOtp.ts @@ -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(`${API_ENDPOINTS.account.send_otp}`, variables); + return data; +}; + +// composable + +const useOtp = () => { + return useMutation({ + mutationFn: (variables: OtpRequest) => handleOtp(variables) + }); +}; + +export default useOtp; diff --git a/frontend/composables/api/auth/useSignIn.ts b/frontend/composables/api/auth/useSignIn.ts new file mode 100644 index 0000000..3ace4f1 --- /dev/null +++ b/frontend/composables/api/auth/useSignIn.ts @@ -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(`${API_ENDPOINTS.auth.signin}/`, variables); + return data; +}; + +// composable + +const useSignIn = () => { + return useMutation({ + mutationFn: (variables: SignInRequest) => handleSignIn(variables) + }); +}; + +export default useSignIn; diff --git a/frontend/composables/useTimer.ts b/frontend/composables/useTimer.ts new file mode 100644 index 0000000..5f6b943 --- /dev/null +++ b/frontend/composables/useTimer.ts @@ -0,0 +1,42 @@ +type Props = { + duration: number; + callback?: () => void +} + +export function useTimer({ duration, callback }: Props) { + const timeout = ref(null); + const interval = ref(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 + }; +} \ No newline at end of file diff --git a/frontend/composables/useToast.ts b/frontend/composables/useToast.ts new file mode 100644 index 0000000..95bec80 --- /dev/null +++ b/frontend/composables/useToast.ts @@ -0,0 +1,32 @@ +type Toast = { + id: number; + message: string; + description?: string; + duration?: number; +} + +type Props = { + message: string, + description?: string, + options?: Omit +} + +const toasts = ref([]); + +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 + }; +} \ No newline at end of file diff --git a/frontend/constants/index.ts b/frontend/constants/index.ts index ca13d19..51d3b72 100644 --- a/frontend/constants/index.ts +++ b/frontend/constants/index.ts @@ -1,6 +1,9 @@ export const API_ENDPOINTS = { + account : { + send_otp : "/accounts/send_otp", + }, auth: { - login: "/token", + signin: "/token", logout: "/accounts/logout", } }; diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index d599fba..d863918 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -44,4 +44,10 @@ export default defineNuxtConfig({ "@nuxt/icon", "reka-ui/nuxt", ], + + runtimeConfig: { + public: { + API_BASE_URL: "http://38.60.202.91:8000", + }, + }, }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3780039..76a2eee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,8 @@ "@nuxtjs/google-fonts": "^3.2.0", "@tanstack/vue-query": "^5.62.2", "@tanstack/vue-query-devtools": "^5.62.3", + "@vuelidate/core": "^2.0.3", + "@vuelidate/validators": "^2.0.4", "axios": "^1.7.9", "gsap": "^3.12.5", "nuxt": "^3.14.1592", @@ -18,8 +20,7 @@ "swiper": "^11.1.15", "vue": "latest", "vue-router": "latest", - "vue-scrollto": "^2.20.0", - "vue-toastification": "^2.0.0-rc.5" + "vue-scrollto": "^2.20.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.0.0-beta.5", @@ -3309,6 +3310,94 @@ "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "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": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz", @@ -10451,15 +10540,6 @@ "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": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index c025667..f1154d8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,7 @@ "private": true, "type": "module", "scripts": { + "start": "node .output/server/index.mjs", "build": "nuxt build", "dev": "nuxt dev", "dev-o": "nuxt dev -- -o", @@ -15,6 +16,8 @@ "@nuxtjs/google-fonts": "^3.2.0", "@tanstack/vue-query": "^5.62.2", "@tanstack/vue-query-devtools": "^5.62.3", + "@vuelidate/core": "^2.0.3", + "@vuelidate/validators": "^2.0.4", "axios": "^1.7.9", "gsap": "^3.12.5", "nuxt": "^3.14.1592", @@ -22,8 +25,7 @@ "swiper": "^11.1.15", "vue": "latest", "vue-router": "latest", - "vue-scrollto": "^2.20.0", - "vue-toastification": "^2.0.0-rc.5" + "vue-scrollto": "^2.20.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.0.0-beta.5", diff --git a/frontend/pages/signin/index.vue b/frontend/pages/signin/index.vue new file mode 100644 index 0000000..73d33f8 --- /dev/null +++ b/frontend/pages/signin/index.vue @@ -0,0 +1,196 @@ + + + + + + فرم ورود + + + + + + + +98 + + + + + + + + + + + + ارسال کد + + + + + تغییر شماره + + + ارسال مجدد کد + {{ isResendOtpBlocked ? otpBlockerTimePassed : "" }} + + + + + + بازگشت به فروشگاه + + + + + + \ No newline at end of file diff --git a/frontend/plugins/toast.client.ts b/frontend/plugins/toast.client.ts deleted file mode 100644 index 9ddfc86..0000000 --- a/frontend/plugins/toast.client.ts +++ /dev/null @@ -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(), - }, - }; -});