feat: Implement core application layout, global styling, dark mode, and essential UI components.
This commit is contained in:
parent
84e4d478c7
commit
a2ce1d79a2
7 changed files with 87 additions and 67 deletions
|
|
@ -7,8 +7,8 @@
|
||||||
=========================== */
|
=========================== */
|
||||||
:root {
|
:root {
|
||||||
/* Colors - Light Mode Default */
|
/* Colors - Light Mode Default */
|
||||||
--bg-body: #f8fafc; /* Light Background */
|
--bg-body: #f8fafc;
|
||||||
--bg-surface: #ffffff; /* White Card Background */
|
--bg-surface: #ffffff;
|
||||||
--text-main: #0f172a; /* Dark Text */
|
--text-main: #0f172a; /* Dark Text */
|
||||||
--text-secondary: #64748b; /* Secondary Text */
|
--text-secondary: #64748b; /* Secondary Text */
|
||||||
--primary: #3b82f6; /* Primary Blue */
|
--primary: #3b82f6; /* Primary Blue */
|
||||||
|
|
@ -60,9 +60,9 @@
|
||||||
.dark {
|
.dark {
|
||||||
--bg-body: #0f172a;
|
--bg-body: #0f172a;
|
||||||
--bg-surface: #1e293b; /* User requested specific color */
|
--bg-surface: #1e293b; /* User requested specific color */
|
||||||
--text-main: #f1f5f9;
|
--text-main: #f8fafc; /* text-slate-50: Brighter white for main text */
|
||||||
--text-secondary: #cbd5e1;
|
--text-secondary: #cbd5e1; /* text-slate-300: Lighter grey for secondary text */
|
||||||
--border-color: #334155;
|
--border-color: #334155; /* border-slate-700: Higher contrast border */
|
||||||
--neutral-50: #1e293b;
|
--neutral-50: #1e293b;
|
||||||
--neutral-100: #334155;
|
--neutral-100: #334155;
|
||||||
--neutral-200: #475569;
|
--neutral-200: #475569;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const searchText = ref('')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-toolbar class="bg-white text-slate-800 border-b border-gray-100 h-16 px-4">
|
<q-toolbar class="bg-transparent text-slate-800 dark:text-white h-16 px-4">
|
||||||
<!-- Mobile Menu Toggle -->
|
<!-- Mobile Menu Toggle -->
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
|
|
@ -27,7 +27,7 @@ const searchText = ref('')
|
||||||
dense
|
dense
|
||||||
icon="menu"
|
icon="menu"
|
||||||
@click="emit('toggleSidebar')"
|
@click="emit('toggleSidebar')"
|
||||||
class="md:hidden mr-2"
|
class="md:hidden mr-2 text-slate-700 dark:text-white"
|
||||||
aria-label="Menu"
|
aria-label="Menu"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ const searchText = ref('')
|
||||||
<div class="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-bold">
|
<div class="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-bold">
|
||||||
E
|
E
|
||||||
</div>
|
</div>
|
||||||
<span class="font-bold text-xl text-blue-600 hidden xs:block">e-Learning</span>
|
<span class="font-bold text-xl text-blue-600 dark:text-blue-400 hidden xs:block">e-Learning</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
@ -49,8 +49,8 @@ const searchText = ref('')
|
||||||
rounded
|
rounded
|
||||||
v-model="searchText"
|
v-model="searchText"
|
||||||
:placeholder="$t('menu.searchCourses')"
|
:placeholder="$t('menu.searchCourses')"
|
||||||
class="bg-slate-50 search-input"
|
class="bg-slate-50 dark:bg-slate-700/50 search-input"
|
||||||
bg-color="slate-50"
|
bg-color="transparent"
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-icon name="search" class="text-slate-400" />
|
<q-icon name="search" class="text-slate-400" />
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,10 @@ const navItems = computed(() => [
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full bg-white">
|
<div class="flex flex-col h-full bg-transparent">
|
||||||
<!-- Branding Area (Optional if not in Header) -->
|
<!-- Branding Area (Optional if not in Header) -->
|
||||||
|
|
||||||
<q-list padding class="text-slate-600 flex-grow">
|
<q-list padding class="text-slate-600 dark:text-slate-300 flex-grow">
|
||||||
|
|
||||||
|
|
||||||
<q-item
|
<q-item
|
||||||
|
|
@ -54,8 +54,7 @@ const navItems = computed(() => [
|
||||||
:to="item.to"
|
:to="item.to"
|
||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
active-class="text-primary bg-blue-50 font-bold"
|
class="rounded-r-full mr-2 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-white/5"
|
||||||
class="rounded-r-full mr-2"
|
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon :name="item.icon" />
|
<q-icon :name="item.icon" />
|
||||||
|
|
@ -75,6 +74,17 @@ const navItems = computed(() => [
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
/* Custom styles if needed */
|
/* 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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,44 @@
|
||||||
* @description User profile dropdown menu component using Quasar.
|
* @description User profile dropdown menu component using Quasar.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useAuth } from '~/composables/useAuth'
|
import { useAuth } from '~/composables/useAuth'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
|
|
||||||
const { currentUser, logout } = useAuth()
|
const { currentUser, logout } = useAuth()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const $q = useQuasar()
|
||||||
|
|
||||||
const isDarkMode = ref(false)
|
const isDarkMode = ref(false)
|
||||||
|
const isHydrated = ref(false)
|
||||||
|
|
||||||
// Sync Dark Mode state
|
const applyTheme = (value: boolean) => {
|
||||||
|
// Tailwind dark mode
|
||||||
|
document.documentElement.classList.toggle('dark', value)
|
||||||
|
|
||||||
|
// Quasar dark mode
|
||||||
|
$q.dark.set(value)
|
||||||
|
|
||||||
|
// persist
|
||||||
|
localStorage.setItem('theme', value ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDarkMode = (val: boolean) => {
|
||||||
|
isDarkMode.value = val
|
||||||
|
applyTheme(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync Dark Mode state on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (document.documentElement.classList.contains('dark')) {
|
isHydrated.value = true
|
||||||
isDarkMode.value = true
|
const savedTheme = localStorage.getItem('theme')
|
||||||
}
|
const preferredDark = window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false
|
||||||
})
|
|
||||||
|
|
||||||
// Watch for dark mode changes
|
isDarkMode.value = savedTheme
|
||||||
watch(isDarkMode, (val) => {
|
? savedTheme === 'dark'
|
||||||
if (val) {
|
: preferredDark
|
||||||
document.documentElement.classList.add('dark')
|
|
||||||
localStorage.setItem('theme', 'dark')
|
applyTheme(isDarkMode.value)
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark')
|
|
||||||
localStorage.setItem('theme', 'light')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// User Initials
|
// User Initials
|
||||||
|
|
@ -37,11 +52,6 @@ const userInitials = computed(() => {
|
||||||
return f + l
|
return f + l
|
||||||
})
|
})
|
||||||
|
|
||||||
const userName = computed(() => {
|
|
||||||
if (!currentUser.value) return 'User'
|
|
||||||
return `${currentUser.value.firstName} ${currentUser.value.lastName}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const menuItems = computed(() => [
|
const menuItems = computed(() => [
|
||||||
{ label: t('userMenu.home'), to: '/dashboard' },
|
{ label: t('userMenu.home'), to: '/dashboard' },
|
||||||
{ label: t('userMenu.courseList'), to: '/browse/discovery' },
|
{ label: t('userMenu.courseList'), to: '/browse/discovery' },
|
||||||
|
|
@ -64,42 +74,40 @@ const handleLogout = async () => {
|
||||||
<q-menu
|
<q-menu
|
||||||
anchor="bottom end"
|
anchor="bottom end"
|
||||||
self="top end"
|
self="top end"
|
||||||
class="rounded-2xl shadow-xl overflow-hidden border border-slate-100 dark:border-slate-700"
|
|
||||||
:offset="[0, 10]"
|
:offset="[0, 10]"
|
||||||
style="min-width: 240px; background-color: var(--bg-surface);"
|
content-class="bg-white dark:bg-slate-800 text-slate-900 dark:text-white rounded-2xl shadow-xl border border-slate-200/70 dark:border-white/10"
|
||||||
|
style="min-width: 240px;"
|
||||||
>
|
>
|
||||||
<div class="bg-[#1e293b] text-white">
|
<q-list class="py-2">
|
||||||
<!-- Optional header if needed, but per design keeping it simple list -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-list class="bg-[#1e293b] text-white py-2">
|
|
||||||
<q-item
|
<q-item
|
||||||
v-for="item in menuItems"
|
v-for="item in menuItems"
|
||||||
:key="item.label"
|
:key="item.label"
|
||||||
clickable
|
clickable
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="navigateTo(item.to)"
|
@click="navigateTo(item.to)"
|
||||||
class="hover:bg-white/10 transition-colors"
|
class="hover:bg-slate-100 dark:hover:bg-white/10 transition-colors"
|
||||||
>
|
>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label class="font-bold text-sm">{{ item.label }}</q-item-label>
|
<q-item-label class="font-bold text-sm">{{ item.label }}</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-item class="hover:bg-white/10 transition-colors">
|
<q-item class="hover:bg-slate-100 dark:hover:bg-white/10 transition-colors">
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label class="font-bold text-sm">{{ $t('userMenu.darkMode') }}</q-item-label>
|
<q-item-label class="font-bold text-sm">{{ $t('userMenu.darkMode') }}</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="isDarkMode"
|
v-if="isHydrated"
|
||||||
|
:model-value="isDarkMode"
|
||||||
|
@update:model-value="toggleDarkMode"
|
||||||
color="blue"
|
color="blue"
|
||||||
keep-color
|
keep-color
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-separator class="bg-white/10 my-1" />
|
<q-separator class="bg-slate-100 dark:bg-white/10 my-1" />
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
@ -116,7 +124,3 @@ const handleLogout = async () => {
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Override Quasar generic styles if necessary */
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,12 @@ const toggleLeftDrawer = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-layout view="hHh lpr lFf" class="bg-slate-50 font-inter">
|
<q-layout view="hHh lpr lFf" class="!bg-slate-50 dark:!bg-[#0f172a] !text-slate-900 dark:!text-white font-sans">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<q-header bordered class="bg-white text-slate-800">
|
<q-header
|
||||||
|
bordered
|
||||||
|
class="!bg-white dark:!bg-[#1e293b] !text-slate-900 dark:!text-white border-b border-slate-200 dark:border-slate-700"
|
||||||
|
>
|
||||||
<AppHeader @toggle-sidebar="toggleLeftDrawer" />
|
<AppHeader @toggle-sidebar="toggleLeftDrawer" />
|
||||||
</q-header>
|
</q-header>
|
||||||
|
|
||||||
|
|
@ -23,7 +26,7 @@ const toggleLeftDrawer = () => {
|
||||||
<q-drawer
|
<q-drawer
|
||||||
v-model="leftDrawerOpen"
|
v-model="leftDrawerOpen"
|
||||||
bordered
|
bordered
|
||||||
class="bg-white"
|
class="!bg-white dark:!bg-[#1e293b] !text-slate-900 dark:!text-white border-r border-slate-200 dark:border-slate-700"
|
||||||
:width="260"
|
:width="260"
|
||||||
>
|
>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
|
|
@ -37,7 +40,11 @@ const toggleLeftDrawer = () => {
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
|
|
||||||
<!-- Mobile Bottom Nav -->
|
<!-- Mobile Bottom Nav -->
|
||||||
<q-footer v-if="$q.screen.lt.md" bordered class="bg-white text-primary">
|
<q-footer
|
||||||
|
v-if="$q.screen.lt.md"
|
||||||
|
bordered
|
||||||
|
class="!bg-white dark:!bg-[#1e293b] text-primary border-t border-slate-200 dark:border-slate-700"
|
||||||
|
>
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
</q-footer>
|
</q-footer>
|
||||||
</q-layout>
|
</q-layout>
|
||||||
|
|
|
||||||
10
Frontend-Learner/plugins/theme.client.ts
Normal file
10
Frontend-Learner/plugins/theme.client.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
// Client-side only theme initialization to prevent flash of wrong theme
|
||||||
|
if (process.client) {
|
||||||
|
const saved = localStorage.getItem('theme')
|
||||||
|
const isDark = saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
|
||||||
|
// Apply class immediately for Tailwind
|
||||||
|
document.documentElement.classList.toggle('dark', isDark)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -27,18 +27,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: [
|
sans: ['var(--font-main)'],
|
||||||
'Prompt',
|
|
||||||
'Sarabun',
|
|
||||||
'Inter',
|
|
||||||
'-apple-system',
|
|
||||||
'BlinkMacSystemFont',
|
|
||||||
'Segoe UI',
|
|
||||||
'Roboto',
|
|
||||||
'Helvetica Neue',
|
|
||||||
'Arial',
|
|
||||||
'sans-serif'
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue