init frontend_management
This commit is contained in:
parent
af58550f7f
commit
62812f2090
23 changed files with 13174 additions and 0 deletions
5
frontend_management/.env.example
Normal file
5
frontend_management/.env.example
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# API Configuration
|
||||
API_BASE_URL=http://localhost:3001/api
|
||||
|
||||
# Application
|
||||
NODE_ENV=development
|
||||
24
frontend_management/.gitignore
vendored
Normal file
24
frontend_management/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
75
frontend_management/README.md
Normal file
75
frontend_management/README.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
9
frontend_management/app.vue
Normal file
9
frontend_management/app.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// This is the main app entry point
|
||||
</script>
|
||||
109
frontend_management/assets/css/main.css
Normal file
109
frontend_management/assets/css/main.css
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/* Import Google Fonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Prompt:wght@400;500;600;700&family=Sarabun:wght@400;500;600;700&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* CSS Variables for Design System */
|
||||
:root {
|
||||
/* Primary Colors */
|
||||
--primary-50: #EFF6FF;
|
||||
--primary-100: #DBEAFE;
|
||||
--primary-200: #BFDBFE;
|
||||
--primary-300: #93C5FD;
|
||||
--primary-400: #60A5FA;
|
||||
--primary-500: #3B82F6;
|
||||
--primary-600: #2563EB;
|
||||
--primary-700: #1D4ED8;
|
||||
--primary-800: #1E40AF;
|
||||
--primary-900: #1E3A8A;
|
||||
|
||||
/* Secondary Colors */
|
||||
--secondary-500: #10B981;
|
||||
--accent-500: #F59E0B;
|
||||
--error-500: #EF4444;
|
||||
|
||||
/* Neutral Colors */
|
||||
--gray-50: #F9FAFB;
|
||||
--gray-100: #F3F4F6;
|
||||
--gray-200: #E5E7EB;
|
||||
--gray-300: #D1D5DB;
|
||||
--gray-400: #9CA3AF;
|
||||
--gray-500: #6B7280;
|
||||
--gray-600: #4B5563;
|
||||
--gray-700: #374151;
|
||||
--gray-800: #1F2937;
|
||||
--gray-900: #111827;
|
||||
|
||||
/* Dark Mode */
|
||||
--dark-bg-primary: #0F172A;
|
||||
--dark-bg-secondary: #1E293B;
|
||||
--dark-bg-tertiary: #334155;
|
||||
--dark-text-primary: #F1F5F9;
|
||||
--dark-text-secondary: #CBD5E1;
|
||||
|
||||
/* Typography */
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
--text-3xl: 1.875rem;
|
||||
--text-4xl: 2.25rem;
|
||||
--text-5xl: 3rem;
|
||||
|
||||
/* Font Weights */
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
|
||||
/* Spacing (8px base unit) */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.25rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-8: 2rem;
|
||||
--space-10: 2.5rem;
|
||||
--space-12: 3rem;
|
||||
--space-16: 4rem;
|
||||
--space-20: 5rem;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Thai Language Support */
|
||||
html[lang="th"] body {
|
||||
font-family: 'Prompt', 'Sarabun', 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Custom Utilities */
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
74
frontend_management/layouts/instructor.vue
Normal file
74
frontend_management/layouts/instructor.vue
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- Sidebar -->
|
||||
<aside class="fixed left-0 top-0 h-screen w-64 bg-white shadow-lg">
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-primary-600">E-Learning</h2>
|
||||
<p class="text-sm text-gray-500">Instructor Panel</p>
|
||||
</div>
|
||||
|
||||
<nav class="px-4">
|
||||
<NuxtLink
|
||||
to="/instructor"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-primary-100 shadow-md transition mb-2"
|
||||
active-class="bg-primary-500 text-white hover:bg-primary-600 "
|
||||
>
|
||||
<span>📊</span>
|
||||
<span>Dashboard</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
to="/instructor/courses"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-primary-200 shadow-md transition mb-2"
|
||||
active-class="bg-primary-500 text-white hover:bg-primary-600"
|
||||
>
|
||||
<span>📚</span>
|
||||
<span>หลักสูตร</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
to="/instructor/announcements"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-primary-200 shadow-md transition mb-2"
|
||||
active-class="bg-primary-500 text-white hover:bg-primary-600"
|
||||
>
|
||||
<span>📢</span>
|
||||
<span>ประกาศ</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
to="/instructor/reports"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-primary-200 shadow-md transition mb-2"
|
||||
active-class="bg-primary-500 text-white hover:bg-primary-600"
|
||||
>
|
||||
<span>📈</span>
|
||||
<span>รายงาน</span>
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 border-t">
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="w-full flex items-center gap-3 px-4 py-3 text-red-600 hover:bg-red-50 rounded-lg transition"
|
||||
>
|
||||
<span>🚪</span>
|
||||
<span>ออกจากระบบ</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="ml-64 p-8">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = () => {
|
||||
authStore.logout();
|
||||
router.push('/login');
|
||||
};
|
||||
</script>
|
||||
7
frontend_management/middleware/admin.ts
Normal file
7
frontend_management/middleware/admin.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const authStore = useAuthStore();
|
||||
authStore.checkAuth();
|
||||
if (!authStore.isAdmin) {
|
||||
return navigateTo('/login');
|
||||
}
|
||||
});
|
||||
7
frontend_management/middleware/auth.ts
Normal file
7
frontend_management/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const authStore = useAuthStore();
|
||||
authStore.checkAuth();
|
||||
if (!authStore.isAuthenticated) {
|
||||
return navigateTo('/login');
|
||||
}
|
||||
});
|
||||
7
frontend_management/middleware/instructor.ts
Normal file
7
frontend_management/middleware/instructor.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const authStore = useAuthStore();
|
||||
authStore.checkAuth();
|
||||
if (!authStore.isInstructor) {
|
||||
return navigateTo('/login');
|
||||
}
|
||||
});
|
||||
46
frontend_management/nuxt.config.ts
Normal file
46
frontend_management/nuxt.config.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'nuxt-quasar-ui',
|
||||
'@pinia/nuxt',
|
||||
'@nuxtjs/tailwindcss'
|
||||
],
|
||||
quasar: {
|
||||
plugins: [
|
||||
'Notify',
|
||||
'Dialog',
|
||||
'Loading',
|
||||
'LocalStorage'
|
||||
],
|
||||
config: {
|
||||
brand: {
|
||||
primary: '#3B82F6',
|
||||
secondary: '#10B981',
|
||||
accent: '#F59E0B',
|
||||
dark: '#1F2937',
|
||||
positive: '#10B981',
|
||||
negative: '#EF4444',
|
||||
info: '#3B82F6',
|
||||
warning: '#F59E0B'
|
||||
}
|
||||
}
|
||||
},
|
||||
css: [
|
||||
'~/assets/css/main.css'
|
||||
],
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBase: process.env.API_BASE_URL || 'http://localhost:3001/api',
|
||||
useMockData: process.env.USE_MOCK_DATA === 'true'
|
||||
}
|
||||
},
|
||||
devtools: { enabled: true },
|
||||
app: {
|
||||
head: {
|
||||
title: 'E-Learning System',
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
12221
frontend_management/package-lock.json
generated
Normal file
12221
frontend_management/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
frontend_management/package.json
Normal file
26
frontend_management/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "frontend_management",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"@quasar/extras": "^1.17.0",
|
||||
"nuxt": "^3.20.2",
|
||||
"pinia": "^3.0.4",
|
||||
"quasar": "^2.18.6",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@types/node": "^25.0.3",
|
||||
"nuxt-quasar-ui": "^3.0.0"
|
||||
}
|
||||
}
|
||||
59
frontend_management/pages/admin/index.vue
Normal file
59
frontend_management/pages/admin/index.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-6">Dashboard</h1>
|
||||
<p class="text-gray-600 mb-8">ยินดีต้อนรับ, {{ authStore.user?.fullName }}</p>
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">หลักสูตรทั้งหมด</p>
|
||||
<p class="text-3xl font-bold text-primary-600">5</p>
|
||||
</div>
|
||||
<q-icon name="school" size="48px" class="text-primary-200" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">ผู้เรียนทั้งหมด</p>
|
||||
<p class="text-3xl font-bold text-secondary-500">125</p>
|
||||
</div>
|
||||
<q-icon name="people" size="48px" class="text-secondary-200" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">เรียนจบแล้ว</p>
|
||||
<p class="text-3xl font-bold text-accent-500">45</p>
|
||||
</div>
|
||||
<q-icon name="emoji_events" size="48px" class="text-accent-200" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Recent Courses -->
|
||||
<div class="card">
|
||||
<h2 class="text-xl font-semibold mb-4">หลักสูตรล่าสุด</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4 p-4 bg-gray-50 rounded-lg">
|
||||
<div class="w-16 h-16 bg-primary-100 rounded-lg flex items-center justify-center">
|
||||
<q-icon name="code" size="32px" class="text-primary-600" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold">Python เบื้องต้น</h3>
|
||||
<p class="text-sm text-gray-600">45 ผู้เรียน • 8 บทเรียน</p>
|
||||
</div>
|
||||
<q-btn flat color="primary" label="ดูรายละเอียด" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'instructor',
|
||||
middleware: 'instructor'
|
||||
});
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
47
frontend_management/pages/index.vue
Normal file
47
frontend_management/pages/index.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-4xl font-bold text-primary-600 mb-4">
|
||||
E-Learning Management System
|
||||
</h1>
|
||||
<p class="text-lg text-gray-600 mb-8">
|
||||
ระบบจัดการเรียนการสอนออนไลน์
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<q-card class="shadow-lg">
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-primary-600">📚 Courses</div>
|
||||
<p class="text-gray-600">จัดการหลักสูตรและเนื้อหา</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card class="shadow-lg">
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-secondary-500">👥 Students</div>
|
||||
<p class="text-gray-600">จัดการผู้เรียน</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card class="shadow-lg">
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-accent-500">📊 Reports</div>
|
||||
<p class="text-gray-600">รายงานและสถิติ</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
label="login"
|
||||
@click="handleLogin"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Home page
|
||||
const handleLogin = async () => {
|
||||
await navigateTo('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
106
frontend_management/pages/instructor/index.vue
Normal file
106
frontend_management/pages/instructor/index.vue
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">สวัสดี, อาจารย์ทดสอบ 👋</h1>
|
||||
<p class="text-gray-600 mt-2">ยินดีต้อนรับกลับสู่ระบบ</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center text-2xl">
|
||||
👤
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<q-card class="p-6 text-center">
|
||||
<div class="text-4xl font-bold text-primary-600 mb-2">
|
||||
{{ instructorStore.stats.totalCourses }}
|
||||
</div>
|
||||
<div class="text-gray-600">หลักสูตรทั้งหมด</div>
|
||||
</q-card>
|
||||
|
||||
<q-card class="p-6 text-center">
|
||||
<div class="text-4xl font-bold text-secondary-600 mb-2">
|
||||
{{ instructorStore.stats.totalStudents }}
|
||||
</div>
|
||||
<div class="text-gray-600">ผู้เรียนทั้งหมด</div>
|
||||
</q-card>
|
||||
|
||||
<q-card class="p-6 text-center">
|
||||
<div class="text-4xl font-bold text-accent-600 mb-2">
|
||||
{{ instructorStore.stats.completedStudents }}
|
||||
</div>
|
||||
<div class="text-gray-600">เรียนจบแล้ว</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Chart and Recent Courses -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Chart Placeholder -->
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h3 class="text-xl font-semibold mb-4">📊 สถิติผู้สมัครรวม (รายเดือน)</h3>
|
||||
<div class="bg-gray-100 rounded-lg p-12 text-center text-gray-500">
|
||||
[กราฟแสดงสถิติผู้สมัครรวม (รายเดือน)]
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- Recent Courses -->
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-semibold">📚 หลักสูตรล่าสุด</h3>
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
label="ดูทั้งหมด"
|
||||
@click="router.push('/instructor/courses')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<q-card
|
||||
v-for="course in instructorStore.recentCourses"
|
||||
:key="course.id"
|
||||
class="cursor-pointer hover:shadow-md transition"
|
||||
@click="router.push(`/instructor/courses/${course.id}`)"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="flex gap-4">
|
||||
<div class="w-20 h-16 bg-primary-100 rounded-lg flex items-center justify-center text-3xl">
|
||||
{{ course.icon }}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-gray-900">{{ course.title }}</div>
|
||||
<div class="text-sm text-gray-600 mt-1">
|
||||
{{ course.students }} ผู้เรียน • {{ course.lessons }} บทเรียน
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'instructor',
|
||||
middleware: 'auth'
|
||||
});
|
||||
|
||||
const instructorStore = useInstructorStore();
|
||||
const router = useRouter();
|
||||
|
||||
// Fetch dashboard data on mount
|
||||
onMounted(() => {
|
||||
instructorStore.fetchDashboardData();
|
||||
});
|
||||
</script>
|
||||
93
frontend_management/pages/login.vue
Normal file
93
frontend_management/pages/login.vue
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100">
|
||||
<q-card class="w-full max-w-md p-8 shadow-xl">
|
||||
<q-card-section>
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">E-Learning</h1>
|
||||
<p class="text-gray-600 mt-2">เข้าสู่ระบบ</p>
|
||||
</div>
|
||||
<q-form @submit="handleLogin" class="space-y-4">
|
||||
<q-input
|
||||
v-model="email"
|
||||
label="อีเมล"
|
||||
type="email"
|
||||
outlined
|
||||
:rules="[val => !!val || 'กรุณากรอกอีเมล']"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="email" />
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="password"
|
||||
label="รหัสผ่าน"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
outlined
|
||||
:rules="[val => !!val || 'กรุณากรอกรหัสผ่าน']"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="lock" />
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
:name="showPassword ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
label="เข้าสู่ระบบ"
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:loading="loading"
|
||||
/>
|
||||
</q-form>
|
||||
<div class="mt-6 text-center text-sm text-gray-600">
|
||||
<p>ทดสอบ: instructor@test.com / admin@test.com</p>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useQuasar } from 'quasar';
|
||||
definePageMeta({
|
||||
layout: 'auth'
|
||||
});
|
||||
const $q = useQuasar();
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
const email = ref('');
|
||||
const password = ref('');
|
||||
const showPassword = ref(false);
|
||||
const loading = ref(false);
|
||||
const handleLogin = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await authStore.login(email.value, password.value);
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'เข้าสู่ระบบสำเร็จ',
|
||||
position: 'top'
|
||||
});
|
||||
// Redirect based on role
|
||||
if (authStore.isInstructor) {
|
||||
router.push('/instructor');
|
||||
} else if (authStore.isAdmin) {
|
||||
router.push('/admin');
|
||||
}
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'อีเมลหรือรหัสผ่านไม่ถูกต้อง',
|
||||
position: 'top'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
74
frontend_management/plugins/mock-api.ts
Normal file
74
frontend_management/plugins/mock-api.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
export default defineNuxtPlugin(() => {
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
if (!config.public.useMockData) {
|
||||
return; // ใช้ API จริง
|
||||
}
|
||||
// Mock Data
|
||||
const mockUsers = [
|
||||
{ id: '1', email: 'instructor@test.com', fullName: 'อาจารย์ทดสอบ', role: 'INSTRUCTOR' },
|
||||
{ id: '2', email: 'admin@test.com', fullName: 'ผู้ดูแลระบบ', role: 'ADMIN' }
|
||||
];
|
||||
const mockCourses = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Python เบื้องต้น',
|
||||
description: 'เรียนรู้ Python จากพื้นฐาน',
|
||||
instructorId: '1',
|
||||
status: 'PUBLISHED',
|
||||
thumbnail: '/images/python.jpg'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'JavaScript สำหรับเว็บ',
|
||||
description: 'พัฒนาเว็บด้วย JavaScript',
|
||||
instructorId: '1',
|
||||
status: 'DRAFT',
|
||||
thumbnail: '/images/javascript.jpg'
|
||||
}
|
||||
];
|
||||
// Mock API Functions
|
||||
const mockApi = {
|
||||
// Auth
|
||||
login: async (email: string, password: string) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate delay
|
||||
const user = mockUsers.find(u => u.email === email);
|
||||
if (user) {
|
||||
return {
|
||||
token: 'mock-jwt-token',
|
||||
user
|
||||
};
|
||||
}
|
||||
throw new Error('Invalid credentials');
|
||||
},
|
||||
// Courses
|
||||
getCourses: async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return mockCourses;
|
||||
},
|
||||
getCourse: async (id: string) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return mockCourses.find(c => c.id === id);
|
||||
},
|
||||
createCourse: async (data: any) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const newCourse = {
|
||||
id: String(mockCourses.length + 1),
|
||||
...data,
|
||||
status: 'DRAFT'
|
||||
};
|
||||
mockCourses.push(newCourse);
|
||||
return newCourse;
|
||||
},
|
||||
// Users
|
||||
getUsers: async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return mockUsers;
|
||||
}
|
||||
};
|
||||
return {
|
||||
provide: {
|
||||
mockApi
|
||||
}
|
||||
};
|
||||
});
|
||||
BIN
frontend_management/public/favicon.ico
Normal file
BIN
frontend_management/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
frontend_management/public/robots.txt
Normal file
2
frontend_management/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-Agent: *
|
||||
Disallow:
|
||||
77
frontend_management/stores/auth.ts
Normal file
77
frontend_management/stores/auth.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { defineStore } from 'pinia';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: 'INSTRUCTOR' | 'ADMIN' | 'STUDENT';
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
user: null as User | null,
|
||||
token: null as string | null,
|
||||
isAuthenticated: false
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isInstructor: (state) => state.user?.role === 'INSTRUCTOR',
|
||||
isAdmin: (state) => state.user?.role === 'ADMIN',
|
||||
isStudent: (state) => state.user?.role === 'STUDENT'
|
||||
},
|
||||
|
||||
actions: {
|
||||
async login(email: string, password: string) {
|
||||
// TODO: Replace with real API call
|
||||
// const { $api } = useNuxtApp();
|
||||
// const response = await $api('/auth/login', {
|
||||
// method: 'POST',
|
||||
// body: { email, password }
|
||||
// });
|
||||
|
||||
// Mock login for development
|
||||
const mockUser: User = {
|
||||
id: '1',
|
||||
email: email,
|
||||
fullName: 'อาจารย์ทดสอบ',
|
||||
role: 'INSTRUCTOR'
|
||||
};
|
||||
|
||||
this.token = 'mock-jwt-token';
|
||||
this.user = mockUser;
|
||||
this.isAuthenticated = true;
|
||||
|
||||
// Save to localStorage
|
||||
if (process.client) {
|
||||
localStorage.setItem('token', this.token);
|
||||
localStorage.setItem('user', JSON.stringify(this.user));
|
||||
}
|
||||
|
||||
return { token: this.token, user: this.user };
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.user = null;
|
||||
this.token = null;
|
||||
this.isAuthenticated = false;
|
||||
|
||||
if (process.client) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
},
|
||||
|
||||
checkAuth() {
|
||||
if (process.client) {
|
||||
const token = localStorage.getItem('token');
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
if (token && user) {
|
||||
this.token = token;
|
||||
this.user = JSON.parse(user);
|
||||
this.isAuthenticated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
60
frontend_management/stores/instructor.ts
Normal file
60
frontend_management/stores/instructor.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { defineStore } from 'pinia';
|
||||
|
||||
interface Course {
|
||||
id: string;
|
||||
title: string;
|
||||
students: number;
|
||||
lessons: number;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface DashboardStats {
|
||||
totalCourses: number;
|
||||
totalStudents: number;
|
||||
completedStudents: number;
|
||||
}
|
||||
|
||||
export const useInstructorStore = defineStore('instructor', {
|
||||
state: () => ({
|
||||
stats: {
|
||||
totalCourses: 5,
|
||||
totalStudents: 125,
|
||||
completedStudents: 45
|
||||
} as DashboardStats,
|
||||
|
||||
recentCourses: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Python เบื้องต้น',
|
||||
students: 45,
|
||||
lessons: 8,
|
||||
icon: '📘'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'JavaScript สำหรับเว็บ',
|
||||
students: 32,
|
||||
lessons: 12,
|
||||
icon: '📗'
|
||||
}
|
||||
] as Course[]
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getDashboardStats: (state) => state.stats,
|
||||
getRecentCourses: (state) => state.recentCourses
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchDashboardData() {
|
||||
// TODO: Replace with real API call
|
||||
// const { $api } = useNuxtApp();
|
||||
// const data = await $api('/instructor/dashboard');
|
||||
// this.stats = data.stats;
|
||||
// this.recentCourses = data.recentCourses;
|
||||
|
||||
// Using mock data for now
|
||||
console.log('Using mock data for instructor dashboard');
|
||||
}
|
||||
}
|
||||
});
|
||||
42
frontend_management/tailwind.config.js
Normal file
42
frontend_management/tailwind.config.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./components/**/*.{vue,js,ts}',
|
||||
'./layouts/**/*.vue',
|
||||
'./pages/**/*.vue',
|
||||
'./plugins/**/*.{js,ts}',
|
||||
'./app.vue'
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#EFF6FF',
|
||||
100: '#DBEAFE',
|
||||
200: '#BFDBFE',
|
||||
300: '#93C5FD',
|
||||
400: '#60A5FA',
|
||||
500: '#3B82F6',
|
||||
600: '#2563EB',
|
||||
700: '#1D4ED8',
|
||||
800: '#1E40AF',
|
||||
900: '#1E3A8A'
|
||||
},
|
||||
secondary: {
|
||||
500: '#10B981'
|
||||
},
|
||||
accent: {
|
||||
500: '#F59E0B'
|
||||
},
|
||||
error: {
|
||||
500: '#EF4444'
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Prompt', 'Sarabun', 'Inter', 'sans-serif'],
|
||||
thai: ['Prompt', 'Sarabun', 'Inter', 'sans-serif']
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
}
|
||||
4
frontend_management/tsconfig.json
Normal file
4
frontend_management/tsconfig.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue