Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
8dfbace
#86et9akm3 - Enhance profile settings page with new fields. Update lo…
SomiVista Jul 11, 2025
6e36912
#86et9akm3 - Update profile settings page to include a checkbox for d…
SomiVista Jul 11, 2025
82fc7d5
#86et9akm3 - Implement profile picture upload functionality and enhan…
SomiVista Jul 11, 2025
e48bb75
#86et9akm3 - management: Restore profile navigation item, remove unus…
SomiVista Jul 13, 2025
141bd8b
#86et9akm3 - Implement profile update and image upload functionality …
SomiVista Jul 13, 2025
53c14cf
#86et9akm3 - Update profile functionality to support new data structu…
SomiVista Jul 14, 2025
c924a40
Remove console log statements from Mixpanel plugin to clean up code a…
navidshad Jul 14, 2025
88e8a1f
#86et9akm3 - Update user details section for better responsiveness an…
SomiVista Jul 16, 2025
f3a875b
#86et9akm3 - Enhance profile settings page layout and responsiveness.…
SomiVista Jul 16, 2025
9e527da
#86et9akm3 - Update profile settings page to include 'Coming soon' la…
SomiVista Jul 17, 2025
5288ef7
Refactor Mixpanel plugin to remove unnecessary console log statements…
navidshad Jul 19, 2025
a7a01d2
Merge branch 'main' into dev
navidshad Jul 21, 2025
4ecf543
#86et9akm3 - Update profile navigation and functionality: Change 'Mem…
navidshad Jul 26, 2025
f29ff13
#86et9akm3 - Update @codebridger/lib-vue-components dependency to ver…
navidshad Jul 26, 2025
43f3419
Merge branch 'dev' into CU-86et9akm3_Implement-User-Profile-Settings_…
navidshad Jul 26, 2025
3096b36
Merge pull request #23 from codebridger/CU-86et9akm3_Implement-User-P…
navidshad Jul 26, 2025
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
20 changes: 20 additions & 0 deletions .github/pr-description-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
> **Important**: Generate a PR description, Only include the sections listed below. Do not add any extra sections, titles, or content beyond what is specified in this template.

**🏷️ PR Title**: <!-- Concise, descriptive title for the pull request -->

## 📋 Summary
<!-- Brief overview of what this PR accomplishes -->

## 🔗 Related Tasks
<!-- Task IDs with links and descriptions from commit messages -->
<!-- Format: [linked task id] - [task description] -->
<!-- Example 1: [#12345](https://app.clickup.com/t/12345) - Implement user authentication -->
<!-- Example 2: [CU-34234](https://app.clickup.com/t/34234) - the description for this task -->
<!-- Note: Merge duplicate task IDs and combine their descriptions into a single task entry -->

## 📝 Additional Details
<!-- Any relevant extra information for reviewers -->

## 📜 Commit List
<!-- List of commit titles in this PR with links -->
<!-- Format: [commit sha](commit-url) [commit title] -->
67 changes: 67 additions & 0 deletions .github/workflows/auto-pr-description.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# https://github.com/vblagoje/pr-auto
# How to use:
# 1. Create a new pull request and save it.
# 2. Wait for the workflow `Generate PR Description` to finish.
# 3. Then copy past the generated title for the PR title.

name: Generate PR Description

on:
pull_request:
types: [opened, synchronize]

env:
TEMPLATE_FILE_PATH: .github/pr-description-template.md

jobs:
generate-description:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Read template content
id: read-template
run: |
content=$(cat ${{ env.TEMPLATE_FILE_PATH }} | sed ':a;N;$!ba;s/\n/\\n/g')
echo "template_content=$content" >> $GITHUB_OUTPUT

- name: Get Commit Messages
id: commit_messages
run: |
git fetch origin ${{ github.event.pull_request.base.ref }}
COMMITS=$(git log --oneline origin/${{ github.event.pull_request.base.ref }}..${{ github.event.pull_request.head.sha }})
echo "commits=$(echo "$COMMITS" | jq -sRr @uri)" >> $GITHUB_OUTPUT
echo "=== COMMIT MESSAGES ==="
echo "$COMMITS"
echo "=== END COMMIT MESSAGES ==="

- name: Generate PR Description
id: generate_desc
uses: navidshad/pull-request-description@master
with:
api_key: ${{ secrets.OPENAI_API_KEY_FOR_PR_DESC_GENERATOR }}
prompt: ${{ steps.read-template.outputs.template_content }}
git_diff: ${{ steps.commit_messages.outputs.commits }}
model: gpt-4.1-mini-2025-04-14

- name: Update PR Description
uses: actions/github-script@v6
env:
PR_DESCRIPTION: ${{ steps.generate_desc.outputs.description }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
await github.rest.pulls.update({
owner,
repo,
pull_number: context.payload.pull_request.number,
body: process.env.PR_DESCRIPTION
});
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
Loading