feat: add curriculum sidebar component for course navigation and announcements

This commit is contained in:
supalerk-ar66 2026-02-04 11:56:20 +07:00
parent ff841c7638
commit 827b016114
5 changed files with 1 additions and 329 deletions

View file

@ -8,7 +8,7 @@
const props = defineProps<{
modelValue: boolean; // Sidebar open state (v-model)
courseData: any;
currentLessonId: number;
currentLessonId?: number;
isLoading: boolean;
hasUnreadAnnouncements: boolean;
}>();

View file

@ -1,70 +0,0 @@
<script setup lang="ts">
/**
* @file AppHeader.vue
* @description The main header for the authenticated application dashboard.
* Uses Quasar QToolbar.
*/
defineProps<{
/** Controls visibility of the search bar */
showSearch?: boolean
}>()
const searchText = ref('')
</script>
<template>
<q-toolbar class="bg-transparent text-slate-800 dark:text-white h-16 px-4">
<!-- Mobile Menu Toggle -->
<!-- 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">
E
</div>
<span class="font-bold text-xl text-blue-600 dark:text-blue-400 hidden xs:block">e-Learning</span>
</div>
<q-space />
<!-- Center Search (Optional) -->
<div v-if="showSearch !== false" class="hidden md:block w-1/3 max-w-md mx-4">
<q-input
dense
outlined
rounded
v-model="searchText"
:placeholder="$t('menu.searchCourses')"
class="bg-slate-50 dark:bg-slate-700/50 search-input"
bg-color="transparent"
>
<template v-slot:prepend>
<q-icon name="search" class="text-slate-400" />
</template>
</q-input>
</div>
<q-space />
<!-- Right Actions -->
<div class="flex items-center gap-2">
<!-- Language Switcher -->
<LanguageSwitcher />
<!-- User Profile Dropdown -->
<UserMenu />
</div>
</q-toolbar>
</template>
<style scoped>
.search-input :deep(.q-field__control) {
border-radius: 9999px; /* Full rounded */
}
.search-input :deep(.q-field__control:before) {
border-color: #e2e8f0; /* slate-200 */
}
</style>

View file

@ -1,90 +0,0 @@
<script setup lang="ts">
/**
* @file AppSidebar.vue
* @description Sidebar navigation for the authenticated dashboard.
* Uses Quasar QList for structure.
*/
const { t } = useI18n()
const navItems = computed(() => [
{
to: "/dashboard",
label: t('sidebar.overview'),
icon: "dashboard", // Using Material Icons names where possible or SVG paths
isSvg: false
},
{
to: "/dashboard/my-courses",
label: t('sidebar.myCourses'),
icon: "school",
isSvg: false
},
{
to: "/browse/discovery",
label: t('sidebar.browseCourses'),
icon: "explore",
isSvg: false
},
{
to: "/dashboard/announcements",
label: t('sidebar.announcements'),
icon: "campaign",
isSvg: false
},
{
to: "/dashboard/profile",
label: t('sidebar.profile'),
icon: "person",
isSvg: false
},
]);
</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">
<q-item
v-for="item in navItems"
:key="item.to"
:to="item.to"
clickable
v-ripple
class="rounded-r-full mr-2 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-white/5"
>
<q-item-section avatar>
<q-icon :name="item.icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ 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>&copy; 2026</p>
</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;
}
/* 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 */
}
</style>

View file

@ -1,134 +0,0 @@
<script setup lang="ts">
/**
* @file LandingHeader.vue
* @description The main header for the public landing pages.
* Features a transparent background that becomes solid/glass upon scrolling.
*/
// Track scrolling state to adjust header styling
const isScrolled = ref(false)
const { isAuthenticated } = useAuth()
onMounted(() => {
// Add scroll listener to toggle 'isScrolled' class
window.addEventListener('scroll', () => {
isScrolled.value = window.scrollY > 20
})
})
</script>
<template>
<!--
Header Container
- Transitions between transparent and glass effect based on scroll.
-->
<header
class="landing-header transition-all duration-300"
:class="[isScrolled ? 'h-16 glass-nav shadow-lg' : 'h-24 bg-transparent']"
>
<div class="container h-full flex items-center justify-between">
<!--
Left Section: Logo & Desktop Navigation
-->
<div class="flex items-center gap-12">
<!-- Logo -->
<NuxtLink to="/" class="flex items-center gap-3 group">
<div class="logo-box bg-blue-600 text-white font-black rounded-xl w-10 h-10 flex items-center justify-center 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 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">E-Learning</span>
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-[0.2em] leading-none mt-1">Platform</span>
</div>
</NuxtLink>
<!-- Desktop Links -->
<nav class="hidden md:block">
<ul class="flex items-center gap-8 text-sm font-bold">
<li>
<NuxtLink to="/browse" class="text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-white transition-colors relative group">
{{ $t('landing.allCourses') }}
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-blue-600 transition-all group-hover:w-full"/>
</NuxtLink>
</li>
<li>
<NuxtLink to="/browse/discovery" class="text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-white transition-colors relative group">
{{ $t('landing.discovery') }}
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-blue-600 transition-all group-hover:w-full"/>
</NuxtLink>
</li>
</ul>
</nav>
</div>
<!--
Right Section: Action Buttons (Login/Register or Dashboard)
-->
<div class="flex items-center gap-4">
<template v-if="!isAuthenticated">
<NuxtLink to="/auth/login" class="text-sm font-bold text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-white px-6 py-2.5 rounded-xl border border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/5 transition-all">{{ $t('auth.login') }}</NuxtLink>
<NuxtLink to="/auth/register" class="btn-primary-premium shadow-lg shadow-blue-600/20">
{{ $t('auth.getStarted') }}
</NuxtLink>
</template>
<template v-else>
<NuxtLink to="/dashboard" class="btn-primary-premium shadow-lg shadow-blue-600/20">
{{ $t('landing.goToDashboard') }}
</NuxtLink>
</template>
</div>
</div>
</header>
</template>
<style scoped>
/* Header content */
.landing-header {
width: 100%;
z-index: 100;
transition: all 0.3s ease;
}
/* Glassmorphism Effect for Scrolled Header */
.glass-nav {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
:global(.dark) .glass-nav {
background: rgba(15, 23, 42, 0.85);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.container {
max-width: 1440px;
margin: 0 auto;
padding: 0 24px;
}
/* Premium Primary Button Styling */
.btn-primary-premium {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
font-size: 0.875rem;
font-weight: 800;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary-premium:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px -4px rgba(37, 99, 235, 0.5);
}
@media (max-width: 768px) {
.container {
padding: 0 16px;
}
}
</style>

View file

@ -1,34 +0,0 @@
<script setup lang="ts">
const navItems = [
{ to: '/dashboard', icon: 'dashboard', label: 'หน้าหลัก' },
{ to: '/browse/discovery', icon: 'explore', label: 'รายการคอร์ส' },
{ to: '/dashboard/my-courses', icon: 'school', label: 'คอร์สของฉัน' }
]
</script>
<template>
<q-tabs
indicator-color="primary"
active-color="primary"
class="bg-white text-slate-500 shadow-up-1"
align="justify"
dense
>
<q-route-tab
v-for="item in navItems"
:key="item.to"
:to="item.to"
:icon="item.icon"
:label="item.label"
no-caps
class="py-2"
/>
</q-tabs>
</template>
<style scoped>
/* Optional shadow for better separation */
.shadow-up-1 {
box-shadow: 0 -1px 3px rgba(0,0,0,0.05);
}
</style>