diff --git a/frontend_management/.dockerignore b/frontend_management/.dockerignore new file mode 100644 index 00000000..5d93bccf --- /dev/null +++ b/frontend_management/.dockerignore @@ -0,0 +1,37 @@ +# Dependencies +node_modules + +# Build output +.output +.nuxt +dist + +# Development files +.env +.env.local +*.log + +# IDE & OS files +.vscode +.idea +*.swp +*.swo +.DS_Store +Thumbs.db + +# Git +.git +.gitignore + +# Docker files (prevent recursive copying) +Dockerfile* +docker-compose*.yml +.dockerignore + +# Documentation +*.md +!README.md + +# Test files +coverage +.nyc_output diff --git a/frontend_management/Dockerfile b/frontend_management/Dockerfile new file mode 100644 index 00000000..84d49d4f --- /dev/null +++ b/frontend_management/Dockerfile @@ -0,0 +1,55 @@ +# ================================ +# Build Stage +# ================================ +FROM node:20-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# ================================ +# Production Stage +# ================================ +FROM node:20-alpine AS production + +# Set working directory +WORKDIR /app + +# Set environment to production +ENV NODE_ENV=production + +# Create non-root user for security +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 nuxtjs + +# Copy package files for preview command +COPY --from=builder --chown=nuxtjs:nodejs /app/package*.json ./ +COPY --from=builder --chown=nuxtjs:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=nuxtjs:nodejs /app/.nuxt ./.nuxt +COPY --from=builder --chown=nuxtjs:nodejs /app/.output ./.output + +# Switch to non-root user +USER nuxtjs + +# Expose port +EXPOSE 3001 + +# Set default environment variables +ENV HOST=0.0.0.0 +ENV PORT=3001 +ENV NUXT_HOST=0.0.0.0 +ENV NUXT_PORT=3001 + +# Start the application using preview command +CMD ["npm", "run", "preview"] diff --git a/frontend_management/app.vue b/frontend_management/app.vue index ad39ea29..be012dac 100644 --- a/frontend_management/app.vue +++ b/frontend_management/app.vue @@ -7,3 +7,59 @@ + + diff --git a/frontend_management/assets/css/main.css b/frontend_management/assets/css/main.css index 8048aefa..132180ef 100644 --- a/frontend_management/assets/css/main.css +++ b/frontend_management/assets/css/main.css @@ -89,13 +89,30 @@ --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); } -/* Base Styles */ +/* Base Styles - Force consistent font across dev and production */ +html { + font-size: 16px !important; +} + body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; + font-family: 'Prompt', 'Sarabun', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; + font-size: 1rem !important; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +/* Override Quasar's default font settings for production build */ +.q-page, +.q-layout, +.q-card, +.q-item, +.q-btn, +.q-table, +h1, h2, h3, h4, h5, h6, +p, span, div, a, label { + font-family: inherit !important; +} + /* Thai Language Support */ html[lang="th"] body { font-family: 'Prompt', 'Sarabun', 'Inter', sans-serif; diff --git a/frontend_management/docker-compose.yml b/frontend_management/docker-compose.yml new file mode 100644 index 00000000..abafbb22 --- /dev/null +++ b/frontend_management/docker-compose.yml @@ -0,0 +1,26 @@ +version: "3.8" + +services: + frontend_management: + build: + context: . + dockerfile: Dockerfile + container_name: elearning-frontend-management + ports: + - "3001:3001" + environment: + - NODE_ENV=production + - NUXT_PUBLIC_API_BASE_URL=${NUXT_PUBLIC_API_BASE_URL:-http://localhost:4000/api} + restart: unless-stopped + healthcheck: + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - elearning-network + +networks: + elearning-network: + driver: bridge diff --git a/frontend_management/pages/login.vue b/frontend_management/pages/login.vue index c87c80c9..f6c716da 100644 --- a/frontend_management/pages/login.vue +++ b/frontend_management/pages/login.vue @@ -140,6 +140,15 @@ onMounted(() => { navigateTo('/instructor'); } } + + // Load remembered email + if (typeof window !== 'undefined') { + const savedEmail = localStorage.getItem('rememberedEmail'); + if (savedEmail) { + email.value = savedEmail; + rememberMe.value = true; + } + } }); // Login form @@ -159,6 +168,13 @@ const handleLogin = async () => { try { const response = await authStore.login(email.value, password.value); + // Handle Remember Me + if (rememberMe.value) { + localStorage.setItem('rememberedEmail', email.value); + } else { + localStorage.removeItem('rememberedEmail'); + } + $q.notify({ type: 'positive', message: response.message || 'เข้าสู่ระบบสำเร็จ', diff --git a/frontend_management/pages/register.vue b/frontend_management/pages/register.vue index aed4f822..762ce9e3 100644 --- a/frontend_management/pages/register.vue +++ b/frontend_management/pages/register.vue @@ -98,13 +98,13 @@
@@ -183,6 +183,7 @@ const router = useRouter(); const loading = ref(false); const showPassword = ref(false); const confirmPassword = ref(''); +const selectedPrefix = ref(null); // Form const form = ref({ @@ -191,23 +192,41 @@ const form = ref({ password: '', first_name: '', last_name: '', - prefix: { en: '', th: '' }, + prefix: { th: '', en: '' }, // We construct this on submit phone: '' }); // Prefix options +const prefixMap: Record = { + 'mr': { th: 'นาย', en: 'Mr.' }, + 'mrs': { th: 'นาง', en: 'Mrs.' }, + 'ms': { th: 'นางสาว', en: 'Ms.' }, + 'dr': { th: 'ดร.', en: 'Dr.' }, + 'asst_prof': { th: 'ผศ.', en: 'Asst.Prof.' }, + 'assoc_prof': { th: 'รศ.', en: 'Assoc.Prof.' }, + 'prof': { th: 'ศ.', en: 'Prof.' } +}; + const prefixOptions = [ - { label: 'นาย / Mr.', value: { th: 'นาย', en: 'Mr.' } }, - { label: 'นาง / Mrs.', value: { th: 'นาง', en: 'Mrs.' } }, - { label: 'นางสาว / Ms.', value: { th: 'นางสาว', en: 'Ms.' } }, - { label: 'ดร. / Dr.', value: { th: 'ดร.', en: 'Dr.' } }, - { label: 'ผศ. / Asst.Prof.', value: { th: 'ผศ.', en: 'Asst.Prof.' } }, - { label: 'รศ. / Assoc.Prof.', value: { th: 'รศ.', en: 'Assoc.Prof.' } }, - { label: 'ศ. / Prof.', value: { th: 'ศ.', en: 'Prof.' } } + { label: 'นาย / Mr.', value: 'mr' }, + { label: 'นาง / Mrs.', value: 'mrs' }, + { label: 'นางสาว / Ms.', value: 'ms' }, + { label: 'ดร. / Dr.', value: 'dr' }, + { label: 'ผศ. / Asst.Prof.', value: 'asst_prof' }, + { label: 'รศ. / Assoc.Prof.', value: 'assoc_prof' }, + { label: 'ศ. / Prof.', value: 'prof' } ]; // Methods const handleRegister = async () => { + if (!selectedPrefix.value) { + $q.notify({ type: 'warning', message: 'กรุณาเลือกคำนำหน้า', position: 'top' }); + return; + } + + // Map selected prefix string back to object + form.value.prefix = prefixMap[selectedPrefix.value]; + loading.value = true; try { await authService.registerInstructor(form.value);