Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions frontend/components/partial/ProfileButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
class="dark:hover:text-white"
@click="
close();
goToMembership();
goToProfileSettings();
"
>
<Icon name="IconDollarSignCircle" class="h-4.5 w-4.5 shrink-0 ltr:mr-2 rtl:ml-2" />
{{ t('subscription.title') }}
<Icon name="IconUser" class="h-4.5 w-4.5 shrink-0 ltr:mr-2 rtl:ml-2" />
{{ t('profile.profile') }}
</a>
</li>
<li class="cursor-pointer border-t border-white-light dark:border-white-light/10">
Expand Down Expand Up @@ -88,7 +88,7 @@
logout();
}

function goToMembership() {
router.push('/settings/subscription');
function goToProfileSettings() {
router.push('/settings/profile');
}
</script>
14 changes: 12 additions & 2 deletions frontend/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,15 @@
"start-new-session": "Start New Session"
},
"profile": {
"profile": "Profile"
"profile": "Profile",
"receive-daily-practice-email-reminders": "Receive daily practice email reminders?",
"full-name": "Full Name",
"email": "Email",
"password": "Password",
"reset-password": "Reset Password",
"uploading": "Uploading...",
"profile-updated": "Profile updated successfully",
"profile-update-failed": "Failed to update profile"
},
"billing": {
"billing": "Billing",
Expand Down Expand Up @@ -209,5 +217,7 @@
"logout": "Log out",
"sign-out": "Sign Out",
"confirm-sign-out": "Confirm Sign Out",
"confirm-sign-out-message": "Are you sure you want to sign out of your account?"
"confirm-sign-out-message": "Are you sure you want to sign out of your account?",
"save-changes": "Save Changes",
"coming-soon": "Coming soon"
}
4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"vue-tsc": "^0.40.4"
},
"dependencies": {
"@codebridger/lib-vue-components": "^1.19.0",
"@codebridger/lib-vue-components": "^1.20.0",
"@modular-rest/client": "^1.14.0",
"@pinia/nuxt": "^0.4.6",
"apexcharts": "^4.4.0",
Expand All @@ -46,4 +46,4 @@
"vue3-popper": "^1.5.0",
"yup": "^1.6.1"
}
}
}
212 changes: 209 additions & 3 deletions frontend/pages/settings/profile.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,111 @@
<template>
<div class="p-4">
<div class="flex gap-8">
<h1 class="text-2xl font-bold">{{ t('profile.profile') }}</h1>
</div>
<!-- <h1 class="mb-6 text-lg font-bold">{{ t('profile.profile') }}</h1> -->
<section class="mx-auto max-w-3xl space-y-4 p-4">
<!-- User Details Section -->
<Card class="shadow-none">
<form @submit.prevent="handleSubmit" class="flex flex-col items-center p-4">
<!-- Avatar Section -->
<div class="mb-6">
<div class="group relative mx-auto h-24 w-24 md:h-32 md:w-32">
<img
:src="profilePhotoPreview"
alt="Profile Photo"
class="h-24 w-24 cursor-pointer rounded-full border border-gray-200 object-cover transition-opacity group-hover:opacity-80 md:h-32 md:w-32"
/>
<div
class="absolute inset-0 flex items-center justify-center rounded-full bg-black bg-opacity-0 transition-all duration-200 group-hover:bg-opacity-30"
>
<svg
class="h-6 w-6 text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</div>
<input
ref="fileInput"
type="file"
accept="image/*"
class="absolute inset-0 h-full w-full cursor-pointer opacity-0"
@change="handleFileUpload"
:disabled="true"
/>
</div>
<div v-if="isUploading" class="mt-2 text-center text-sm text-gray-500">
{{ t('profile.uploading') }}
</div>
</div>

<!-- User Info Section -->
<div class="mb-6 text-center">
<h2 class="mb-1 text-xl font-bold text-gray-800">{{ name || t('profile.full-name') }}</h2>
</div>

<!-- Personal Information Section -->
<div class="w-full">
<div class="mb-4 border-t border-gray-200"></div>
<h3 class="mb-4 text-lg font-bold text-gray-800">Personal Information</h3>

<!-- Form Fields Section -->

<div class="mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2">
<Input
:label="t('profile.full-name')"
v-model="name"
type="text"
:placeholder="t('profile.full-name')"
required
:disabled="isSubmitting"
/>
<Input :label="t('profile.email')" :model-value="email" type="email" :placeholder="t('profile.email')" required disabled />
</div>
<div class="pointer-events-none mb-6">
<CheckboxInput
v-for="option in options"
:key="option.value"
v-model="selectedValues[option.value]"
:text="`${option.label} (${t('coming-soon')})`"
:value="option.value"
:disabled="true"
/>
</div>

<div class="border-t border-gray-200 pt-4">
<div class="flex justify-end">
<Button
:label="t('save-changes')"
type="submit"
size="lg"
:shadow="!(isSubmitting || isUploading || !hasChanges)"
color="primary"
:loading="isSubmitting"
:disabled="isSubmitting || isUploading || !hasChanges"
/>
</div>
</div>
</div>
</form>
</Card>
</section>
</div>
</template>

<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import { useProfileStore } from '~/stores/profile';
import { Card, Input, Button, CheckboxInput } from '@codebridger/lib-vue-components/elements.ts';
import { toastSuccess, toastError } from '@codebridger/lib-vue-components/toast.ts';

const profileStore = useProfileStore();
const { t } = useI18n();

