Add:api-login
This commit is contained in:
parent
a6cddc6318
commit
c411f2a8a4
20 changed files with 434 additions and 185 deletions
|
|
@ -648,7 +648,9 @@ const _inlineRuntimeConfig = {
|
|||
}
|
||||
}
|
||||
},
|
||||
"public": {}
|
||||
"public": {
|
||||
"apiBase": "http://192.168.1.137:4000/api"
|
||||
}
|
||||
};
|
||||
const envOptions = {
|
||||
prefix: "NITRO_",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
{"id":"dev","timestamp":1768363426076}
|
||||
{"id":"dev","timestamp":1768378394619}
|
||||
|
|
@ -1 +1 @@
|
|||
{"id":"dev","timestamp":1768363426076,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}
|
||||
{"id":"dev","timestamp":1768378394619,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"date": "2026-01-14T04:03:52.160Z",
|
||||
"date": "2026-01-14T08:13:20.972Z",
|
||||
"preset": "nitro-dev",
|
||||
"framework": {
|
||||
"name": "nuxt",
|
||||
|
|
@ -9,9 +9,9 @@
|
|||
"nitro": "2.12.8"
|
||||
},
|
||||
"dev": {
|
||||
"pid": 23376,
|
||||
"pid": 17488,
|
||||
"workerAddress": {
|
||||
"socketPath": "\\\\.\\pipe\\nitro-worker-23376-1-1-2860.sock"
|
||||
"socketPath": "\\\\.\\pipe\\nitro-worker-17488-1-1-6654.sock"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Frontend-Learner/.nuxt/nuxt.d.ts
vendored
2
Frontend-Learner/.nuxt/nuxt.d.ts
vendored
|
|
@ -1,8 +1,8 @@
|
|||
/// <reference types="quasar" />
|
||||
/// <reference types="nuxt-quasar-ui" />
|
||||
/// <reference types="@nuxtjs/tailwindcss" />
|
||||
/// <reference types="@nuxt/telemetry" />
|
||||
/// <reference types="@nuxt/devtools" />
|
||||
/// <reference types="@nuxt/telemetry" />
|
||||
/// <reference path="types/builder-env.d.ts" />
|
||||
/// <reference types="nuxt" />
|
||||
/// <reference path="types/app-defaults.d.ts" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 14/1/2569 11:33:56
|
||||
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 14/1/2569 15:13:15
|
||||
import "@nuxtjs/tailwindcss/config-ctx"
|
||||
import configMerger from "@nuxtjs/tailwindcss/merger";
|
||||
|
||||
|
|
|
|||
2
Frontend-Learner/.nuxt/types/schema.d.ts
vendored
2
Frontend-Learner/.nuxt/types/schema.d.ts
vendored
|
|
@ -112,7 +112,7 @@ declare module 'nuxt/schema' {
|
|||
},
|
||||
}
|
||||
interface PublicRuntimeConfig {
|
||||
|
||||
apiBase: string,
|
||||
}
|
||||
}
|
||||
declare module 'vue' {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
<script setup>
|
||||
const { fetchUserProfile, isAuthenticated } = useAuth()
|
||||
|
||||
onMounted(() => {
|
||||
if (isAuthenticated.value) {
|
||||
fetchUserProfile()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GlobalLoader />
|
||||
<NuxtLayout>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,239 @@
|
|||
// Shared global state for current user
|
||||
const currentUser = ref({
|
||||
prefix: 'นาย',
|
||||
firstName: 'สมชาย',
|
||||
lastName: 'ใจดี',
|
||||
email: 'student@example.com',
|
||||
photoURL: '' // Set to URL if available
|
||||
})
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
|
||||
|
||||
// Types based on API responses
|
||||
interface User {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
role: {
|
||||
code: string
|
||||
name: { th: string; en: string }
|
||||
}
|
||||
profile?: {
|
||||
prefix: { th: string; en: string }
|
||||
first_name: string
|
||||
last_name: string
|
||||
phone: string | null
|
||||
avatar_url: string | null
|
||||
}
|
||||
}
|
||||
|
||||
interface loginResponse {
|
||||
token: string
|
||||
refreshToken: string
|
||||
user: User
|
||||
profile: User['profile']
|
||||
}
|
||||
|
||||
interface RegisterPayload {
|
||||
username: string
|
||||
email: string
|
||||
password: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
prefix: { th: string; en: string }
|
||||
phone: string
|
||||
}
|
||||
|
||||
export const useAuth = () => {
|
||||
const config = useRuntimeConfig()
|
||||
const API_BASE_URL = config.public.apiBase as string
|
||||
const token = useCookie('auth_token', {
|
||||
maxAge: 60 * 60 * 24, // 1 day
|
||||
sameSite: 'lax',
|
||||
secure: false // Set to true in production with HTTPS
|
||||
})
|
||||
|
||||
const user = useCookie<User | null>('auth_user_data', {
|
||||
maxAge: 60 * 60 * 24 * 7, // 1 week
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV === 'production'
|
||||
secure: false
|
||||
})
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value)
|
||||
|
||||
const login = (mockToken: string = 'demo-token') => {
|
||||
token.value = mockToken
|
||||
// Refresh Token Logic
|
||||
const refreshToken = useCookie('auth_refresh_token', {
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days (matching API likely)
|
||||
sameSite: 'lax',
|
||||
secure: false
|
||||
})
|
||||
|
||||
// ... (previous code)
|
||||
|
||||
// Login
|
||||
const login = async (credentials: { email: string; password: string }) => {
|
||||
try {
|
||||
const { data, error } = await useFetch<loginResponse>(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
body: credentials
|
||||
})
|
||||
|
||||
if (error.value) {
|
||||
throw error.value
|
||||
}
|
||||
|
||||
if (data.value) {
|
||||
token.value = data.value.token
|
||||
refreshToken.value = data.value.refreshToken // Save refresh token
|
||||
|
||||
// The API returns the profile nested inside the user object
|
||||
user.value = data.value.user
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
return { success: false, error: 'No data returned' }
|
||||
} catch (err: any) {
|
||||
console.error('Login failed:', err)
|
||||
return {
|
||||
success: false,
|
||||
error: err.data?.message || err.message || 'เข้าสู่ระบบไม่สำเร็จ'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register
|
||||
const register = async (payload: RegisterPayload) => {
|
||||
try {
|
||||
const { data, error } = await useFetch(`${API_BASE_URL}/auth/register`, {
|
||||
method: 'POST',
|
||||
body: payload
|
||||
})
|
||||
|
||||
if (error.value) {
|
||||
throw error.value
|
||||
}
|
||||
|
||||
return { success: true, data: data.value }
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Register failed:', err)
|
||||
return {
|
||||
success: false,
|
||||
error: err.data?.message || err.data?.error || 'ลงทะเบียนไม่สำเร็จ'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch User Profile (/api/user/me)
|
||||
const fetchUserProfile = async () => {
|
||||
if (!token.value) return
|
||||
|
||||
try {
|
||||
const { data, error } = await useFetch<User>(`${API_BASE_URL}/user/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.value}`
|
||||
}
|
||||
})
|
||||
|
||||
if (error.value) {
|
||||
if (error.value.statusCode === 401) {
|
||||
logout()
|
||||
}
|
||||
throw error.value
|
||||
}
|
||||
|
||||
if (data.value) {
|
||||
user.value = data.value
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch user profile:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Request Password Reset
|
||||
const requestPasswordReset = async (email: string) => {
|
||||
try {
|
||||
const { error } = await useFetch(`${API_BASE_URL}/auth/reset-request`, {
|
||||
method: 'POST',
|
||||
body: { email }
|
||||
})
|
||||
|
||||
if (error.value) throw error.value
|
||||
return { success: true }
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.data?.message || 'ส่งคำขอไม่สำเร็จ' }
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm Reset Password
|
||||
const confirmResetPassword = async (payload: { id: number; token: string; password: string }) => {
|
||||
try {
|
||||
const { error } = await useFetch(`${API_BASE_URL}/auth/reset-password`, {
|
||||
method: 'POST',
|
||||
body: payload
|
||||
})
|
||||
|
||||
if (error.value) throw error.value
|
||||
return { success: true }
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.data?.message || 'รีเซ็ตรหัสผ่านไม่สำเร็จ' }
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh Access Token
|
||||
const refreshAccessToken = async () => {
|
||||
if (!refreshToken.value) return false
|
||||
|
||||
try {
|
||||
const { data, error } = await useFetch<{ token: string; refreshToken: string }>(`${API_BASE_URL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
body: { refreshToken: refreshToken.value }
|
||||
})
|
||||
|
||||
if (error.value) throw error.value
|
||||
|
||||
if (data.value) {
|
||||
token.value = data.value.token
|
||||
refreshToken.value = data.value.refreshToken
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
// Refresh failed, force logout
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Logout
|
||||
const logout = () => {
|
||||
token.value = null
|
||||
// Reset user photo if needed on logout
|
||||
// currentUser.value.photoURL = ''
|
||||
return navigateTo('/auth/login', { replace: true })
|
||||
refreshToken.value = null // Clear refresh token
|
||||
user.value = null
|
||||
const router = useRouter()
|
||||
router.push('/auth/login')
|
||||
}
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
token,
|
||||
currentUser,
|
||||
user,
|
||||
currentUser: computed(() => {
|
||||
if (!user.value) return null
|
||||
|
||||
const prefix = user.value.profile?.prefix?.th || ''
|
||||
const firstName = user.value.profile?.first_name || user.value.username
|
||||
const lastName = user.value.profile?.last_name || ''
|
||||
|
||||
return {
|
||||
prefix,
|
||||
firstName,
|
||||
lastName,
|
||||
email: user.value.email,
|
||||
phone: user.value.profile?.phone || '',
|
||||
photoURL: user.value.profile?.avatar_url || '',
|
||||
role: user.value.role
|
||||
}
|
||||
}),
|
||||
login,
|
||||
register,
|
||||
fetchUserProfile,
|
||||
requestPasswordReset,
|
||||
confirmResetPassword,
|
||||
logout
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export default defineNuxtRouteMiddleware((to) => {
|
||||
const { isAuthenticated } = useAuth()
|
||||
const { isAuthenticated, user } = useAuth()
|
||||
|
||||
// Pages that are accessible only when NOT logged in (Auth pages)
|
||||
const authPages = [
|
||||
|
|
@ -17,6 +17,9 @@ export default defineNuxtRouteMiddleware((to) => {
|
|||
|
||||
// 1. If user is authenticated and tries to access login/register (Keep landing page accessible)
|
||||
if (isAuthenticated.value && authPages.includes(to.path)) {
|
||||
const role = user.value?.role?.code
|
||||
if (role === 'ADMIN') return navigateTo('/admin', { replace: true })
|
||||
if (role === 'INSTRUCTOR') return navigateTo('/instructor', { replace: true })
|
||||
return navigateTo('/dashboard', { replace: true })
|
||||
}
|
||||
|
||||
|
|
|
|||
7
Frontend-Learner/node_modules/.cache/jiti/Frontend-Learner-nuxt.config.949dee75.mjs
generated
vendored
7
Frontend-Learner/node_modules/.cache/jiti/Frontend-Learner-nuxt.config.949dee75.mjs
generated
vendored
|
|
@ -39,5 +39,10 @@ var _default = exports.default = defineNuxtConfig({
|
|||
}]
|
||||
|
||||
}
|
||||
},
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:4000/api'
|
||||
}
|
||||
}
|
||||
}); /* v9-1ad4b5e9404ba42a */
|
||||
}); /* v9-9bf961383479a037 */
|
||||
|
|
|
|||
|
|
@ -40,4 +40,9 @@ export default defineNuxtConfig({
|
|||
],
|
||||
},
|
||||
},
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:4000/api'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,14 +27,22 @@ const forgotRules = {
|
|||
email: { rules: { required: true, email: true }, label: 'อีเมล' }
|
||||
}
|
||||
|
||||
const { requestPasswordReset } = useAuth()
|
||||
|
||||
const sendResetLink = async () => {
|
||||
if (!validate(forgotForm, forgotRules)) return
|
||||
|
||||
isLoading.value = true
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
const result = await requestPasswordReset(forgotForm.email)
|
||||
|
||||
isLoading.value = false
|
||||
forgotStep.value = 'success'
|
||||
|
||||
if (result.success) {
|
||||
forgotStep.value = 'success'
|
||||
} else {
|
||||
alert(result.error || 'ไม่สามารถส่งลิงก์รีเซ็ตได้ กรุณาลองใหม่')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ useHead({
|
|||
})
|
||||
|
||||
const router = useRouter()
|
||||
const { login } = useAuth()
|
||||
const { login, user } = useAuth()
|
||||
const { errors, validate, clearFieldError } = useFormValidation()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
|
@ -41,17 +41,28 @@ const handleLogin = async () => {
|
|||
if (!validate(loginForm, loginRules)) return
|
||||
|
||||
isLoading.value = true
|
||||
// Simulate API call delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
// Demo credential check
|
||||
if (loginForm.email === 'student@example.com' && loginForm.password === '123456') {
|
||||
login() // Set token via auth composable
|
||||
isLoading.value = false
|
||||
router.push('/dashboard')
|
||||
const result = await login({
|
||||
email: loginForm.email,
|
||||
password: loginForm.password
|
||||
})
|
||||
|
||||
isLoading.value = false
|
||||
|
||||
if (result.success) {
|
||||
// Redirect based on user role
|
||||
const role = user.value?.role?.code
|
||||
|
||||
if (role === 'ADMIN') {
|
||||
router.push('/admin')
|
||||
} else if (role === 'INSTRUCTOR') {
|
||||
router.push('/instructor')
|
||||
} else {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
} else {
|
||||
isLoading.value = false
|
||||
alert('อีเมลหรือรหัสผ่านไม่ถูกต้อง! (Demo: student@example.com / 123456)')
|
||||
// Show error from API or fallback
|
||||
alert(result.error || 'อีเมลหรือรหัสผ่านไม่ถูกต้อง')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -109,12 +120,8 @@ const handleLogin = async () => {
|
|||
<span v-else>เข้าสู่ระบบ</span>
|
||||
</button>
|
||||
|
||||
<!-- Demo Credentials Hint (For Development Only) -->
|
||||
<div style="background: var(--neutral-100); padding: 12px; border-radius: 8px; margin-bottom: 16px; border: 1px dashed var(--primary);">
|
||||
<p class="text-xs font-bold text-primary mb-1">🔑 บัญชีทดสอบ:</p>
|
||||
<p class="text-xs text-muted">อีเมล: student@example.com</p>
|
||||
<p class="text-xs text-muted">รหัสผ่าน: 123456</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Social Login (Google) -->
|
||||
<button type="button" class="btn-google w-full mb-6 flex items-center justify-center gap-3">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ useHead({
|
|||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { register } = useAuth(); // Import register from useAuth
|
||||
const { errors, validate, clearFieldError } = useFormValidation();
|
||||
|
||||
const isLoading = ref(false);
|
||||
|
|
@ -52,10 +53,37 @@ const handleRegister = async () => {
|
|||
if (!validate(registerForm, registerRules)) return;
|
||||
|
||||
isLoading.value = true;
|
||||
// Simulate API delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
// Map prefix to { th, en }
|
||||
const prefixMap: Record<string, string> = {
|
||||
'นาย': 'Mr.',
|
||||
'นาง': 'Mrs.',
|
||||
'นางสาว': 'Ms.'
|
||||
};
|
||||
|
||||
const payload = {
|
||||
username: registerForm.username,
|
||||
email: registerForm.email,
|
||||
password: registerForm.password,
|
||||
first_name: registerForm.firstName,
|
||||
last_name: registerForm.lastName,
|
||||
prefix: {
|
||||
th: registerForm.prefix,
|
||||
en: prefixMap[registerForm.prefix] || 'Mr.'
|
||||
},
|
||||
phone: registerForm.phone
|
||||
};
|
||||
|
||||
const result = await register(payload);
|
||||
|
||||
isLoading.value = false;
|
||||
router.push("/dashboard");
|
||||
|
||||
if (result.success) {
|
||||
alert('สมัครสมาชิกสำเร็จ! กรุณาเข้าสู่ระบบ');
|
||||
router.push("/auth/login");
|
||||
} else {
|
||||
alert(result.error || 'การลงทะเบียนล้มเหลว');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ useHead({
|
|||
title: 'ตั้งรหัสผ่านใหม่ - e-Learning'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { confirmResetPassword } = useAuth()
|
||||
const { errors, validate, clearFieldError } = useFormValidation()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
|
@ -28,14 +30,41 @@ const resetRules = {
|
|||
confirmPassword: { rules: { required: true, match: 'password' }, label: 'ยืนยันรหัสผ่าน' }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!route.query.token || !route.query.id) {
|
||||
alert('ลิงก์รีเซ็ตรหัสผ่านไม่ถูกต้องหรือหมดอายุ')
|
||||
router.push('/auth/login')
|
||||
}
|
||||
})
|
||||
|
||||
const resetPassword = async () => {
|
||||
if (!validate(resetForm, resetRules)) return
|
||||
|
||||
// Extract token and id from query
|
||||
const token = route.query.token as string
|
||||
const id = Number(route.query.id)
|
||||
|
||||
if (!token || !id) {
|
||||
alert('ข้อมูลสำหรับรีเซ็ตไม่ครบถ้วน')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
const result = await confirmResetPassword({
|
||||
id,
|
||||
token,
|
||||
password: resetForm.password
|
||||
})
|
||||
|
||||
isLoading.value = false
|
||||
alert('รีเซ็ตรหัสผ่านสำเร็จ!')
|
||||
router.push('/auth/login')
|
||||
|
||||
if (result.success) {
|
||||
alert('รีเซ็ตรหัสผ่านสำเร็จ! กรุณาเข้าสู่ระบบด้วยรหัสผ่านใหม่')
|
||||
router.push('/auth/login')
|
||||
} else {
|
||||
alert(result.error || 'เกิดข้อผิดพลาดในการรีเซ็ตรหัสผ่าน')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -66,11 +66,11 @@ onUnmounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="quiz-shell min-h-screen bg-white dark:bg-[#0b0f1a] text-slate-900 dark:text-slate-200 font-main antialiased selection:bg-blue-500/20 transition-colors">
|
||||
<!-- Header: Precise matching of the image -->
|
||||
<header class="h-14 bg-white dark:bg-[#161b22] fixed top-0 inset-x-0 z-[100] flex items-center px-6 border-b border-slate-300 dark:border-white/5 transition-colors">
|
||||
<div class="quiz-shell min-h-screen bg-slate-50 dark:bg-[#0b0f1a] text-slate-900 dark:text-slate-200 font-main antialiased selection:bg-blue-500/20 transition-colors">
|
||||
<!-- Header -->
|
||||
<header class="h-14 bg-white dark:bg-[#161b22] fixed top-0 inset-x-0 z-[100] flex items-center px-6 border-b border-slate-200 dark:border-white/5 transition-colors">
|
||||
<div class="flex items-center">
|
||||
<button class="flex items-center gap-2 text-sm font-bold text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors" @click="confirmExit">
|
||||
<button class="flex items-center gap-2 text-sm font-bold text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white transition-colors" @click="confirmExit">
|
||||
<span>←</span>
|
||||
<span>ออกจากแบบทดสอบ</span>
|
||||
</button>
|
||||
|
|
@ -82,15 +82,14 @@ onUnmounted(() => {
|
|||
<!-- Main Content Area -->
|
||||
<main class="pt-14 h-screen flex items-center justify-center overflow-y-auto px-4 custom-scrollbar">
|
||||
|
||||
<!-- 1. START SCREEN (MATCHING IMAGE) -->
|
||||
<!-- Displays quiz info, instructions, and start button -->
|
||||
<!-- 1. START SCREEN -->
|
||||
<div v-if="currentScreen === 'start'" class="w-full max-w-[640px] animate-fade-in py-12">
|
||||
<div class="bg-white dark:bg-[#1e293b]/50 border border-slate-300 dark:border-white/5 rounded-[32px] p-8 md:p-14 shadow-lg dark:shadow-2xl dark:backdrop-blur-sm relative overflow-hidden transition-colors">
|
||||
<div class="bg-white dark:!bg-[#1e293b] border border-slate-200 dark:border-white/5 rounded-[32px] p-8 md:p-14 shadow-lg dark:shadow-2xl relative overflow-hidden transition-colors">
|
||||
|
||||
<!-- Top Icon Wrapper -->
|
||||
<div class="flex justify-center mb-10">
|
||||
<div class="w-20 h-20 rounded-3xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center shadow-inner">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<div class="w-20 h-20 rounded-3xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 flex items-center justify-center shadow-inner">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue-600 dark:text-blue-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<path d="M10 12h4"/><path d="M10 16h4"/><path d="M10 8h1"/>
|
||||
|
|
@ -101,36 +100,36 @@ onUnmounted(() => {
|
|||
|
||||
<div class="text-center mb-10">
|
||||
<h2 class="text-[32px] font-black text-slate-900 dark:text-white mb-2 tracking-tight">แบบทดสอบท้ายบท</h2>
|
||||
<p class="text-[13px] font-bold text-slate-700 dark:text-slate-500 uppercase tracking-widest leading-none">เบื้องต้นการออกแบบ UX/UI</p>
|
||||
<p class="text-[13px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest leading-none">เบื้องต้นการออกแบบ UX/UI</p>
|
||||
</div>
|
||||
|
||||
<!-- Instruction Box -->
|
||||
<div class="bg-[#0b121f]/80 p-8 rounded-3xl mb-8 border border-white/5">
|
||||
<h3 class="text-[12px] font-black text-slate-400 mb-6 uppercase tracking-[0.2em] flex items-center gap-2">
|
||||
<div class="bg-slate-50 dark:bg-[#0b121f]/80 p-8 rounded-3xl mb-8 border border-slate-100 dark:border-white/5">
|
||||
<h3 class="text-[12px] font-black text-slate-500 dark:text-slate-400 mb-6 uppercase tracking-[0.2em] flex items-center gap-2">
|
||||
คำชี้แจง
|
||||
</h3>
|
||||
<ul class="space-y-4">
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 mt-1.5 flex-shrink-0"/>
|
||||
<span class="text-[14px] text-slate-300 font-medium leading-relaxed">แบบทดสอบนี้มีทั้งหมด <strong class="text-white">10 ข้อ</strong></span>
|
||||
<span class="text-[14px] text-slate-600 dark:text-slate-300 font-medium leading-relaxed">แบบทดสอบนี้มีทั้งหมด <strong class="text-slate-900 dark:text-white">10 ข้อ</strong></span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 mt-1.5 flex-shrink-0"/>
|
||||
<span class="text-[14px] text-slate-300 font-medium leading-relaxed">เกณฑ์การผ่าน <strong class="text-white">80% ขึ้นไป</strong></span>
|
||||
<span class="text-[14px] text-slate-600 dark:text-slate-300 font-medium leading-relaxed">เกณฑ์การผ่าน <strong class="text-slate-900 dark:text-white">80% ขึ้นไป</strong></span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 mt-1.5 flex-shrink-0"/>
|
||||
<span class="text-[14px] text-slate-300 font-medium leading-relaxed">เวลาในการทำ: <strong class="text-white">30 นาที</strong></span>
|
||||
<span class="text-[14px] text-slate-600 dark:text-slate-300 font-medium leading-relaxed">เวลาในการทำ: <strong class="text-slate-900 dark:text-white">30 นาที</strong></span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 mt-1.5 flex-shrink-0"/>
|
||||
<span class="text-[14px] text-slate-300 font-medium leading-relaxed">ไม่สามารถหยุดเวลาชั่วคราวได้เมื่อเริ่มทำแบบทดสอบแล้ว</span>
|
||||
<span class="text-[14px] text-slate-600 dark:text-slate-300 font-medium leading-relaxed">ไม่สามารถหยุดเวลาชั่วคราวได้เมื่อเริ่มทำแบบทดสอบแล้ว</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="text-[13px] font-black text-slate-500 mb-10 flex items-center justify-between px-2">
|
||||
<span>คะแนนที่ดีที่สุด: <span class="text-white">-</span></span>
|
||||
<div class="text-[13px] font-black text-slate-400 dark:text-slate-500 mb-10 flex items-center justify-between px-2">
|
||||
<span>คะแนนที่ดีที่สุด: <span class="text-slate-900 dark:text-white">-</span></span>
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
|
|
@ -144,70 +143,68 @@ onUnmounted(() => {
|
|||
</div>
|
||||
|
||||
<!-- 2. TAKING SCREEN -->
|
||||
<!-- Quiz Interface with Timer, Question, and Options -->
|
||||
<div v-if="currentScreen === 'taking'" class="w-full max-w-[840px] animate-fade-in py-12">
|
||||
<div class="bg-[#1e293b]/50 border border-white/5 rounded-[32px] p-8 md:p-14 shadow-2xl backdrop-blur-sm">
|
||||
<div class="flex items-center justify-between mb-10 pb-6 border-b border-white/5">
|
||||
<div class="bg-white dark:!bg-[#1e293b] border border-slate-200 dark:border-white/5 rounded-[32px] p-8 md:p-14 shadow-2xl backdrop-blur-sm">
|
||||
<div class="flex items-center justify-between mb-10 pb-6 border-b border-slate-100 dark:border-white/5">
|
||||
<div>
|
||||
<div class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] mb-2">ข้อที่ 1 จาก 10</div>
|
||||
<div class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] mb-2">ข้อที่ 1 จาก 10</div>
|
||||
<!-- Progress Line -->
|
||||
<div class="w-48 h-1 bg-white/5 rounded-full overflow-hidden">
|
||||
<div class="w-48 h-1 bg-slate-100 dark:bg-white/5 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-blue-500" style="width: 10%;"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Timer Display -->
|
||||
<div class="flex items-center gap-3 px-5 py-2.5 bg-amber-500/10 border border-amber-500/20 rounded-2xl text-amber-500">
|
||||
<div class="flex items-center gap-3 px-5 py-2.5 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 rounded-2xl text-amber-600 dark:text-amber-500">
|
||||
<span class="text-sm">⏱</span>
|
||||
<span class="text-[15px] font-black font-mono tracking-widest">{{ timerDisplay }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-12">
|
||||
<h2 class="text-[22px] font-black text-white leading-tight mb-8">ข้อใดต่อไปนี้คือหลักการแรกของ User Experience (UX) ตามโมเดลของ Peter Morville?</h2>
|
||||
<h2 class="text-[22px] font-black text-slate-900 dark:text-white leading-tight mb-8">ข้อใดต่อไปนี้คือหลักการแรกของ User Experience (UX) ตามโมเดลของ Peter Morville?</h2>
|
||||
|
||||
<!-- Question Options -->
|
||||
<div class="space-y-4">
|
||||
<button v-for="i in 4" :key="i" class="w-full p-6 text-left rounded-2xl border border-white/5 bg-white/5 hover:bg-white/10 transition-all flex items-center gap-4 group">
|
||||
<div class="w-6 h-6 rounded-full border-2 border-slate-700 flex items-center justify-center group-hover:border-blue-500 transition-colors">
|
||||
<button v-for="i in 4" :key="i" class="w-full p-6 text-left rounded-2xl border border-slate-200 dark:border-white/5 bg-slate-50 dark:bg-white/5 hover:bg-white hover:border-blue-500 hover:shadow-lg dark:hover:bg-white/10 transition-all flex items-center gap-4 group">
|
||||
<div class="w-6 h-6 rounded-full border-2 border-slate-300 dark:border-slate-700 flex items-center justify-center group-hover:border-blue-500 transition-colors">
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-blue-500 opacity-0 group-focus:opacity-100"/>
|
||||
</div>
|
||||
<span class="text-[15px] font-medium text-slate-300">ตัวเลือกที่ {{ i }} สำหรับคำตอบที่เป็นไปได้</span>
|
||||
<span class="text-[15px] font-medium text-slate-700 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white">ตัวเลือกที่ {{ i }} สำหรับคำตอบที่เป็นไปได้</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<button class="px-8 py-3 text-slate-500 font-bold hover:text-white transition-colors">ย้อนกลับ</button>
|
||||
<button class="px-8 py-3 text-slate-400 dark:text-slate-500 font-bold hover:text-slate-600 dark:hover:text-white transition-colors">ย้อนกลับ</button>
|
||||
<button class="px-10 py-4 bg-blue-600 text-white rounded-2xl font-black text-sm shadow-lg shadow-blue-600/20" @click="submitQuiz(false)">ถัดไป</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. RESULT SCREEN (MATCHING IMAGE) -->
|
||||
<!-- Displays score, status, and summary stats -->
|
||||
<!-- 3. RESULT SCREEN -->
|
||||
<div v-if="currentScreen === 'result'" class="w-full max-w-[640px] animate-fade-in py-12">
|
||||
<div class="bg-[#1e293b]/50 border border-white/5 rounded-[40px] p-10 md:p-14 shadow-2xl text-center backdrop-blur-sm">
|
||||
<div class="bg-white dark:!bg-[#1e293b] border border-slate-200 dark:border-white/5 rounded-[40px] p-10 md:p-14 shadow-2xl text-center backdrop-blur-sm">
|
||||
<!-- Trophy Icon -->
|
||||
<div class="w-20 h-20 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center mx-auto mb-10 shadow-inner">
|
||||
<span class="text-4xl">🏆</span>
|
||||
</div>
|
||||
|
||||
<h2 class="text-[32px] font-black text-white mb-2 tracking-tight">ยินดีด้วยคุณสอบผ่าน!</h2>
|
||||
<p class="text-[13px] font-bold text-slate-500 uppercase tracking-widest mb-12">คุณทำคะแนนได้ยอดเยี่ยมและผ่านเกณฑ์การทดสอบ</p>
|
||||
<h2 class="text-[32px] font-black text-slate-900 dark:text-white mb-2 tracking-tight">ยินดีด้วยคุณสอบผ่าน!</h2>
|
||||
<p class="text-[13px] font-bold text-slate-500 dark:text-slate-500 uppercase tracking-widest mb-12">คุณทำคะแนนได้ยอดเยี่ยมและผ่านเกณฑ์การทดสอบ</p>
|
||||
|
||||
<!-- Stats Boxes -->
|
||||
<div class="grid grid-cols-3 gap-4 mb-14">
|
||||
<div class="p-6 rounded-[24px] bg-[#0b121f]/60 border border-white/5 shadow-inner">
|
||||
<div class="text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3">คะแนน</div>
|
||||
<div class="text-[20px] font-black text-blue-500">90%</div>
|
||||
<div class="p-6 rounded-[24px] bg-slate-50 dark:bg-[#0b121f]/60 border border-slate-100 dark:border-white/5 shadow-inner">
|
||||
<div class="text-[9px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] mb-3">คะแนน</div>
|
||||
<div class="text-[20px] font-black text-blue-600 dark:text-blue-500">90%</div>
|
||||
</div>
|
||||
<div class="p-6 rounded-[24px] bg-[#0b121f]/60 border border-white/5 shadow-inner">
|
||||
<div class="text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3">ตอบถูก</div>
|
||||
<div class="text-[20px] font-black text-emerald-500">9/10</div>
|
||||
<div class="p-6 rounded-[24px] bg-slate-50 dark:bg-[#0b121f]/60 border border-slate-100 dark:border-white/5 shadow-inner">
|
||||
<div class="text-[9px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] mb-3">ตอบถูก</div>
|
||||
<div class="text-[20px] font-black text-emerald-600 dark:text-emerald-500">9/10</div>
|
||||
</div>
|
||||
<div class="p-6 rounded-[24px] bg-[#0b121f]/60 border border-white/5 shadow-inner">
|
||||
<div class="text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3">เวลาที่ใช้</div>
|
||||
<div class="text-[20px] font-black text-white">12:45</div>
|
||||
<div class="p-6 rounded-[24px] bg-slate-50 dark:bg-[#0b121f]/60 border border-slate-100 dark:border-white/5 shadow-inner">
|
||||
<div class="text-[9px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] mb-3">เวลาที่ใช้</div>
|
||||
<div class="text-[20px] font-black text-slate-900 dark:text-white">12:45</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -221,7 +218,7 @@ onUnmounted(() => {
|
|||
</button>
|
||||
<NuxtLink
|
||||
to="/dashboard"
|
||||
class="w-full py-5 bg-[#1e293b] hover:bg-[#253347] text-slate-400 hover:text-white rounded-[24px] font-black text-[14px] tracking-wider transition-all border border-white/5 block"
|
||||
class="w-full py-5 bg-slate-100 dark:bg-[#1e293b] hover:bg-slate-200 dark:hover:bg-[#253347] text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white rounded-[24px] font-black text-[14px] tracking-wider transition-all border border-slate-200 dark:border-white/5 block"
|
||||
>
|
||||
กลับไปหน้าหลัก
|
||||
</NuxtLink>
|
||||
|
|
@ -229,62 +226,61 @@ onUnmounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. REVIEW (เฉลย) SCREEN -->
|
||||
<!-- Detailed review of questions with correct/incorrect indicators and explanations -->
|
||||
<!-- 4. REVIEW SCREEN -->
|
||||
<div v-if="currentScreen === 'review'" class="w-full max-w-[840px] animate-fade-in py-12">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h2 class="text-[24px] font-black text-white tracking-tight">ดูเฉลยและทบทวนรายข้อ</h2>
|
||||
<button class="text-[13px] font-black text-slate-400 hover:text-white transition-colors flex items-center gap-2" @click="currentScreen = 'result'"/>
|
||||
<h2 class="text-[24px] font-black text-slate-900 dark:text-white tracking-tight">ดูเฉลยและทบทวนรายข้อ</h2>
|
||||
<button class="text-[13px] font-black text-slate-400 hover:text-slate-600 dark:hover:text-white transition-colors flex items-center gap-2" @click="currentScreen = 'result'"/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Review Item: Correct Answer -->
|
||||
<div class="bg-[#1e293b]/40 border border-emerald-500/20 rounded-[32px] p-8 md:p-10 shadow-xl backdrop-blur-sm relative overflow-hidden group">
|
||||
<div class="bg-white dark:bg-[#1e293b]/40 border border-emerald-500/20 rounded-[32px] p-8 md:p-10 shadow-xl backdrop-blur-sm relative overflow-hidden group">
|
||||
<div class="absolute left-0 top-0 bottom-0 w-1.5 bg-emerald-500"/>
|
||||
<div class="flex items-center gap-2 mb-6 text-[10px] font-black uppercase tracking-widest">
|
||||
<span class="text-emerald-500">✓ ตอบถูก</span>
|
||||
<span class="text-slate-600">• ข้อที่ 1</span>
|
||||
<span class="text-slate-600 dark:text-slate-500">• ข้อที่ 1</span>
|
||||
</div>
|
||||
<h3 class="text-[18px] font-black text-white leading-tight mb-8">ข้อใดต่อไปนี้อธิบายกระบวนการออกแบบ "Double Diamond" ได้ดีที่สุด?</h3>
|
||||
<h3 class="text-[18px] font-black text-slate-900 dark:text-white leading-tight mb-8">ข้อใดต่อไปนี้อธิบายกระบวนการออกแบบ "Double Diamond" ได้ดีที่สุด?</h3>
|
||||
|
||||
<div class="space-y-3 mb-8">
|
||||
<div class="p-5 rounded-2xl bg-emerald-500/5 border border-emerald-500/20 text-emerald-400 font-bold text-[14px] flex items-center justify-between">
|
||||
<div class="p-5 rounded-2xl bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400 font-bold text-[14px] flex items-center justify-between">
|
||||
<span>กระบวนการแตกประเด็นเพื่อค้นหา/พัฒนา และสรุปประเด็นเพื่อกำหนด/ส่งมอบ</span>
|
||||
<span class="text-xs">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-[#0b121f]/60 p-6 rounded-2xl border border-white/5">
|
||||
<h4 class="text-[11px] font-black text-slate-500 uppercase tracking-widest mb-3">คำอธิบาย:</h4>
|
||||
<p class="text-[14px] text-slate-400 leading-relaxed font-medium">
|
||||
<div class="bg-slate-50 dark:bg-[#0b121f]/60 p-6 rounded-2xl border border-slate-100 dark:border-white/5">
|
||||
<h4 class="text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-3">คำอธิบาย:</h4>
|
||||
<p class="text-[14px] text-slate-600 dark:text-slate-400 leading-relaxed font-medium">
|
||||
Double Diamond ประกอบด้วย 4 ขั้นตอนหลัก: Discover, Define, Develop และ Deliver ซึ่งเน้นการสลับกันระหว่างความคิดสร้างสรรค์แบบเปิดกว้าง (Divergent) และการคัดกรองเพื่อให้ได้ข้อสรุป (Convergent)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Review Item: Incorrect Answer -->
|
||||
<div class="bg-[#1e293b]/40 border border-red-500/20 rounded-[32px] p-8 md:p-10 shadow-xl backdrop-blur-sm relative overflow-hidden">
|
||||
<div class="bg-white dark:bg-[#1e293b]/40 border border-red-500/20 rounded-[32px] p-8 md:p-10 shadow-xl backdrop-blur-sm relative overflow-hidden">
|
||||
<div class="absolute left-0 top-0 bottom-0 w-1.5 bg-red-500"/>
|
||||
<div class="flex items-center gap-2 mb-6 text-[10px] font-black uppercase tracking-widest">
|
||||
<span class="text-red-500">✗ ตอบผิด</span>
|
||||
<span class="text-slate-600">• ข้อที่ 2</span>
|
||||
<span class="text-slate-600 dark:text-slate-500">• ข้อที่ 2</span>
|
||||
</div>
|
||||
<h3 class="text-[18px] font-black text-white leading-tight mb-8">เป้าหมายหลักของ User Research คืออะไร?</h3>
|
||||
<h3 class="text-[18px] font-black text-slate-900 dark:text-white leading-tight mb-8">เป้าหมายหลักของ User Research คืออะไร?</h3>
|
||||
|
||||
<div class="space-y-3 mb-8">
|
||||
<div class="p-5 rounded-2xl bg-white/5 border border-red-500/30 text-red-400 font-medium text-[14px]">
|
||||
<div class="p-5 rounded-2xl bg-red-50 dark:bg-white/5 border border-red-500/30 text-red-500 dark:text-red-400 font-medium text-[14px]">
|
||||
<span class="opacity-50 line-through">เพื่อให้แน่ใจว่าดีไซน์ที่ทำออกมาสวยงามที่สุด</span>
|
||||
<span class="ml-2 text-[10px] bg-red-500/20 px-2 py-0.5 rounded text-red-500">คำตอบของคุณ</span>
|
||||
<span class="ml-2 text-[10px] bg-red-500/20 px-2 py-0.5 rounded text-red-600 dark:text-red-500">คำตอบของคุณ</span>
|
||||
</div>
|
||||
<div class="p-5 rounded-2xl bg-emerald-500/5 border border-emerald-500/20 text-emerald-400 font-bold text-[14px] flex items-center justify-between">
|
||||
<div class="p-5 rounded-2xl bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400 font-bold text-[14px] flex items-center justify-between">
|
||||
<span>เพื่อทำความเข้าใจความต้องการ ปัญหา และพฤติกรรมของผู้ใช้ที่แท้จริง</span>
|
||||
<span class="text-[10px] bg-emerald-500/20 px-2 py-0.5 rounded text-emerald-400">คำตอบที่ถูกต้อง</span>
|
||||
<span class="text-[10px] bg-emerald-500/20 px-2 py-0.5 rounded text-emerald-600 dark:text-emerald-400">คำตอบที่ถูกต้อง</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-[#0b121f]/60 p-6 rounded-2xl border border-white/5">
|
||||
<h4 class="text-[11px] font-black text-slate-500 uppercase tracking-widest mb-3">คำอธิบาย:</h4>
|
||||
<p class="text-[14px] text-slate-400 leading-relaxed font-medium">
|
||||
<div class="bg-slate-50 dark:bg-[#0b121f]/60 p-6 rounded-2xl border border-slate-100 dark:border-white/5">
|
||||
<h4 class="text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-3">คำอธิบาย:</h4>
|
||||
<p class="text-[14px] text-slate-600 dark:text-slate-400 leading-relaxed font-medium">
|
||||
User Research ไม่ใช่แค่การดูความสวยงาม แต่คือการหา "Insights" เพื่อนำมาแก้ปัญหาให้ตรงจุด ลดความเสี่ยงในการสร้างของที่ผู้ใช้งานไม่ได้ต้องการจริงๆ
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -293,7 +289,7 @@ onUnmounted(() => {
|
|||
</div>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<button class="px-10 py-4 bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white rounded-[20px] font-black text-sm border border-white/5 transition-all" @click="currentScreen = 'result'">กลับไปหน้าสรุปผล</button>
|
||||
<button class="px-10 py-4 bg-slate-100 hover:bg-slate-200 dark:bg-white/5 dark:hover:bg-white/10 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white rounded-[20px] font-black text-sm border border-slate-200 dark:border-white/5 transition-all" @click="currentScreen = 'result'">กลับไปหน้าสรุปผล</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ const isEditing = ref(false)
|
|||
|
||||
// User Profile Data Management
|
||||
const userData = ref({
|
||||
firstName: currentUser.value.firstName,
|
||||
lastName: currentUser.value.lastName,
|
||||
email: currentUser.value.email,
|
||||
firstName: currentUser.value?.firstName || '',
|
||||
lastName: currentUser.value?.lastName || '',
|
||||
email: currentUser.value?.email || '',
|
||||
phone: '0812345678',
|
||||
joinDate: '12 ธ.ค. 2024',
|
||||
photoURL: '',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue