feat: implement course discovery page with category filtering sidebar and course detail view.

This commit is contained in:
supalerk-ar66 2026-02-11 12:45:57 +07:00
parent 088bbf4b1b
commit efb50a1ddb
3 changed files with 80 additions and 201 deletions

View file

@ -23,129 +23,80 @@ const getLocalizedText = (text: any) => {
const currentLocale = locale.value as 'th' | 'en';
return text[currentLocale] || text.th || text.en || "";
};
const toggleCategory = (id: number) => {
const current = [...props.modelValue];
const index = current.indexOf(id);
if (index > -1) {
current.splice(index, 1);
} else {
current.push(id);
}
emit("update:modelValue", current);
};
</script>
<template>
<div class="category-sidebar-root border rounded-2xl overflow-hidden shadow-sm">
<q-expansion-item
expand-separator
:label="`${$t('discovery.categoryTitle')} (${modelValue.length})`"
class="category-sidebar-expansion"
header-class="category-sidebar-header"
default-opened
>
<q-list class="category-sidebar-list border-t">
<q-item
v-for="cat in showAllCategories ? categories : categories.slice(0, 4)"
:key="cat.id"
clickable
v-ripple
dense
class="category-item"
<div class="bg-white/60 dark:!bg-[#0f172a]/60 backdrop-blur-3xl rounded-[2.5rem] border border-slate-200 dark:border-white/10 shadow-xl shadow-blue-900/5 overflow-hidden transition-all duration-500">
<q-expansion-item
default-opened
expand-separator
header-class="py-6 px-8"
class="group"
>
<q-item-section avatar>
<q-checkbox
:model-value="modelValue"
@update:model-value="(val) => emit('update:modelValue', val)"
:val="cat.id"
color="primary"
dense
class="checkbox-visible"
/>
</q-item-section>
<q-item-section>
<q-item-label class="category-item-label text-sm font-medium">
{{ getLocalizedText(cat.name) }}
</q-item-label>
</q-item-section>
</q-item>
<template v-slot:header>
<div class="flex items-center gap-4 w-full">
<div class="w-10 h-10 rounded-xl bg-blue-600/10 dark:bg-blue-400/10 flex items-center justify-center">
<q-icon name="category" class="text-blue-600 dark:text-blue-400" size="20px" />
</div>
<div class="flex-1">
<div class="text-lg font-black text-slate-900 dark:!text-white leading-none mb-1">{{ $t('discovery.categoryTitle') }}</div>
<div class="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-slate-500">{{ modelValue.length }} Selectable</div>
</div>
</div>
</template>
<!-- Show More/Less Button -->
<q-item
v-if="categories.length > 4"
clickable
v-ripple
@click="showAllCategories = !showAllCategories"
class="show-more-item font-bold text-sm"
>
<q-item-section>
<div class="flex items-center gap-1 text-blue-600 dark:text-blue-400">
{{ showAllCategories ? $t('discovery.showLess') : $t('discovery.showMore') }}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
:class="showAllCategories ? 'rotate-180' : ''"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
<div class="px-3 pb-6 pt-2">
<div class="space-y-1">
<div
v-for="cat in showAllCategories ? categories : categories.slice(0, 5)"
:key="cat.id"
class="relative group/item"
>
<div
class="flex items-center gap-3 px-5 py-3 rounded-2xl transition-all cursor-pointer border border-transparent"
:class="modelValue.includes(cat.id) ? 'bg-blue-600/5 dark:bg-blue-400/5 border-blue-100 dark:border-blue-400/20 shadow-sm shadow-blue-500/5' : 'hover:bg-slate-50 dark:hover:bg-white/5'"
@click="toggleCategory(cat.id)"
>
<q-checkbox
:model-value="modelValue.includes(cat.id)"
@update:model-value="toggleCategory(cat.id)"
color="primary"
keep-color
dense
/>
<span
class="text-sm font-bold transition-colors truncate"
:class="modelValue.includes(cat.id) ? 'text-blue-700 dark:text-blue-400' : 'text-slate-700 dark:text-slate-400 group-hover/item:text-slate-900 dark:group-hover/item:text-white'"
>
{{ getLocalizedText(cat.name) }}
</span>
<!-- Active Indicator Dot -->
<div v-if="modelValue.includes(cat.id)" class="ml-auto w-1.5 h-1.5 rounded-full bg-blue-600 dark:bg-blue-400 shadow-lg shadow-blue-500/50"></div>
</div>
</div>
</div>
<!-- Show More/Less Action -->
<div
v-if="categories.length > 5"
@click="showAllCategories = !showAllCategories"
class="mt-4 mx-2 py-3 px-5 rounded-xl border border-dashed border-slate-200 dark:border-white/10 flex items-center justify-center gap-2 cursor-pointer text-slate-500 dark:text-slate-400 hover:border-blue-300 dark:hover:border-blue-500/40 hover:text-blue-600 dark:hover:text-blue-400 transition-all font-bold text-xs uppercase tracking-widest"
>
{{ showAllCategories ? $t('discovery.showLess') : $t('discovery.showMore') }}
<q-icon :name="showAllCategories ? 'keyboard_arrow_up' : 'keyboard_arrow_down'" size="16px" />
</div>
</div>
</q-item-section>
</q-item>
</q-list>
</q-expansion-item>
</div>
</q-expansion-item>
</div>
</template>
<style scoped>
/* Base Styles - Rely on main.css variables */
.category-sidebar-root {
background-color: var(--bg-surface);
border-color: var(--border-color);
transition: all 0.3s ease;
}
/* Internal Quasar components color management */
:deep(.category-sidebar-header),
.category-sidebar-list {
background-color: var(--bg-surface) !important;
color: var(--text-main) !important;
}
/* Labels and Icons - use var(--text-main) but force opacity */
:deep(.q-item__label),
:deep(.q-icon) {
color: var(--text-main) !important;
opacity: 1 !important;
}
.category-item-label {
color: var(--text-main);
}
/* ✅ DARK MODE SPECIFIC OVERRIDES using :global(.dark) as recommended */
:global(.dark) .category-item-label {
color: #f8fafc !important; /* Forces slate-50 in dark mode */
}
:global(.dark) :deep(.category-sidebar-header *) {
color: #f8fafc !important;
opacity: 1 !important;
}
/* Hover effects */
.category-item:hover {
background-color: rgba(0, 0, 0, 0.03);
}
:global(.dark) .category-item:hover {
background-color: rgba(255, 255, 255, 0.05) !important;
}
/* Checkbox Label handling */
:deep(.q-checkbox__label) {
color: var(--text-secondary) !important;
}
/* Show More button fix */
.show-more-item {
background-color: transparent !important;
}
</style>