definePageMeta({
Expand All @@ -15,4 +114,111 @@
// @ts-ignore
middleware: ['auth'],
});

const name = ref(profileStore.userDetail?.name || '');
const email = ref(profileStore.email);
const profilePicture = ref(profileStore.profilePicture);
const selectedFile = ref<File | null>(null);
const filePreviewUrl = ref<string | null>(null);
const options = [{ label: t('profile.receive-daily-practice-email-reminders'), value: 'dailyReminders' }];
const selectedValues = ref<Record<string, boolean>>({});
const isSubmitting = ref(false);
const isUploading = ref(false);
const fileInput = ref<HTMLInputElement>();

// Track initial values for change detection
const initialName = ref('');
const initialSelectedValues = ref<Record<string, boolean>>({});
const hasChanges = computed(() => {
const nameChanged = name.value !== initialName.value;
const preferencesChanged = JSON.stringify(selectedValues.value) !== JSON.stringify(initialSelectedValues.value);
const fileChanged = selectedFile.value !== null;

return nameChanged || preferencesChanged || fileChanged;
});

const handleFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];

if (file) {
selectedFile.value = file;

// Create preview URL for immediate UI feedback
const reader = new FileReader();
reader.onload = (e) => {
filePreviewUrl.value = e.target?.result as string;
};
reader.readAsDataURL(file);
}
};

const profilePhotoPreview = computed(() => {
// Show local preview if available
if (filePreviewUrl.value) {
return filePreviewUrl.value;
}
return profilePicture.value || '/assets/images/user.png';
});

const handleSubmit = async () => {
try {
isSubmitting.value = true;

const profileData: {
name?: string;
profileImage?: File;
preferences?: Record<string, boolean>;
} = {};

// Include name if it has changed
if (name.value !== profileStore.userDetail?.name) {
profileData.name = name.value;
}

// Include profile image if a new one was selected
if (selectedFile.value) {
profileData.profileImage = selectedFile.value;
}

// Include preferences
profileData.preferences = { ...selectedValues.value };

// Call the store function
await profileStore.updateProfile(profileData);

toastSuccess(t('profile.profile-updated'));
} catch (error) {
console.error('Error updating profile:', error);
toastError(t('profile.profile-update-failed'));
} finally {
isSubmitting.value = false;
}
};

onMounted(async () => {
//profile
await profileStore.getProfileInfo();

// Initialize initial values for change detection
initialName.value = profileStore.userDetail?.name || '';
initialSelectedValues.value = { ...selectedValues.value };
});
</script>

<style scoped>
/* Override hover styles for disabled checkboxes */
:deep(.checkbox-input:has(input:disabled)) {
cursor: not-allowed;
}

:deep(.checkbox-input:has(input:disabled) .checkbox-label) {
cursor: not-allowed;
pointer-events: none;
}

:deep(.checkbox-input:has(input:disabled):hover .checkbox-label) {
color: inherit;
text-decoration: none;
}
</style>
2 changes: 1 addition & 1 deletion frontend/pages/statistic.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="p-6">
<div class="p-4">
<h1 class="mb-6 text-lg font-bold">{{ t('statistic.your-statistic') }}</h1>
<section class="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-4">
<Card class="col-span-1 rounded-md shadow-none lg:col-span-3">
Expand Down
23 changes: 23 additions & 0 deletions frontend/stores/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,28 @@ export const useProfileStore = defineStore('profile', () => {
.catch((error) => null);
}

function updateProfile(profileData: { name?: string; profileImage?: File; preferences?: Record<string, boolean> }) {
if (!profileData.name) {
return Promise.resolve(userDetail.value);
}

return dataProvider
.updateOne({
database: DATABASE.USER_CONTENT,
collection: COLLECTIONS.PROFILE,
query: {
refId: authentication.user?.id,
},
update: {
name: profileData.name,
},
})
.then((res) => {
userDetail.value!.name = profileData.name || '';
return res;
});
}

return {
authUser,
userDetail,
Expand All @@ -106,5 +128,6 @@ export const useProfileStore = defineStore('profile', () => {
getProfileInfo,
loginWithLastSession,
bootstrap,
updateProfile,
};
});
8 changes: 4 additions & 4 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,10 @@
dependencies:
mime "^3.0.0"

"@codebridger/lib-vue-components@^1.19.0":
version "1.19.0"
resolved "https://npm.pkg.github.com/download/@codebridger/lib-vue-components/1.19.0/29d9e6a35318149450b61ab9812e0c879a8c8eb1#29d9e6a35318149450b61ab9812e0c879a8c8eb1"
integrity sha512-zWUPUNpu/kb1iOVM0zF0Egj1sKZ2/Uy26cGFxznSjzM4eLqy0UeyWnxx7OGf3BHcWJWyDCS2X3ZtDuWJXnJVHQ==
"@codebridger/lib-vue-components@^1.20.0":
version "1.20.0"
resolved "https://npm.pkg.github.com/download/@codebridger/lib-vue-components/1.20.0/bd7f4c0f755eb572b2345f5580b7f59b3f8c9935#bd7f4c0f755eb572b2345f5580b7f59b3f8c9935"
integrity sha512-u5cSYCagjpsbAdGQh/zDXV82nzdOpXoBOYmeGPlT36fy/D2RnQCGoy8diczXbmVOkaNX/0tF0+jzn8dcRdckbQ==
dependencies:
"@headlessui/vue" "^1.7.23"
"@storybook/builder-vite" "^8.4.5"
Expand Down