feat: initialize Nuxt.js frontend application with Docker setup, global styling, and authentication pages.
This commit is contained in:
parent
cf37d7371c
commit
dfcabe306c
7 changed files with 238 additions and 12 deletions
37
frontend_management/.dockerignore
Normal file
37
frontend_management/.dockerignore
Normal file
|
|
@ -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
|
||||||
55
frontend_management/Dockerfile
Normal file
55
frontend_management/Dockerfile
Normal file
|
|
@ -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"]
|
||||||
|
|
@ -7,3 +7,59 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// This is the main app entry point
|
// This is the main app entry point
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Global font override for production build consistency */
|
||||||
|
:root {
|
||||||
|
--q-body-font-size: 16px !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
body.q-body--force-scrollbar-y,
|
||||||
|
.q-body--force-scrollbar-y {
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-family: 'Prompt', 'Sarabun', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force Quasar components to use correct font size */
|
||||||
|
.q-page,
|
||||||
|
.q-layout,
|
||||||
|
.q-drawer,
|
||||||
|
.q-header,
|
||||||
|
.q-footer,
|
||||||
|
.q-card,
|
||||||
|
.q-card__section,
|
||||||
|
.q-item,
|
||||||
|
.q-item__label,
|
||||||
|
.q-btn,
|
||||||
|
.q-table,
|
||||||
|
.q-tabs,
|
||||||
|
.q-tab,
|
||||||
|
.q-field,
|
||||||
|
.q-field__native,
|
||||||
|
.q-input,
|
||||||
|
.q-select {
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography sizes */
|
||||||
|
h1, .text-h1 { font-size: 2rem !important; }
|
||||||
|
h2, .text-h2 { font-size: 1.5rem !important; }
|
||||||
|
h3, .text-h3 { font-size: 1.25rem !important; }
|
||||||
|
h4, .text-h4 { font-size: 1.15rem !important; }
|
||||||
|
h5, .text-h5 { font-size: 1.125rem !important; }
|
||||||
|
h6, .text-h6 { font-size: 1rem !important; }
|
||||||
|
|
||||||
|
.text-body1 { font-size: 1rem !important; }
|
||||||
|
.text-body2 { font-size: 0.875rem !important; }
|
||||||
|
.text-caption { font-size: 0.75rem !important; }
|
||||||
|
.text-overline { font-size: 0.75rem !important; }
|
||||||
|
.text-subtitle1 { font-size: 1rem !important; }
|
||||||
|
.text-subtitle2 { font-size: 0.875rem !important; }
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -89,13 +89,30 @@
|
||||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
--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 {
|
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;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-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 */
|
/* Thai Language Support */
|
||||||
html[lang="th"] body {
|
html[lang="th"] body {
|
||||||
font-family: 'Prompt', 'Sarabun', 'Inter', sans-serif;
|
font-family: 'Prompt', 'Sarabun', 'Inter', sans-serif;
|
||||||
|
|
|
||||||
26
frontend_management/docker-compose.yml
Normal file
26
frontend_management/docker-compose.yml
Normal file
|
|
@ -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
|
||||||
|
|
@ -140,6 +140,15 @@ onMounted(() => {
|
||||||
navigateTo('/instructor');
|
navigateTo('/instructor');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load remembered email
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const savedEmail = localStorage.getItem('rememberedEmail');
|
||||||
|
if (savedEmail) {
|
||||||
|
email.value = savedEmail;
|
||||||
|
rememberMe.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Login form
|
// Login form
|
||||||
|
|
@ -159,6 +168,13 @@ const handleLogin = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await authStore.login(email.value, password.value);
|
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({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: response.message || 'เข้าสู่ระบบสำเร็จ',
|
message: response.message || 'เข้าสู่ระบบสำเร็จ',
|
||||||
|
|
|
||||||
|
|
@ -98,13 +98,13 @@
|
||||||
<!-- Prefix & Name -->
|
<!-- Prefix & Name -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
<q-select
|
<q-select
|
||||||
v-model="form.prefix"
|
v-model="selectedPrefix"
|
||||||
:options="prefixOptions"
|
:options="prefixOptions"
|
||||||
label="คำนำหน้า *"
|
label="คำนำหน้า *"
|
||||||
outlined
|
outlined
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
:rules="[val => !!val.th || 'กรุณาเลือกคำนำหน้า']"
|
:rules="[val => !!val || 'กรุณาเลือกคำนำหน้า']"
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
/>
|
/>
|
||||||
|
|
@ -183,6 +183,7 @@ const router = useRouter();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
const confirmPassword = ref('');
|
const confirmPassword = ref('');
|
||||||
|
const selectedPrefix = ref<string | null>(null);
|
||||||
|
|
||||||
// Form
|
// Form
|
||||||
const form = ref<RegisterInstructorRequest>({
|
const form = ref<RegisterInstructorRequest>({
|
||||||
|
|
@ -191,23 +192,41 @@ const form = ref<RegisterInstructorRequest>({
|
||||||
password: '',
|
password: '',
|
||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
prefix: { en: '', th: '' },
|
prefix: { th: '', en: '' }, // We construct this on submit
|
||||||
phone: ''
|
phone: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prefix options
|
// Prefix options
|
||||||
|
const prefixMap: Record<string, { th: string; en: string }> = {
|
||||||
|
'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 = [
|
const prefixOptions = [
|
||||||
{ label: 'นาย / Mr.', value: { th: 'นาย', en: 'Mr.' } },
|
{ label: 'นาย / Mr.', value: 'mr' },
|
||||||
{ label: 'นาง / Mrs.', value: { th: 'นาง', en: 'Mrs.' } },
|
{ label: 'นาง / Mrs.', value: 'mrs' },
|
||||||
{ label: 'นางสาว / Ms.', value: { th: 'นางสาว', en: 'Ms.' } },
|
{ label: 'นางสาว / Ms.', value: 'ms' },
|
||||||
{ label: 'ดร. / Dr.', value: { th: 'ดร.', en: 'Dr.' } },
|
{ label: 'ดร. / Dr.', value: 'dr' },
|
||||||
{ label: 'ผศ. / Asst.Prof.', value: { th: 'ผศ.', en: 'Asst.Prof.' } },
|
{ label: 'ผศ. / Asst.Prof.', value: 'asst_prof' },
|
||||||
{ label: 'รศ. / Assoc.Prof.', value: { th: 'รศ.', en: 'Assoc.Prof.' } },
|
{ label: 'รศ. / Assoc.Prof.', value: 'assoc_prof' },
|
||||||
{ label: 'ศ. / Prof.', value: { th: 'ศ.', en: 'Prof.' } }
|
{ label: 'ศ. / Prof.', value: 'prof' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const handleRegister = async () => {
|
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;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await authService.registerInstructor(form.value);
|
await authService.registerInstructor(form.value);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue