diff --git a/app.vue b/app.vue index 9343435..ec8256b 100644 --- a/app.vue +++ b/app.vue @@ -2,3 +2,12 @@ + \ No newline at end of file diff --git a/components/auth/auth.vue b/components/auth/auth.vue index 84ccc5a..d1aea75 100644 --- a/components/auth/auth.vue +++ b/components/auth/auth.vue @@ -12,7 +12,7 @@
- +

Please Wait...

diff --git a/components/auth/forgot-password.vue b/components/auth/forgot-password.vue index 55c6a82..0f52ba6 100644 --- a/components/auth/forgot-password.vue +++ b/components/auth/forgot-password.vue @@ -3,69 +3,67 @@

Enter your email to receive password reset instructions

- +
- - + + - + Submit
-
+

Email Sent

- We've sent you an email with a link to reset your password. - Please check your inbox and follow the instructions provided. + {{ data?.message || `We've sent you an email with a link to reset your password. + Please check your inbox and follow the instructions provided.`}}

- -
- true - false -
+ + +
+ +
+

{{ data?.message || 'Email not activated' }}

+

+ Your email has not been activated. Please check your inbox or spam folder for + the activation + email. +

+
+
+ +
+
\ No newline at end of file diff --git a/components/auth/login.vue b/components/auth/login.vue index 2c3a4a3..4e98918 100644 --- a/components/auth/login.vue +++ b/components/auth/login.vue @@ -1,8 +1,6 @@ - + - Submit diff --git a/components/auth/modal-email-activation-sent.vue b/components/auth/modal-email-activation-sent.vue new file mode 100644 index 0000000..6e52a96 --- /dev/null +++ b/components/auth/modal-email-activation-sent.vue @@ -0,0 +1,26 @@ + + \ No newline at end of file diff --git a/components/auth/register.vue b/components/auth/register.vue index 38a5d44..3d6cbe4 100644 --- a/components/auth/register.vue +++ b/components/auth/register.vue @@ -3,68 +3,63 @@ Create your account to get started with predictive financial insights

- - - + + + - + - + Submit + + + {{ registerData?.message || 'User registered. Please check your email to verify your account.' + }} + + diff --git a/components/landing/demo/modalMakePrediction.vue b/components/landing/demo/modalMakePrediction.vue index 909f82c..b336c61 100644 --- a/components/landing/demo/modalMakePrediction.vue +++ b/components/landing/demo/modalMakePrediction.vue @@ -1,41 +1,65 @@ \ No newline at end of file diff --git a/components/overlay/loading/clipboard.vue b/components/loader/clipboard.vue similarity index 100% rename from components/overlay/loading/clipboard.vue rename to components/loader/clipboard.vue diff --git a/components/overlay/loading/flipping-book-page.vue b/components/loader/flipping-book-page.vue similarity index 100% rename from components/overlay/loading/flipping-book-page.vue rename to components/loader/flipping-book-page.vue diff --git a/components/overlay/loading/flipping-card.vue b/components/loader/flipping-card.vue similarity index 100% rename from components/overlay/loading/flipping-card.vue rename to components/loader/flipping-card.vue diff --git a/components/overlay/loading/padlock.vue b/components/loader/padlock.vue similarity index 100% rename from components/overlay/loading/padlock.vue rename to components/loader/padlock.vue diff --git a/components/overlay/loading/pulse-ring.vue b/components/loader/pulse-ring.vue similarity index 100% rename from components/overlay/loading/pulse-ring.vue rename to components/loader/pulse-ring.vue diff --git a/components/overlay/loading/text.vue b/components/loader/text.vue similarity index 100% rename from components/overlay/loading/text.vue rename to components/loader/text.vue diff --git a/components/overlay/loading/trailing-letter.vue b/components/loader/trailing-letter.vue similarity index 100% rename from components/overlay/loading/trailing-letter.vue rename to components/loader/trailing-letter.vue diff --git a/components/my/form/input-with-type.vue b/components/my/form/input-with-type.vue index dc647dc..cea3b36 100644 --- a/components/my/form/input-with-type.vue +++ b/components/my/form/input-with-type.vue @@ -5,14 +5,14 @@ option-attribute="label" value-attribute="value" /> - + - +
- + @@ -27,6 +27,8 @@ type TValueFormatOptions = { label: string, } +const model = defineModel() + const props = defineProps<{ name: string }>() @@ -38,6 +40,8 @@ const valueFormatOptions: TValueFormatOptions[] = [ { value: 'number', label: 'Number' }, ] +const localValue = ref() + onMounted(() => { if (props.name.includes('date')) { valueFormat.value = 'date' @@ -47,4 +51,12 @@ onMounted(() => { valueFormat.value = 'text' } }); + +onMounted(() => { + localValue.value = model.value +}) + +onUnmounted(() => { + model.value = localValue.value +}) \ No newline at end of file diff --git a/components/my/modal/basic-confirmation.vue b/components/my/modal/basic-confirmation.vue new file mode 100644 index 0000000..f35b364 --- /dev/null +++ b/components/my/modal/basic-confirmation.vue @@ -0,0 +1,39 @@ + + + + diff --git a/components/my/modal/prediction-tutorial.vue b/components/my/modal/prediction-tutorial.vue new file mode 100644 index 0000000..af01778 --- /dev/null +++ b/components/my/modal/prediction-tutorial.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/components/my/modal/update-prediction-row.vue b/components/my/modal/update-prediction-row.vue index 1f35e87..86b16e7 100644 --- a/components/my/modal/update-prediction-row.vue +++ b/components/my/modal/update-prediction-row.vue @@ -1,4 +1,24 @@ \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/components/my/modal/view-prediction-row.vue b/components/my/modal/view-prediction-row.vue new file mode 100644 index 0000000..9554c14 --- /dev/null +++ b/components/my/modal/view-prediction-row.vue @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/composables/core.ts b/composables/core.ts new file mode 100644 index 0000000..aebfd33 --- /dev/null +++ b/composables/core.ts @@ -0,0 +1,13 @@ +import { useLocalStorage } from "@vueuse/core"; + +export function useMyAppState() { + return { + isOffline: useState('myAppState-isOffline', () => false), + authState: useCookie<'logged-in' | 'logged-out' | 'uncheck'>('myAppState-authState', { + default: () => 'uncheck', + }), + apiAccessToken: useCookie("myAppState-accessToken", { + default: () => null, + }), + } +} \ No newline at end of file diff --git a/composables/useAuth$fetch.ts b/composables/useAuth$fetch.ts new file mode 100644 index 0000000..6c09ba8 --- /dev/null +++ b/composables/useAuth$fetch.ts @@ -0,0 +1,115 @@ +import { useLocalStorage } from '@vueuse/core'; +import type { AsyncDataRequestStatus } from 'nuxt/app'; +import type { TAPIResponse } from '~/types/api-response/basicResponse'; +import type { NitroFetchOptions, NitroFetchRequest } from 'nitropack'; + +export function use$fetchWithAutoReNew( + url: string, + options?: NitroFetchOptions +) { + const config = useRuntimeConfig(); + const { authState, apiAccessToken } = useMyAppState(); + + const headers = computed(() => { + let h = {} + if (!!options?.headers) { + if (Array.isArray(options.headers)) { + Object.assign(h, Object.fromEntries(options.headers)) + } else if (typeof options.headers === 'object') { + Object.assign(h, options.headers) + } + } + Object.assign(h, { + Authorization: apiAccessToken.value ? `Bearer ${apiAccessToken.value}` : '', + Accept: 'application/json', + }) + return h + }) + + const data = ref(null); + const status = ref('idle'); + const error = ref(null); + + async function refreshAccessToken(): Promise { + try { + let newAccessToken = null + const res = await $fetch>('/auth/refresh-token', { + baseURL: config.public.API_HOST, + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json', + }, + onResponse: async (ctx) => { + if (ctx.response.ok) { + newAccessToken = ctx.response._data.data.accessToken; + } + }, + onResponseError: async (ctx) => { + const statusCode = ctx.response?.status; + + if ([401, 403].includes(statusCode)) { + authState.value = 'logged-out' + apiAccessToken.value = null; + } + }, + }); + + if (!!newAccessToken) { + apiAccessToken.value = newAccessToken; + return true; + } + + throw new Error('No accessToken received'); + } catch (e) { + console.error('🔄 Failed to refresh token', e); + return false; + } + } + + async function execute() { + status.value = 'pending'; + error.value = null; + + try { + await $fetch(url, { + ...options, + headers: headers.value, + baseURL: config.public.API_HOST, + // credentials: 'include', + onResponse: async (ctx) => { + data.value = ctx.response._data; + if (ctx.response.ok) { + if (typeof options?.onResponse === 'function') { + await options.onResponse(ctx); + } + status.value = 'success'; + } + }, + onResponseError: async (ctx) => { + error.value = ctx?.error ?? ctx.response._data ?? null; + const statusCode = ctx.response?.status; + + if ([401, 403].includes(statusCode)) { + const refreshed = await refreshAccessToken(); + if (refreshed) { + await execute(); + return; + } + } + + if (typeof options?.onResponseError === 'function') { + await options?.onResponseError?.(ctx); + } + status.value = 'error'; + }, + }); + } catch (err) { + error.value = err as ErrorData; + status.value = 'error'; + console.error('❌ Fetch failed:', err); + } + } + + return { data, status, error, execute }; +} diff --git a/composables/useAuth.ts b/composables/useAuth.ts new file mode 100644 index 0000000..cd892af --- /dev/null +++ b/composables/useAuth.ts @@ -0,0 +1,180 @@ +import { z } from "zod" +import type { TAPIResponse } from "~/types/api-response/basicResponse" + +export function useAuthLogin() { + const { authState, apiAccessToken } = useMyAppState() + + type Schema = z.output + + const loginForm = reactive({ + email: '', + password: '' + }) + + const schema = z.object({ + email: z.string().email('Invalid email'), + password: z.string().min(8, 'Must be at least 8 characters') + }) + + const { execute: loginNow, status: loginStatus } = use$fetchWithAutoReNew>('/auth/login', { + body: loginForm, + method: 'post', + credentials: 'include', + onResponse(ctx) { + authState.value = 'logged-in' + apiAccessToken.value = ctx.response._data.data.accessToken + navigateTo('/dashboard') + } + }) + + return { + loginForm, schema, + loginNow, loginStatus + } +} + +export function useAuthLogout() { + const { authState, apiAccessToken } = useMyAppState() + + const { execute: logoutNow, status: logoutStatus } = use$fetchWithAutoReNew('/auth/logout', { + method: 'get', + credentials: 'include', + onResponse(ctx) { + authState.value = 'logged-out' + apiAccessToken.value = null + navigateTo('/auth') + } + }) + + return { + logoutNow, logoutStatus + } +} + +export function useAuthRegister() { + const config = useRuntimeConfig() + const baseSchema = z.object({ + email: z.string().email('Invalid email'), + password: z.string() + .min(8, 'Must be at least 8 characters') + .regex(/[A-Z]/, 'Must contain at least one uppercase letter') + .regex(/[a-z]/, 'Must contain at least one lowercase letter') + .regex(/[0-9]/, 'Must contain at least one number') + .regex(/[^A-Za-z0-9]/, 'Must contain at least one special character'), + password_confirmation: z.string().min(8, 'Must be at least 8 characters'), + emailActivationPage: z.optional(z.string()), + }) + + const schema = baseSchema + .refine((data) => data.password === data.password_confirmation, { + message: "Passwords don't match", + path: ['password_confirmation'], + }) + + type Schema = z.output + + const registerForm = reactive({ + email: '', + password: '', + password_confirmation: '', + emailActivationPage: `${config.public.HOST}/auth/verify` + }) + + const isEmailAlreadyRegistered = ref(false) + + const { + execute: registerNow, + status: registerStatus, + error: registerError, + data: registerData + } = use$fetchWithAutoReNew>('/auth/register', { + method: 'post', + body: registerForm, + onResponseError(ctx) { + if (ctx.response._data.message === 'Email already exists') { + isEmailAlreadyRegistered.value = true + } + } + }) + + return { registerForm, schema, registerNow, registerStatus, registerData, registerError, isEmailAlreadyRegistered } +} + +export function useAuthForgotPasswordGetToken() { + const config = useRuntimeConfig() + const schema = z.object({ + email: z.string().email('Invalid email'), + emailActivationPage: z.optional(z.string()), + }) + + type Schema = z.output + + const forgotPasswordForm = reactive({ + email: '', + emailActivationPage: `${config.public.HOST}/auth/forgot-password` + }) + + const isEmailNotActivated = ref(false) + + const { + execute: submitNow, + status: submitStatus, + error: submitError, + data: data + } = use$fetchWithAutoReNew>('/auth/forgot-password', { + method: 'post', + body: forgotPasswordForm, + onResponseError(ctx) { + if (ctx.response.status === 403) { + isEmailNotActivated.value = true + } + } + }) + + return { forgotPasswordForm, schema, submitNow, submitStatus, data, submitError, isEmailNotActivated } +} + +export function useAuthForgotPasswordChangePassword() { + const route = useRoute(); + const baseSchema = z.object({ + password: z.string() + .min(8, 'Must be at least 8 characters') + .regex(/[A-Z]/, 'Must contain at least one uppercase letter') + .regex(/[a-z]/, 'Must contain at least one lowercase letter') + .regex(/[0-9]/, 'Must contain at least one number') + .regex(/[^A-Za-z0-9]/, 'Must contain at least one special character'), + password_confirmation: z.string().min(8, 'Must be at least 8 characters'), + }) + + const schema = baseSchema + .refine((data) => data.password === data.password_confirmation, { + message: "Passwords don't match", + path: ['password_confirmation'], + }) + + type Schema = z.output + + const forgotPasswordForm = reactive({ + password: '', + password_confirmation: '' + }) + + const isTokenExpired = ref(false) + + const { + execute: submitNow, + status: submitStatus, + error: submitError, + data: data + } = use$fetchWithAutoReNew>(`/auth/forgot-password/${route.params.token}`, { + method: 'patch', + body: forgotPasswordForm, + onResponseError(ctx) { + isTokenExpired.value = true + } + }) + + return { forgotPasswordForm, schema, submitNow, submitStatus, data, submitError, isTokenExpired } +} \ No newline at end of file diff --git a/composables/useAuthFetch.ts b/composables/useAuthFetch.ts new file mode 100644 index 0000000..12597a8 --- /dev/null +++ b/composables/useAuthFetch.ts @@ -0,0 +1,96 @@ +import { useLocalStorage, useNetwork } from '@vueuse/core'; +import type { UseFetchOptions } from 'nuxt/app'; +import type { TAPIResponse } from '~/types/api-response/basicResponse'; + +export function useFetchWithAutoReNew( + url: string | Request | Ref | (() => string | Request), + options?: UseFetchOptions +) { + const config = useRuntimeConfig(); + const { authState, apiAccessToken } = useMyAppState(); + + const originalHeadersAsObject = () => { + if (options?.headers) { + if (Array.isArray(options.headers)) { + return Object.fromEntries(options.headers as any[][]); + } else { + return options.headers; + } + } + } + + const headers = computed(() => { + return { + ...originalHeadersAsObject, + Authorization: `Bearer ${apiAccessToken.value}`, + Accept: 'application/json', + }; + }); + + const mergedOptions: UseFetchOptions = { + ...options, + headers, + baseURL: config.public.API_HOST, + async onResponse(ctx) { + if (ctx.response.ok) { + if (typeof options?.onResponse === "function") { + options.onResponse(ctx); + } + } + }, + async onResponseError(ctx) { + const status = ctx.response.status; + if ([401, 403].includes(status)) { + await refreshAccessToken() + } + if (typeof options?.onResponseError === "function") { + options.onResponseError(ctx); + } + }, + }; + + const { data, status, error, refresh, clear } = useFetch(url, mergedOptions) + + async function refreshAccessToken(): Promise { + try { + let newAccessToken = null + await $fetch>(`/auth/refresh-token`, { + baseURL: config.public.API_HOST, + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json' + }, + onResponse: async (ctx) => { + if (ctx.response.ok) { + newAccessToken = ctx.response._data.data.accessToken; + } + }, + onResponseError: async (ctx) => { + error.value = ctx?.error ?? ctx.response._data ?? null; + const statusCode = ctx.response?.status; + + if ([401, 403].includes(statusCode)) { + authState.value = 'logged-out' + apiAccessToken.value = null; + } + }, + }); + if (!!newAccessToken) { + apiAccessToken.value = newAccessToken; + return true; + } + + throw new Error('No accessToken received'); + } catch (e) { + console.error("🔄 Failed to refresh token:", e); + return false; + } finally { + + } + } + + return { data, status, error, refresh, clear } +} \ No newline at end of file diff --git a/composables/usePredictionTable.ts b/composables/usePredictionTable.ts index 8b24a7a..f8e6dfc 100644 --- a/composables/usePredictionTable.ts +++ b/composables/usePredictionTable.ts @@ -1,38 +1,150 @@ +import dayjs from '#build/dayjs.imports.mjs' import type { TableColumn } from '#ui/types' -export function usePredictionTableColumn() { - const requiredColumn = ['date', 'product code', 'product name', 'sold(qty)'] - const columnKeys = ref([]) - const columns: ComputedRef = computed(() => { - if (columnKeys.value.length >= 1) { - return [{ - key: 'actions', - sortable: true, - label: 'Actions' - }, ...columnKeys.value.map(v => ({ - key: v, - sortable: true, - label: v - }) as TableColumn)] - } else { - return [{ - key: 'actions', - sortable: true, - label: 'Actions' - }, ...requiredColumn.map(v => ({ - key: v, - sortable: true, - label: v, - }))] - } +import type { TProduct } from '~/types/landing-page/demo/modalMakePrediction' + +const requiredColumn = [ + { label: 'Date', key: 'date', sortable: true, }, + { label: 'Product Code', key: 'product code', sortable: true, }, + { label: 'Product Name', key: 'product name', sortable: true, }, + { label: 'Sold(qty)', key: 'sold(qty)', sortable: true, } +] + +export function usePredictionTable() { + const records = ref[]>([]) + const processedRecords = ref[]>([]) + const status = ref<'idle' | 'loading' | 'loaded'>('idle') + const loadingDetail = ref(); + const mismatchDetail = ref([]) + + const columns = ref([]) + const missingColumns = ref([]) + + const page = ref(1) + const pageCount = ref(10) + + const products = ref[]>([]) + + const rows = computed(() => { + return processedRecords.value.slice((page.value - 1) * pageCount.value, (page.value) * pageCount.value) }) - const missingColumn = computed(() => { - const currentColumn = columns.value.map(v => v.key) - return requiredColumn.filter(v => !currentColumn.includes(v)) + + watch(records, newVal => { + status.value = 'loading' + mismatchDetail.value = [] + + loadingDetail.value = 'Collecting and validating record' + processedRecords.value = normalizeRecord(newVal, requiredColumn.map(v => v.key)) + }) + + watch(processedRecords, newVal => { + console.log('processed') + loadingDetail.value = 'Load all column keys from record' + columns.value = getColumnsFromRecord(newVal[0], requiredColumn) + + loadingDetail.value = 'Checking missing column' + missingColumns.value = getMissingColumns( + Object.keys(newVal[0]), + requiredColumn.map(v => v.key) + ) + + loadingDetail.value = 'Collecting product list' + const hasProductCode = Object.keys(newVal[0]).includes('product code') + const listedProduct = new Set() + let dateInvalidCounter: number = 0 + newVal.forEach((v, i) => { + if (hasProductCode) { + const cv = v['product code'] + const cl = v['product name'] + if (!listedProduct.has(cv)) { + listedProduct.add(cv) + products.value?.push({ + label: cv, + value: cl + }) + } + } else { + const cl = v['product name'] + if (!listedProduct.has(cl)) { + listedProduct.add(cl) + products.value?.push({ + label: cl, + value: cl + }) + } + } + + if (!dayjs(v.date).isValid()) { + dateInvalidCounter += 1 + } + }) + + if (dateInvalidCounter >= 1) { + mismatchDetail.value.push(`There is ${dateInvalidCounter} invalid date inside column 'date'.`) + } + + loadingDetail.value = undefined + status.value = 'loaded' }) return { - columnKeys, - columns, - missingColumn + records, processedRecords, status, loadingDetail, mismatchDetail, + columns, missingColumns, + page, pageCount, + products, rows } +} + +function getProductList(records: Record[]) { + return +} + +function normalizeRecord(records: Record[], requiredColumnKey: string[]) { + return records.map(record => { + const normalized: Record = {} + + Object.entries(record).forEach(([key, value]) => { + const lowerKey = key.toLowerCase() + if (requiredColumnKey.includes(lowerKey)) { + normalized[lowerKey] = value + } else { + normalized[key] = value + } + }) + + return normalized + }) +} + +function getColumnsFromRecord( + records: Record = {}, + requiredColumn: TableColumn[] +) { + const columnKeys: string[] | Record[] = Object.keys(records || {}) + const requiredColumnKey = requiredColumn.map(v => v.key) + const finalColumn = [ + // { + // key: 'actions', + // sortable: true, + // label: 'Actions' + // }, + ...requiredColumn, + ] + if (columnKeys.length >= 1) { + const candidateCol = columnKeys.map(v => ({ + key: v, + label: v, + sortable: true, + })) + finalColumn.push( + ...candidateCol.filter(v => !requiredColumnKey.includes(v.key)) + ) + } + return finalColumn +} + +function getMissingColumns( + columnsKey: string[], + requiredColumnKey: string[] +) { + return requiredColumnKey.filter(v => !columnsKey.includes(v)) } \ No newline at end of file diff --git a/composables/useSpreadsheet.ts b/composables/useSpreadsheet.ts index 5a43b49..1fca8fd 100644 --- a/composables/useSpreadsheet.ts +++ b/composables/useSpreadsheet.ts @@ -14,6 +14,30 @@ export function useFileToJSON() { try { const json = await sheetToJSON(newVal); if (json) { + const newJSON = json.map((jsonObj) => { + const entries = Object.entries( + jsonObj as Record + ).map(([key, value]) => { + switch (key.toLowerCase().trim()) { + case 'date': + key = 'date' + break; + case 'product code': + key = 'product code' + break; + case 'product name': + key = 'product name' + break; + case 'sold(qty)': + key = 'sold(qty)' + break; + default: + break; + } + return [key, value]; + }); + return Object.fromEntries(entries); + }) result.value = json as Record[] } } catch (e: unknown) { diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..e90f51a --- /dev/null +++ b/env.d.ts @@ -0,0 +1,6 @@ +declare namespace NodeJS { + interface ProcessEnv { + HOST: string + API_HOST: string + } +} \ No newline at end of file diff --git a/middleware/account-activation.ts b/middleware/account-activation.ts new file mode 100644 index 0000000..b06bce3 --- /dev/null +++ b/middleware/account-activation.ts @@ -0,0 +1,17 @@ +export default defineNuxtRouteMiddleware(async (to, from) => { + const route = useRoute(); + const activationToken = route.params.token + const emailVerification = useState<'activated' | 'invalid-token' | 'unset'>('email-verification-state', () => 'unset') + + if (!!activationToken) { + const { execute } = use$fetchWithAutoReNew(`/auth/verify/${activationToken}`, { + onResponse(ctx) { + emailVerification.value = 'activated' + }, + onResponseError(ctx) { + emailVerification.value = 'invalid-token' + } + }) + await execute() + } +}); diff --git a/middleware/authentication.ts b/middleware/authentication.ts new file mode 100644 index 0000000..98e125d --- /dev/null +++ b/middleware/authentication.ts @@ -0,0 +1,15 @@ +export default defineNuxtRouteMiddleware(async (to, from) => { + const { apiAccessToken } = useMyAppState(); + + if (!apiAccessToken.value) { + let gotoAuth = false + const { execute } = use$fetchWithAutoReNew('/auth/refresh-token', { + onResponseError() { + gotoAuth = true + } + }) + await execute() + if (gotoAuth) + return navigateTo('/auth') + } +}); diff --git a/middleware/forgot-password-confirmation.ts b/middleware/forgot-password-confirmation.ts index ada9576..fe0f3dc 100644 --- a/middleware/forgot-password-confirmation.ts +++ b/middleware/forgot-password-confirmation.ts @@ -1,42 +1,17 @@ export default defineNuxtRouteMiddleware(async (to, from) => { - const token = to.params.token; - const forgotPasswordTokenStatus = useState<'unset' | 'invalid' | 'valid'>('forgot-password-token-status', () => 'unset') + const route = useRoute(); + const forgotPasswordToken = route.params.token + const forgotPasswordState = useState<'valid' | 'invalid' | 'unset'>('forgot-password-state', () => 'unset') - if (!token) { - return navigateTo('/login'); - } - - try { - const isValid = await verifyToken(token as string); - - if (!isValid) { - forgotPasswordTokenStatus.value = 'invalid' - } else { - forgotPasswordTokenStatus.value = 'valid' - } - } catch (error) { - forgotPasswordTokenStatus.value = 'invalid' - } - - async function verifyToken(token: string) { - return new Promise((resolve) => setTimeout(() => { - if (token === 'success') { - return resolve(true) - } else { - return resolve(false) + if (!!forgotPasswordToken) { + const { execute } = use$fetchWithAutoReNew(`/auth/forgot-password/${forgotPasswordToken}`, { + onResponse(ctx) { + forgotPasswordState.value = 'valid' + }, + onResponseError(ctx) { + forgotPasswordState.value = 'invalid' } - }, 2000)) - // const response = await fetch('https://your-backend.com/api/verify-token', { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json', - // }, - // body: JSON.stringify({ token }), - // }); - - // const data = await response.json(); - - // return data.isValid; + }) + await execute() } - }); diff --git a/middleware/guest.ts b/middleware/guest.ts new file mode 100644 index 0000000..ed2cbdb --- /dev/null +++ b/middleware/guest.ts @@ -0,0 +1,15 @@ +export default defineNuxtRouteMiddleware(async (to, from) => { + const { authState } = useMyAppState(); + + if (authState.value === 'logged-in') { + let gotoDashboard = false + const { execute } = use$fetchWithAutoReNew('/is-authenticated', { + onResponse() { + gotoDashboard = true + } + }) + await execute() + if (gotoDashboard) + return navigateTo('/dashboard') + } +}); diff --git a/nuxt.config.ts b/nuxt.config.ts index 70ceaaa..0c79c8d 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -16,6 +16,12 @@ export default defineNuxtConfig({ ui: { prefix: 'NuxtUi' }, + runtimeConfig: { + public: { + HOST: process.env.HOST, + API_HOST: process.env.API_HOST + } + }, shadcn: { /** * Prefix for all the imported component diff --git a/package-lock.json b/package-lock.json index 028b9b0..10c4aa9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,9 @@ "vue-router": "^4.5.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^22.15.12" } }, "node_modules/@alloc/quick-lru": { @@ -2975,6 +2978,16 @@ "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.15.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.12.tgz", + "integrity": "sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/parse-path": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", @@ -10570,6 +10583,13 @@ "unplugin": "^2.1.0" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/unenv": { "version": "2.0.0-rc.15", "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.15.tgz", diff --git a/package.json b/package.json index a7e0b0c..f118617 100644 --- a/package.json +++ b/package.json @@ -28,5 +28,8 @@ "vue-router": "^4.5.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^22.15.12" } } diff --git a/pages/auth/activate-email.vue b/pages/auth/activate-email.vue deleted file mode 100644 index e919578..0000000 --- a/pages/auth/activate-email.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/pages/auth/forgot-password/[token].vue b/pages/auth/forgot-password/[token].vue index ba42135..2a6f13b 100644 --- a/pages/auth/forgot-password/[token].vue +++ b/pages/auth/forgot-password/[token].vue @@ -4,86 +4,113 @@ import type { FormSubmitEvent } from '#ui/types' definePageMeta({ middleware: ['forgot-password-confirmation'], }); -const forgotPasswordTokenStatus = useState<'unset' | 'invalid' | 'valid'>('forgot-password-token-status', () => 'unset') + +const forgotPasswordState = useState<'valid' | 'invalid' | 'unset'>('forgot-password-state', () => 'unset') + const authSection = useState<'login' | 'register' | 'forgot-password'>('auth-section', () => 'login') -const isSubmitting = ref(false) - -const baseSchema = z.object({ - newPassword: z.string().min(8, 'Must be at least 8 characters'), - newPassword2: z.string().min(8, 'Must be at least 8 characters') -}) - -const schema = baseSchema - .refine((data) => data.newPassword === data.newPassword2, { - message: "Passwords don't match", - path: ['newPassword2'], - }) - -type Schema = z.output - -const state = reactive({ - newPassword: undefined, - newPassword2: undefined -}) - -async function onSubmit(event: FormSubmitEvent) { - isSubmitting.value = true - setTimeout(() => { - isSubmitting.value = false - }, 3000); -} +const { + forgotPasswordForm, schema, + submitNow, submitStatus, + isTokenExpired +} = useAuthForgotPasswordChangePassword() \ No newline at end of file diff --git a/pages/auth/verify/[token].vue b/pages/auth/verify/[token].vue new file mode 100644 index 0000000..8a3a40e --- /dev/null +++ b/pages/auth/verify/[token].vue @@ -0,0 +1,160 @@ + + + \ No newline at end of file diff --git a/pages/dashboard/index.vue b/pages/dashboard/index.vue index a4fa340..6ae1beb 100644 --- a/pages/dashboard/index.vue +++ b/pages/dashboard/index.vue @@ -1,5 +1,9 @@ + \ No newline at end of file diff --git a/pages/demo.vue b/pages/demo.vue index 4bbf4e4..c7181f3 100644 --- a/pages/demo.vue +++ b/pages/demo.vue @@ -5,72 +5,132 @@ handleDragFile(e) } }"> - - diff --git a/pages/index.vue b/pages/index.vue index 5bbc361..9f627a7 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -5,4 +5,4 @@ - + \ No newline at end of file diff --git a/types/api-response/basicResponse.ts b/types/api-response/basicResponse.ts new file mode 100644 index 0000000..c13b7b7 --- /dev/null +++ b/types/api-response/basicResponse.ts @@ -0,0 +1,15 @@ +export type TAPIResponse> = { + success: boolean; + message?: string; + error?: Record, + data?: T; +}; + +export type TPaginatedResponse> = TAPIResponse & { + pagination?: { + total: number; + page: number; + pageSize: number; + totalPages: number; + } +}; \ No newline at end of file diff --git a/types/landing-page/demo/modalMakePrediction.ts b/types/landing-page/demo/modalMakePrediction.ts index 6710c44..7bca600 100644 --- a/types/landing-page/demo/modalMakePrediction.ts +++ b/types/landing-page/demo/modalMakePrediction.ts @@ -1,13 +1,18 @@ +export type TPredictionMode = 'auto' | 'optimal' | 'custom' export type TDurationType = 'daily' | 'weekly' | 'monthly' export type TModalMakePredictionModel = { recordPeriod?: TDurationType, selectedProduct?: string, predictionPeriod?: TDurationType, + predictionMode?: TPredictionMode, + arimaModel?: number[] } export type TProduct = { product_code: string, product_name: string } export type TModalMakePredictionProps = { - products?: TProduct[] + products?: TProduct[], + disabled?: boolean, + [key: string]: any; } \ No newline at end of file