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">
|
||||
// This is the main app entry point
|
||||
</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);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
|
|
|
|||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// 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 || 'เข้าสู่ระบบสำเร็จ',
|
||||
|
|
|
|||
|
|
@ -98,13 +98,13 @@
|
|||
<!-- Prefix & Name -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<q-select
|
||||
v-model="form.prefix"
|
||||
v-model="selectedPrefix"
|
||||
:options="prefixOptions"
|
||||
label="คำนำหน้า *"
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
:rules="[val => !!val.th || 'กรุณาเลือกคำนำหน้า']"
|
||||
:rules="[val => !!val || 'กรุณาเลือกคำนำหน้า']"
|
||||
hide-bottom-space
|
||||
lazy-rules="ondemand"
|
||||
/>
|
||||
|
|
@ -183,6 +183,7 @@ const router = useRouter();
|
|||
const loading = ref(false);
|
||||
const showPassword = ref(false);
|
||||
const confirmPassword = ref('');
|
||||
const selectedPrefix = ref<string | null>(null);
|
||||
|
||||
// Form
|
||||
const form = ref<RegisterInstructorRequest>({
|
||||
|
|
@ -191,23 +192,41 @@ const form = ref<RegisterInstructorRequest>({
|
|||
password: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
prefix: { en: '', th: '' },
|
||||
prefix: { th: '', en: '' }, // We construct this on submit
|
||||
phone: ''
|
||||
});
|
||||
|
||||
// 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 = [
|
||||
{ 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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue