feat: introduce core e-learning frontend pages and components for dashboard, profile, classroom, and layout.
This commit is contained in:
parent
3f93dc8ab5
commit
eb248f7ca2
12 changed files with 137 additions and 135 deletions
|
|
@ -660,6 +660,19 @@ ul {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
/* Page Container for consistent layout across dashboard/browse pages */
|
||||
.page-container {
|
||||
max-width: 1280px; /* max-7xl equivalent roughly */
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.page-container {
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Responsive Helpers
|
||||
=========================== */
|
||||
|
|
|
|||
|
|
@ -41,22 +41,7 @@ const getLocalizedText = (text: any) => {
|
|||
>
|
||||
<div v-if="courseData" class="h-full scroll">
|
||||
<q-list class="pb-10">
|
||||
<!-- Announcements Sidebar Item -->
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
@click="emit('open-announcements')"
|
||||
class="bg-blue-50 dark:bg-blue-900/10 border-b border-blue-100 dark:border-blue-900/20"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label class="font-bold text-slate-800 dark:text-blue-200 text-sm pl-2">
|
||||
{{ $t('classroom.announcements', 'ประกาศในคอร์ส') }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side v-if="hasUnreadAnnouncements">
|
||||
<q-badge color="red" rounded label="New" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
|
||||
<template v-for="chapter in courseData.chapters" :key="chapter.id">
|
||||
<q-item-label header class="bg-slate-100 dark:bg-slate-800 text-[var(--text-main)] font-bold sticky top-0 z-10 border-b dark:border-white/5 text-sm py-4">
|
||||
|
|
|
|||
|
|
@ -32,11 +32,14 @@ const searchText = ref('')
|
|||
/>
|
||||
|
||||
<!-- Branding -->
|
||||
<div class="flex items-center gap-2 cursor-pointer" @click="navigateTo('/dashboard')">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-bold">
|
||||
<div class="flex items-center gap-3 cursor-pointer group" @click="navigateTo('/dashboard')">
|
||||
<div class="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center text-white font-black shadow-lg shadow-blue-600/30 group-hover:scale-110 transition-transform">
|
||||
E
|
||||
</div>
|
||||
<span class="font-bold text-xl text-blue-600 dark:text-blue-400 hidden xs:block">e-Learning</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-black text-lg leading-none tracking-tight text-slate-900 dark:text-white group-hover:text-blue-600 transition-colors">E-Learning</span>
|
||||
<span class="text-[10px] font-bold uppercase tracking-[0.2em] leading-none mt-1 text-slate-500 dark:text-slate-400">Platform</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
|
|
|||
|
|
@ -48,50 +48,94 @@ const handleNavigate = (path: string) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full bg-transparent">
|
||||
<!-- Branding Area (Optional if not in Header) -->
|
||||
|
||||
<q-list padding class="text-slate-600 dark:text-slate-300 flex-grow">
|
||||
<div class="flex flex-col h-full bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-700">
|
||||
|
||||
<!-- Branding / Logo Area -->
|
||||
<div class="px-6 py-8 flex items-center gap-3 cursor-pointer group" @click="handleNavigate('/dashboard')">
|
||||
<div class="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center text-white font-black shadow-lg shadow-blue-600/30 group-hover:scale-110 transition-transform">
|
||||
E
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-black text-lg leading-none tracking-tight text-slate-900 dark:text-white">E-Learning</span>
|
||||
<span class="text-[10px] font-bold uppercase tracking-[0.2em] leading-none mt-1 text-slate-500">Platform</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Items -->
|
||||
<q-list padding class="text-slate-600 dark:text-slate-300 flex-grow px-3">
|
||||
<q-item
|
||||
v-for="item in navItems"
|
||||
:key="item.to"
|
||||
clickable
|
||||
v-ripple
|
||||
@click="handleNavigate(item.to)"
|
||||
class="rounded-r-full mr-2 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-white/5"
|
||||
:class="{ 'q-router-link--active': $route.path === item.to || ($route.path === '/dashboard' && item.to === '/dashboard') }"
|
||||
class="rounded-xl mb-1 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-white/5"
|
||||
:class="{ 'sidebar-item--active': $route.path === item.to || ($route.path === '/dashboard' && item.to === '/dashboard') }"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="item.icon" />
|
||||
<q-icon :name="item.icon" size="22px" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ item.label }}</q-item-label>
|
||||
<q-item-label class="font-bold text-sm">{{ item.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<!-- Footer / Version -->
|
||||
<div class="mt-auto p-4 text-xs text-center opacity-50 pb-8">
|
||||
<p>e-Learning v0.1.0</p>
|
||||
<p>© 2026</p>
|
||||
<!-- Sidebar Bottom: Logout & Version -->
|
||||
<div class="p-4 border-t border-slate-100 dark:border-slate-700/50">
|
||||
<q-btn
|
||||
flat
|
||||
rounded
|
||||
class="w-full text-slate-500 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/10 transition-all font-bold"
|
||||
no-caps
|
||||
@click="() => useAuth().logout()"
|
||||
>
|
||||
<q-icon name="logout" size="20px" class="mr-3" />
|
||||
{{ t('auth.logout') }}
|
||||
</q-btn>
|
||||
|
||||
<div class="mt-4 px-4 text-center">
|
||||
<p class="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Version 0.1.0 (Alpha)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Light Mode Active State */
|
||||
.q-item.q-router-link--active {
|
||||
background: rgb(239 246 255); /* blue-50 */
|
||||
color: rgb(29 78 216); /* blue-700 */
|
||||
font-weight: 700;
|
||||
/* Active State styling for Sidebar items */
|
||||
.sidebar-item--active {
|
||||
background: rgb(239 246 255) !important; /* blue-50 */
|
||||
color: rgb(29 78 216) !important; /* blue-700 */
|
||||
}
|
||||
|
||||
.sidebar-item--active .q-icon {
|
||||
color: rgb(37 99 235) !important; /* blue-600 */
|
||||
}
|
||||
|
||||
/* Vertical indicator for active item */
|
||||
.sidebar-item--active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
top: 15%;
|
||||
height: 70%;
|
||||
width: 4px;
|
||||
background: #2563eb;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
/* Dark Mode Active State */
|
||||
.dark .q-item.q-router-link--active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgb(147 197 253); /* blue-300 */
|
||||
.dark .sidebar-item--active {
|
||||
background: rgba(37, 99, 235, 0.15) !important;
|
||||
color: rgb(147 197 253) !important; /* blue-300 */
|
||||
}
|
||||
|
||||
.dark .sidebar-item--active .q-icon {
|
||||
color: rgb(96 165 250) !important; /* blue-400 */
|
||||
}
|
||||
|
||||
.dark .sidebar-item--active::after {
|
||||
background: #60a5fa;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@
|
|||
* Uses Quasar QLayout for responsive structure.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// Initialize global theme management
|
||||
useThemeMode()
|
||||
</script>
|
||||
|
|
@ -16,7 +14,7 @@ useThemeMode()
|
|||
<!-- Header -->
|
||||
<q-header
|
||||
bordered
|
||||
class="!bg-white dark:!bg-[#1e293b] !text-slate-900 dark:!text-white border-b border-slate-200 dark:border-slate-700"
|
||||
class="!bg-white/80 dark:!bg-[#1e293b]/80 backdrop-blur-md !text-slate-900 dark:!text-white border-b border-slate-200 dark:border-slate-700"
|
||||
>
|
||||
<AppHeader />
|
||||
</q-header>
|
||||
|
|
|
|||
|
|
@ -105,18 +105,18 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 max-w-[1400px] mx-auto">
|
||||
<div class="page-container">
|
||||
|
||||
|
||||
<!-- CATALOG VIEW: Browse courses -->
|
||||
<div v-if="!showDetail">
|
||||
|
||||
<!-- Top Header Area -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
||||
<!-- Title -->
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">
|
||||
<h1 class="text-3xl font-black text-slate-900 dark:text-white">
|
||||
{{ $t('discovery.title') }}
|
||||
</h2>
|
||||
</h1>
|
||||
|
||||
<!-- Right Side: Search & Sort -->
|
||||
<div class="flex items-center gap-3">
|
||||
|
|
|
|||
|
|
@ -471,20 +471,43 @@ onBeforeUnmount(() => {
|
|||
<q-toolbar>
|
||||
<q-btn flat round dense icon="menu" class="lg:hidden mr-2 text-slate-900 dark:text-white" @click="toggleSidebar" />
|
||||
|
||||
<a
|
||||
href="/dashboard/my-courses"
|
||||
class="inline-flex items-center gap-2 text-slate-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-300 transition-all font-black text-sm md:text-base group mr-4 no-underline"
|
||||
>
|
||||
<q-icon name="arrow_back" size="24px" class="transition-transform group-hover:-translate-x-1" />
|
||||
<span>{{ $t('classroom.backToDashboard') }}</span>
|
||||
</a>
|
||||
<!-- Back Button & Branding -->
|
||||
<div class="flex items-center gap-2 mr-6">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="arrow_back"
|
||||
class="text-slate-600 dark:text-slate-300 hover:text-blue-600 transition-colors"
|
||||
@click="$router.push('/dashboard/my-courses')"
|
||||
>
|
||||
<q-tooltip>{{ $t('classroom.backToDashboard') }}</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-toolbar-title class="text-base font-bold text-center lg:text-left truncate text-slate-900 dark:text-white">
|
||||
<div class="hidden sm:flex items-center gap-2 cursor-pointer group" @click="$router.push('/dashboard')">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-black shadow-lg shadow-blue-600/30 group-hover:scale-110 transition-transform">
|
||||
E
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-toolbar-title class="text-base font-bold text-left truncate text-slate-900 dark:text-white">
|
||||
{{ courseData ? getLocalizedText(courseData.course.title) : $t('classroom.loadingTitle') }}
|
||||
</q-toolbar-title>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Right actions -->
|
||||
<div class="flex items-center gap-2 pr-2">
|
||||
<!-- Announcements Button -->
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="campaign"
|
||||
@click="handleOpenAnnouncements"
|
||||
class="text-slate-600 dark:text-slate-300 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
<q-badge v-if="hasUnreadAnnouncements" color="red" floating rounded />
|
||||
<q-tooltip>{{ $t('classroom.announcements', 'ประกาศในคอร์ส') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ useHead({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto max-w-7xl px-4 py-8">
|
||||
<div class="page-container">
|
||||
|
||||
|
||||
<div v-if="course" class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
|
|
@ -80,7 +80,7 @@ useHead({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-slate-900 dark:text-white mb-4 leading-tight">{{ getLocalizedText(course.title) }}</h1>
|
||||
<h1 class="text-3xl md:text-4xl font-black text-slate-900 dark:text-white mb-6 leading-tight">{{ getLocalizedText(course.title) }}</h1>
|
||||
|
||||
<div class="prose prose-slate dark:prose-invert max-w-none mb-10 text-slate-600 dark:text-slate-300">
|
||||
<p class="text-lg leading-relaxed">{{ getLocalizedText(course.description) }}</p>
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ useHead({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-container">
|
||||
<!-- Page Header -->
|
||||
<h1 class="text-[28px] font-bold mb-6 text-slate-900 dark:text-white">ประกาศ</h1>
|
||||
<h1 class="text-3xl font-black mb-10 text-slate-900 dark:text-white">ประกาศ</h1>
|
||||
|
||||
<!--
|
||||
Main Layout: 12-column Grid
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ onMounted(async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard-container py-8 px-4">
|
||||
<div class="page-container">
|
||||
<!-- Welcome Header Section -->
|
||||
<div class="welcome-section mb-10 overflow-hidden relative rounded-[2.5rem] p-10 md:p-14 text-white shadow-lg dark:shadow-2xl dark:shadow-blue-900/20 transition-all">
|
||||
<div class="relative z-10 flex flex-col md:flex-row justify-between items-center gap-8">
|
||||
|
|
@ -87,18 +87,18 @@ onMounted(async () => {
|
|||
</div>
|
||||
|
||||
<!-- Recommended Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<NuxtLink v-for="(course, idx) in recommendedCourses" :key="course.id" :to="`/course/${course.id}`" class="p-0 overflow-hidden group border border-slate-200 dark:border-white/5 rounded-3xl shadow-sm dark:shadow-xl transition-all hover:-translate-y-1 dark:hover:-translate-y-1 block" style="background-color: var(--bg-surface);">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 pb-20">
|
||||
<NuxtLink v-for="(course, idx) in recommendedCourses" :key="course.id" :to="`/course/${course.id}`" class="p-0 overflow-hidden group border border-slate-200 dark:border-white/5 rounded-3xl shadow-sm dark:shadow-xl transition-all hover:-translate-y-1 block" style="background-color: var(--bg-surface);">
|
||||
<div class="h-48 overflow-hidden relative rounded-t-3xl">
|
||||
<img v-if="course.image" :src="course.image" :alt="course.title" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700" >
|
||||
<div class="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-slate-900/20 dark:from-[#1e293b] to-transparent"/>
|
||||
</div>
|
||||
<div class="p-7" style="background-color: var(--bg-surface);">
|
||||
<div class="text-[10px] font-black text-slate-700 dark:text-slate-500 uppercase tracking-[0.2em] mb-2">{{ course.category }}</div>
|
||||
<h4 class="font-black text-xl mb-6 text-slate-900 dark:text-white group-hover:text-blue-700 dark:group-hover:text-blue-400 transition-colors line-clamp-2">{{ course.title }}</h4>
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-black text-xl mb-6 text-slate-900 dark:text-white group-hover:text-blue-700 dark:group-hover:text-blue-400 transition-colors line-clamp-2 h-14">{{ course.title }}</h4>
|
||||
<div class="flex items-center justify-between pt-4 border-t border-slate-50 dark:border-white/5">
|
||||
<span class="text-xs font-bold text-slate-700 dark:text-slate-400 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-slate-600 dark:text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<q-icon name="schedule" size="14px" />
|
||||
{{ course.duration }}
|
||||
</span>
|
||||
<span class="px-4 py-2 bg-blue-600 text-white text-xs font-bold rounded-lg hover:bg-blue-700 transition-colors shadow-md">{{ $t('menu.viewDetails') }}</span>
|
||||
|
|
@ -116,48 +116,6 @@ onMounted(async () => {
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-premium {
|
||||
background: #ffffff;
|
||||
border-radius: 2.5rem;
|
||||
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-premium:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
:global(.dark) .card-premium {
|
||||
background: #1e293b;
|
||||
box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
:global(.dark) .card-premium:hover {
|
||||
box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
:global(.app-main) {
|
||||
background-color: #ffffff;
|
||||
color: #1e293b;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
:global(.dark) :global(.app-main) {
|
||||
background-color: #0f172a;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.glass-bright {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
|
|
@ -167,26 +125,4 @@ onMounted(async () => {
|
|||
animation: slide-up 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
@apply px-4 py-1.5 rounded-full font-black uppercase tracking-widest border;
|
||||
}
|
||||
.status-success {
|
||||
@apply bg-emerald-500/10 text-emerald-400 border-emerald-500/30;
|
||||
}
|
||||
.status-warning {
|
||||
@apply bg-amber-500/10 text-amber-400 border-amber-500/30;
|
||||
}
|
||||
|
||||
:global(.app-main) {
|
||||
background-color: #ffffff;
|
||||
color: #1e293b;
|
||||
min-height: 100vh;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
:global(.dark) :global(.app-main) {
|
||||
background-color: #0f172a;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -131,12 +131,12 @@ const validCourseId = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-8 max-w-6xl mx-auto">
|
||||
<div class="page-container">
|
||||
|
||||
|
||||
<!-- Page Header & Filters -->
|
||||
<div class="flex flex-col items-start mb-12">
|
||||
<h1 class="text-3xl font-extrabold text-slate-900 dark:text-white mb-8">{{ $t('sidebar.myCourses') }}</h1>
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
||||
<h1 class="text-3xl font-black text-slate-900 dark:text-white">{{ $t('sidebar.myCourses') }}</h1>
|
||||
|
||||
<!-- Filter Tabs -->
|
||||
<div class="flex gap-2">
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ onMounted(async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="profile-page mx-auto px-4 py-8 max-w-4xl lg:max-w-5xl">
|
||||
<div class="page-container">
|
||||
|
||||
<div class="flex items-center justify-between mb-8 md:mb-10">
|
||||
<div class="flex items-center gap-4">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue