feat: Implement initial core features including course browsing, authentication, user dashboard, and internationalization.

This commit is contained in:
supalerk-ar66 2026-02-24 11:12:26 +07:00
parent 031ca5c984
commit 797e3db644
19 changed files with 401 additions and 399 deletions

View file

@ -216,7 +216,6 @@ onMounted(async () => {
@click="toggleEdit(false)"
/>
<div class="flex items-start gap-4">
<span class="w-1.5 h-10 md:h-12 bg-blue-600 rounded-full shadow-lg shadow-blue-500/50 mt-1 flex-shrink-0"></span>
<div>
<h1 class="text-3xl md:text-4xl font-black text-slate-900 dark:text-white leading-tight">
{{ (isHydrated && isEditing) ? $t('profile.editProfile') : $t('profile.myProfile') }}
@ -250,7 +249,7 @@ onMounted(async () => {
<div v-else class="max-w-4xl mx-auto pb-20">
<!-- VIEW MODE: Premium Card with Banner -->
<div v-if="!isEditing" class="bg-white dark:bg-[#1e293b] border border-slate-200 dark:border-slate-700/50 rounded-3xl shadow-xl dark:shadow-none overflow-hidden fade-in min-h-[500px] flex flex-col transition-colors duration-300">
<div v-if="!isEditing" class="bg-white dark:!bg-slate-900/50 border border-slate-200 dark:border-white/5 rounded-3xl shadow-xl dark:shadow-none overflow-hidden fade-in min-h-[500px] flex flex-col transition-colors duration-300">
<!-- Identity Header (Banner & Avatar) -->
<div class="relative">
@ -269,7 +268,7 @@ onMounted(async () => {
:first-name="userData.firstName"
:last-name="userData.lastName"
size="140"
class="border-[6px] border-white dark:border-[#1e293b] shadow-2xl rounded-[2.5rem] bg-white dark:bg-slate-800 transition-colors duration-300"
class="border-[6px] border-white dark:border-slate-900 shadow-2xl rounded-[2.5rem] bg-white dark:bg-slate-800 transition-colors duration-300"
/>
</div>
@ -278,11 +277,11 @@ onMounted(async () => {
{{ userData.firstName }} {{ userData.lastName }}
</h2>
<div class="flex flex-wrap items-center justify-center md:justify-start gap-4">
<div class="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-bold bg-slate-50 dark:bg-slate-800/50 px-3 py-1.5 rounded-xl border border-slate-100 dark:border-slate-700">
<div class="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-bold bg-slate-50 dark:bg-slate-900/50 px-3 py-1.5 rounded-xl border border-slate-100 dark:border-white/5">
<q-icon name="alternate_email" size="xs" class="text-blue-500" />
<span class="text-sm">{{ userData.email }}</span>
</div>
<div class="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-bold bg-slate-50 dark:bg-slate-800/50 px-3 py-1.5 rounded-xl border border-slate-100 dark:border-slate-700">
<div class="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-bold bg-slate-50 dark:bg-slate-900/50 px-3 py-1.5 rounded-xl border border-slate-100 dark:border-white/5">
<q-icon name="verified_user" size="xs" :class="userData.emailVerifiedAt ? 'text-green-500' : 'text-amber-500'" />
<span class="text-sm">{{ userData.emailVerifiedAt ? $t('profile.emailVerified') : $t('profile.verifyEmail') }}</span>
</div>
@ -325,7 +324,7 @@ onMounted(async () => {
<div v-else class="fade-in">
<!-- Tab Selector -->
<div class="flex justify-center mb-8">
<div class="bg-white dark:bg-[#1e293b] p-1.5 rounded-2xl flex items-center gap-1 border border-slate-200 dark:border-slate-700 shadow-sm">
<div class="bg-white dark:!bg-slate-900/50 p-1.5 rounded-2xl flex items-center gap-1 border border-slate-200 dark:border-white/5 shadow-sm">
<button
@click="activeTab = 'general'"
class="px-6 md:px-8 py-3 rounded-xl font-black text-xs uppercase tracking-widest transition-all flex items-center gap-2"
@ -336,7 +335,7 @@ onMounted(async () => {
<button
@click="activeTab = 'security'"
class="px-6 md:px-8 py-3 rounded-xl font-black text-xs uppercase tracking-widest transition-all flex items-center gap-2"
:class="activeTab === 'security' ? 'bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 shadow-sm scale-100' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 scale-95 opacity-70'"
:class="activeTab === 'security' ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 shadow-sm scale-100' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 scale-95 opacity-70'"
>
<q-icon name="lock_open" size="18px" /> {{ $t('profile.security') }}
</button>
@ -345,7 +344,7 @@ onMounted(async () => {
<!-- Edit Content -->
<div class="max-w-3xl mx-auto">
<div v-if="activeTab === 'general'" class="bg-white dark:bg-[#1e293b] border border-slate-200 dark:border-slate-700/50 rounded-3xl shadow-xl dark:shadow-none p-6 md:p-10">
<div v-if="activeTab === 'general'" class="bg-white dark:!bg-slate-900/50 border border-slate-200 dark:border-white/5 rounded-3xl shadow-xl dark:shadow-none p-6 md:p-10">
<ProfileEditForm
v-model="userData"
:loading="isProfileSaving"
@ -355,7 +354,7 @@ onMounted(async () => {
@verify="handleSendVerifyEmail"
/>
</div>
<div v-else class="bg-white dark:bg-[#1e293b] border border-slate-200 dark:border-slate-700/50 rounded-3xl shadow-xl dark:shadow-none p-6 md:p-10">
<div v-else class="bg-white dark:!bg-slate-900/50 border border-slate-200 dark:border-white/5 rounded-3xl shadow-xl dark:shadow-none p-6 md:p-10">
<PasswordChangeForm
v-model="passwordForm"
:loading="isPasswordSaving"