feat: Initialize core frontend application structure, including layouts, authentication pages, and common UI components.

This commit is contained in:
supalerk-ar66 2026-01-26 09:27:31 +07:00
parent ae84e7e879
commit 69eb60f901
16 changed files with 1178 additions and 1396 deletions

View file

@ -2,7 +2,7 @@
/**
* @file AppHeader.vue
* @description The main header for the authenticated application dashboard.
* Includes sidebar toggle, branding, search functionality, and user menu.
* Uses Quasar QToolbar.
*/
defineProps<{
@ -14,35 +14,68 @@ const emit = defineEmits<{
/** Emitted when the hamburger menu is clicked */
toggleSidebar: []
}>()
const searchText = ref('')
</script>
<template>
<header class="app-header transition-colors" style="background-color: var(--bg-surface); border-bottom: 1px solid var(--border-color);">
<!-- Branding & Toggle -->
<div class="flex items-center gap-2">
<NuxtLink to="/dashboard" style="font-weight: 800; color: var(--primary); font-size: 20px;">
e-Learning
</NuxtLink>
<q-toolbar class="bg-white text-slate-800 border-b border-gray-100 h-16 px-4">
<!-- Mobile Menu Toggle -->
<q-btn
flat
round
dense
icon="menu"
@click="emit('toggleSidebar')"
class="md:hidden mr-2"
aria-label="Menu"
/>
<!-- 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 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 search-input"
bg-color="slate-50"
>
<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-3">
<!-- Search Bar (Optional) -->
<div v-if="showSearch !== false" class="relative hidden-mobile" style="width: 300px;">
<input
type="text"
class="input-field"
:placeholder="$t('menu.searchCourses')"
style="padding-left: 36px;"
>
<span style="position: absolute; left: 12px; top: 10px; color: var(--text-secondary);">🔍</span>
</div>
<!-- Language Switcher (Left of Avatar) -->
<div class="flex items-center gap-2">
<!-- Language Switcher -->
<LanguageSwitcher />
<!-- User Profile Dropdown -->
<UserMenu />
</div>
</header>
</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

@ -2,107 +2,79 @@
/**
* @file AppSidebar.vue
* @description Sidebar navigation for the authenticated dashboard.
* Includes navigation links and responsive behaviors.
* Uses Quasar QList for structure.
*/
const route = useRoute();
const { isAuthenticated } = useAuth(); // Optional if you need auth state
const isSidebarOpen = defineModel<boolean>("open"); // Controlled by layout
const { t } = useI18n()
const navItems = computed(() => [
{
to: "/dashboard",
label: t('sidebar.overview'),
icon: "M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z",
icon: "dashboard", // Using Material Icons names where possible or SVG paths
isSvg: false
},
{
to: "/dashboard/my-courses",
label: t('sidebar.myCourses'),
icon: "M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253",
icon: "school",
isSvg: false
},
{
to: "/browse/discovery",
label: t('sidebar.browseCourses'),
icon: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z",
icon: "explore",
isSvg: false
},
{
to: "/dashboard/announcements",
label: t('sidebar.announcements'),
icon: "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9",
icon: "campaign",
isSvg: false
},
{
to: "/dashboard/profile",
label: t('sidebar.profile'),
icon: "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z",
icon: "person",
isSvg: false
},
]);
const isActive = (path: string) => {
if (path === "/dashboard") return route.path === "/dashboard";
return route.path.startsWith(path);
};
const closeSidebar = () => {
isSidebarOpen.value = false;
};
</script>
<template>
<div>
<!-- Desktop & Mobile Sidebar -->
<aside
class="app-sidebar transition-all duration-300"
:class="{ open: isSidebarOpen }"
>
<div class="flex flex-col h-full">
<!-- Menu Items -->
<div class="px-2 py-4 space-y-1">
<NuxtLink
v-for="item in navItems"
:key="item.to"
:to="item.to"
class="nav-item"
:class="{ active: isActive(item.to) }"
@click="closeSidebar"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="item.icon"
/>
</svg>
<span>{{ item.label }}</span>
</NuxtLink>
</div>
<div class="flex flex-col h-full bg-white">
<!-- Branding Area (Optional if not in Header) -->
<q-list padding class="text-slate-600 flex-grow">
<!-- Footer / Version -->
<div class="mt-auto p-4 text-xs text-center opacity-50">
<p>e-Learning v0.1.0</p>
<p>&copy; 2026</p>
</div>
</div>
</aside>
<q-item
v-for="item in navItems"
:key="item.to"
:to="item.to"
clickable
v-ripple
active-class="text-primary bg-blue-50 font-bold"
class="rounded-r-full mr-2"
>
<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>
<!-- Mobile Overlay -->
<div
class="sidebar-overlay"
:class="{ show: isSidebarOpen }"
@click="closeSidebar"
/>
<!-- 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 scoped>
/* Sidebar styles are mainly handled by global main.css (.app-sidebar) */
/* This component structure ensures it hooks into the .app-shell grid correctly */
/* Custom styles if needed */
</style>

View file

@ -1,198 +0,0 @@
<script setup lang="ts">
/**
* @file LandingFooter.vue
* @description The main footer for the public landing pages.
* Contains site links, social media icons, and app download buttons.
*/
</script>
<template>
<footer class="landing-footer">
<div class="container">
<!-- Main Footer Grid -->
<div class="footer-grid">
<!-- Column 1: Brand & Social Media -->
<div class="footer-brand">
<div class="flex items-center gap-2 mb-4">
<div class="logo-box">E</div>
<span class="font-bold text-xl" style="color: white;">E-Learning System</span>
</div>
<p class="text-sm text-slate-400 mb-6">แพลตฟอรมการเรยนออนไลนสำหรบทกษแหงอนาคต</p>
<div class="flex gap-4 social-icons">
<!-- Placeholder icons -->
<a href="#" class="social-icon">f</a>
<a href="#" class="social-icon">IG</a>
<a href="#" class="social-icon">Y</a>
</div>
</div>
<!-- Column 2: Services Links -->
<div class="footer-column">
<h4 class="font-bold mb-4" style="color: white;">บรการทงหมด</h4>
<ul class="footer-links">
<li><a href="#">คอรสเรยนออนไลน</a></li>
<li><a href="#">หลกสตร Onsite</a></li>
<li><a href="#">หลกสตร Class</a></li>
<li><a href="#">สำหรบองคกร</a></li>
</ul>
</div>
<!-- Column 3: About Links -->
<div class="footer-column">
<h4 class="font-bold mb-4" style="color: white;">เกยวกบเรา</h4>
<ul class="footer-links">
<li><a href="#">แพกเกจรายป</a></li>
<li><a href="#">เสนทางการเรยน</a></li>
<li><a href="#">ดระดบทกษะ <span class="badge-beta">Beta</span></a></li>
<li><a href="#">บทความ</a></li>
<li><a href="#">คำถามทพบบอย</a></li>
</ul>
</div>
<!-- Column 4: Join Us Links -->
<div class="footer-column">
<h4 class="font-bold mb-4" style="color: white;">วมงานกบเรา</h4>
<ul class="footer-links">
<li><a href="#">สมครงาน</a></li>
<li><a href="#">สมครเป Affiliate</a></li>
<li><a href="#">สมครเปนผสอน</a></li>
<li><a href="#">เกยวกบเรา</a></li>
<li><a href="#">ดตอเรา</a></li>
</ul>
</div>
<!-- Column 5: App Download & Contact -->
<div class="footer-column">
<h4 class="font-bold mb-4" style="color: white;">ดาวนโหลดแอปพลเคช</h4>
<div class="flex flex-col gap-2">
<button class="app-btn">
<span>🍎</span> App Store
</button>
<button class="app-btn">
<span></span> Google Play
</button>
</div>
<h4 class="font-bold mt-6 mb-4" style="color: white;">ปรกษาการเรยน</h4>
<button class="btn btn-success w-full">
<span>💬</span> เพมเพอน
</button>
</div>
</div>
<!-- Footer Bottom: Copyright & Legal -->
<div class="footer-bottom">
<p class="text-xs text-slate-400">© Copyright 2019-2026 LIKE ME X CO.,LTD All rights reserved.</p>
<div class="flex gap-4 text-xs text-slate-400">
<a href="#">อตกลงการใชบรการ</a>
<span>|</span>
<a href="#">นโยบายความเปนสวนต</a>
<span>|</span>
<a href="#">นโยบายการคนเง</a>
</div>
</div>
</div>
</footer>
</template>
<style scoped>
/* Main Footer Structure */
.landing-footer {
background-color: #080b14; /* Deep Navy Background */
padding: 60px 0 20px;
border-top: 1px solid rgba(255, 255, 255, 0.05); /* Subtle border */
margin-top: auto;
color: #94a3b8;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
/* 5-Column Grid Layout */
.footer-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr; /* First column is wider for branding */
gap: 40px;
margin-bottom: 60px;
}
.logo-box {
width: 32px;
height: 32px;
background: #eff6ff;
color: #3b82f6;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
}
.footer-links li {
margin-bottom: 12px;
}
.footer-links a {
color: #94a3b8;
font-size: 14px;
transition: color 0.2s;
}
.footer-links a:hover {
color: #3b82f6;
}
.badge-beta {
background: #fee2e2;
color: #ef4444;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
margin-left: 4px;
}
.app-btn {
background: #000;
color: white;
border: none;
border-radius: 8px;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
cursor: pointer;
}
.footer-bottom {
border-top: 1px solid rgba(255, 255, 255, 0.05);
padding-top: 20px;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
}
/* Responsive Breakpoints */
@media (max-width: 1024px) {
.footer-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.footer-grid {
grid-template-columns: 1fr;
gap: 32px;
}
.footer-bottom {
flex-direction: column;
align-items: center;
text-align: center;
}
}
</style>

View file

@ -82,13 +82,11 @@ onMounted(() => {
</template>
<style scoped>
/* Fixed header to stay on top */
/* Header content */
.landing-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
transition: all 0.3s ease;
}
/* Glassmorphism Effect for Scrolled Header */

View file

@ -1,29 +1,34 @@
<script setup lang="ts">
const route = useRoute()
const navItems = [
{ to: '/dashboard', icon: '🏠', label: 'หน้าหลัก' },
{ to: '/browse/discovery', icon: '🔍', label: 'รายการคอร์ส' },
{ to: '/dashboard/my-courses', icon: '📚', label: 'คอร์สของฉัน' }
{ to: '/dashboard', icon: 'dashboard', label: 'หน้าหลัก' },
{ to: '/browse/discovery', icon: 'explore', label: 'รายการคอร์ส' },
{ to: '/dashboard/my-courses', icon: 'school', label: 'คอร์สของฉัน' }
]
const isActive = (path: string) => {
if (path === '/') return route.path === '/'
return route.path.startsWith(path)
}
</script>
<template>
<nav class="mobile-nav">
<NuxtLink
<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"
class="mobile-nav-item"
:class="{ active: isActive(item.to) }"
>
<span>{{ item.icon }}</span>
<span>{{ item.label }}</span>
</NuxtLink>
</nav>
: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>