feat: Implement initial e-learning platform frontend including landing page, course discovery, dashboard, and foundational UI components with i18n.
This commit is contained in:
parent
5b9cf72046
commit
3a9da1007b
17 changed files with 1631 additions and 1524 deletions
|
|
@ -1,79 +1,145 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* @file AppSidebar.vue
|
||||
* @description Sidebar navigation for the authenticated dashboard.
|
||||
* Uses Quasar QList for structure.
|
||||
* @description เมนูด้านข้างสำหรับการนำทาง (Sidebar Navigation)
|
||||
*/
|
||||
|
||||
const { sidebarItems } = useNavItems()
|
||||
// 1. นำเข้า Composables และไลบรารี
|
||||
import { useQuasar } from 'quasar'
|
||||
const { t } = useI18n()
|
||||
const navItems = sidebarItems
|
||||
const route = useRoute()
|
||||
const $q = useQuasar()
|
||||
const { logout } = useAuth()
|
||||
const { isDark } = useThemeMode()
|
||||
|
||||
// 2. กำหนดข้อมูลเมนูหลัก (Main Navigation)
|
||||
const menuItems = computed(() => [
|
||||
{ to: '/dashboard', icon: 'grid_view', label: t('sidebar.overview') },
|
||||
{ to: '/dashboard/my-courses', icon: 'book', label: t('sidebar.myCourses') }
|
||||
])
|
||||
|
||||
const handleNavigate = (path: string) => {
|
||||
if (import.meta.client) {
|
||||
window.location.href = path
|
||||
}
|
||||
// 3. กำหนดข้อมูลเมนูบัญชี (Account Navigation)
|
||||
const accountItems = computed(() => [
|
||||
{ to: '/dashboard/profile', icon: 'settings', label: t('userMenu.settings') }
|
||||
])
|
||||
|
||||
// 4. ฟังก์ชันการออกจากระบบ (Logout Function)
|
||||
const handleLogout = () => {
|
||||
$q.dialog({
|
||||
title: t('auth.logoutConfirmTitle'),
|
||||
message: t('auth.logoutConfirmMessage'),
|
||||
cancel: {
|
||||
flat: true,
|
||||
color: isDark.value ? 'grey-4' : 'grey-7',
|
||||
label: t('common.cancel')
|
||||
},
|
||||
ok: {
|
||||
flat: false,
|
||||
color: 'red-500',
|
||||
label: t('auth.logout'),
|
||||
unelevated: true
|
||||
},
|
||||
dark: isDark.value,
|
||||
class: 'p-4 rounded-2xl text-slate-900 dark:text-white',
|
||||
persistent: true
|
||||
}).onOk(async () => {
|
||||
await logout()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full bg-transparent border-r border-slate-200 dark:border-slate-800">
|
||||
<div class="flex flex-col h-full bg-white dark:!bg-[#04091a] px-4 py-6 border-r border-slate-100 dark:border-slate-800">
|
||||
|
||||
<!-- Logo Section -->
|
||||
<div class="flex items-center gap-3 px-2 mb-10 transition-transform active:scale-95 cursor-pointer" @click="navigateTo('/dashboard')">
|
||||
<div class="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center shadow-lg shadow-blue-500/20">
|
||||
<q-icon name="school" color="white" size="24px" />
|
||||
</div>
|
||||
<span class="text-[22px] font-black tracking-tight text-slate-800 dark:text-white">EduLearn</span>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Items -->
|
||||
<q-list padding class="text-slate-600 dark:text-slate-400 flex-grow px-3 pt-6">
|
||||
<q-item
|
||||
v-for="item in navItems"
|
||||
<!-- Main Navigation -->
|
||||
<div class="space-y-1 mb-8">
|
||||
<NuxtLink
|
||||
v-for="item in menuItems"
|
||||
:key="item.to"
|
||||
clickable
|
||||
v-ripple
|
||||
@click="handleNavigate(item.to)"
|
||||
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') }"
|
||||
:to="item.to"
|
||||
class="flex items-center gap-4 px-4 py-3 rounded-2xl transition-all group relative nav-item"
|
||||
:class="route.path === item.to ? 'active' : 'text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-slate-200'"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="item.icon" size="22px" />
|
||||
</q-item-section>
|
||||
<q-icon :name="item.icon" size="24px" class="transition-colors" />
|
||||
<span class="font-bold text-[15px]">{{ item.label }}</span>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label class="font-bold text-sm">{{ $t(item.labelKey) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<!-- Active Indicator -->
|
||||
<div v-if="route.path === item.to" class="absolute left-0 top-1/2 -translate-y-1/2 w-1.5 h-6 bg-blue-600 rounded-r-full shadow-[2px_0_8px_rgba(37,99,235,0.4)]"></div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Account Section -->
|
||||
<div class="px-4 mb-4">
|
||||
<span class="text-[12px] font-bold text-slate-400 uppercase tracking-widest">{{ $t('sidebar.accountGroup') }}</span>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<NuxtLink
|
||||
v-for="item in accountItems"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
class="flex items-center gap-4 px-4 py-3 rounded-2xl transition-all group nav-item"
|
||||
:class="route.path === item.to ? 'active' : 'text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-slate-200'"
|
||||
>
|
||||
<q-icon :name="item.icon" size="24px" />
|
||||
<span class="font-bold text-[15px]">{{ item.label }}</span>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="w-full flex items-center gap-4 px-4 py-3 rounded-2xl transition-all text-red-500 hover:bg-red-50 dark:hover:bg-red-900/10 font-bold text-[15px] group"
|
||||
>
|
||||
<q-icon name="logout" size="24px" class="group-hover:translate-x-1 transition-transform" />
|
||||
<span>{{ $t('userMenu.logout') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
||||
<!-- Promo Card -->
|
||||
<div class="mt-auto p-5 rounded-[2rem] bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-800 relative overflow-hidden group">
|
||||
<div class="relative z-10">
|
||||
<h4 class="font-black text-slate-800 dark:text-white text-sm mb-1">{{ $t('sidebar.promoTitle') }}</h4>
|
||||
<p class="text-[11px] text-slate-500 dark:text-slate-400 mb-4">{{ $t('sidebar.promoSubtitle') }}</p>
|
||||
<q-btn
|
||||
unelevated
|
||||
class="full-width rounded-xl bg-blue-600 hover:bg-blue-700 text-white font-bold py-2.5 no-caps transition-all active:scale-95 text-xs shadow-md shadow-blue-500/20"
|
||||
@click="navigateTo('/browse/discovery')"
|
||||
>
|
||||
{{ $t('sidebar.learnMore') }}
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<!-- Subtle background decoration -->
|
||||
<div class="absolute -right-2 -bottom-2 w-16 h-16 bg-blue-500/5 rounded-full blur-xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-item--active {
|
||||
background: #eff6ff !important; /* blue-50 */
|
||||
color: #1d4ed8 !important; /* blue-700 */
|
||||
position: relative;
|
||||
.nav-item.active {
|
||||
background: #EFF6FF;
|
||||
color: #2563EB;
|
||||
}
|
||||
|
||||
.sidebar-item--active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 15%;
|
||||
height: 70%;
|
||||
width: 4px;
|
||||
background: #2563eb;
|
||||
border-radius: 0 4px 4px 0;
|
||||
.dark .nav-item.active {
|
||||
background: rgba(37,99,235,0.1);
|
||||
color: #60A5FA;
|
||||
}
|
||||
|
||||
/* Dark Mode Active State Enhancement */
|
||||
.dark .sidebar-item--active {
|
||||
background: rgba(59, 130, 246, 0.12) !important;
|
||||
color: #60a5fa !important; /* blue-400 */
|
||||
.nav-item.active .q-icon {
|
||||
color: #2563EB;
|
||||
}
|
||||
|
||||
.dark .sidebar-item--active .q-icon {
|
||||
color: #60a5fa !important; /* blue-400 */
|
||||
}
|
||||
|
||||
.dark .sidebar-item--active::before {
|
||||
background: #3b82f6;
|
||||
.dark .nav-item.active .q-icon {
|
||||
color: #60A5FA;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue