Skip to content

Commit 9f11b97

Browse files
authored
Merge pull request #24 from codebridger/dev
Enhance User Profile Settings: Navigation, Update Logic, Upload, and UI Improvements
2 parents ca08b96 + 3096b36 commit 9f11b97

10 files changed

Lines changed: 343 additions & 20 deletions

File tree

.github/pr-description-template.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
> **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.
2+
3+
**🏷️ PR Title**: <!-- Concise, descriptive title for the pull request -->
4+
5+
## 📋 Summary
6+
<!-- Brief overview of what this PR accomplishes -->
7+
8+
## 🔗 Related Tasks
9+
<!-- Task IDs with links and descriptions from commit messages -->
10+
<!-- Format: [linked task id] - [task description] -->
11+
<!-- Example 1: [#12345](https://app.clickup.com/t/12345) - Implement user authentication -->
12+
<!-- Example 2: [CU-34234](https://app.clickup.com/t/34234) - the description for this task -->
13+
<!-- Note: Merge duplicate task IDs and combine their descriptions into a single task entry -->
14+
15+
## 📝 Additional Details
16+
<!-- Any relevant extra information for reviewers -->
17+
18+
## 📜 Commit List
19+
<!-- List of commit titles in this PR with links -->
20+
<!-- Format: [commit sha](commit-url) [commit title] -->
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# https://github.com/vblagoje/pr-auto
2+
# How to use:
3+
# 1. Create a new pull request and save it.
4+
# 2. Wait for the workflow `Generate PR Description` to finish.
5+
# 3. Then copy past the generated title for the PR title.
6+
7+
name: Generate PR Description
8+
9+
on:
10+
pull_request:
11+
types: [opened, synchronize]
12+
13+
env:
14+
TEMPLATE_FILE_PATH: .github/pr-description-template.md
15+
16+
jobs:
17+
generate-description:
18+
runs-on: ubuntu-latest
19+
permissions:
20+
contents: read
21+
pull-requests: write
22+
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 0
28+
29+
- name: Read template content
30+
id: read-template
31+
run: |
32+
content=$(cat ${{ env.TEMPLATE_FILE_PATH }} | sed ':a;N;$!ba;s/\n/\\n/g')
33+
echo "template_content=$content" >> $GITHUB_OUTPUT
34+
35+
- name: Get Commit Messages
36+
id: commit_messages
37+
run: |
38+
git fetch origin ${{ github.event.pull_request.base.ref }}
39+
COMMITS=$(git log --oneline origin/${{ github.event.pull_request.base.ref }}..${{ github.event.pull_request.head.sha }})
40+
echo "commits=$(echo "$COMMITS" | jq -sRr @uri)" >> $GITHUB_OUTPUT
41+
echo "=== COMMIT MESSAGES ==="
42+
echo "$COMMITS"
43+
echo "=== END COMMIT MESSAGES ==="
44+
45+
- name: Generate PR Description
46+
id: generate_desc
47+
uses: navidshad/pull-request-description@master
48+
with:
49+
api_key: ${{ secrets.OPENAI_API_KEY_FOR_PR_DESC_GENERATOR }}
50+
prompt: ${{ steps.read-template.outputs.template_content }}
51+
git_diff: ${{ steps.commit_messages.outputs.commits }}
52+
model: gpt-4.1-mini-2025-04-14
53+
54+
- name: Update PR Description
55+
uses: actions/github-script@v6
56+
env:
57+
PR_DESCRIPTION: ${{ steps.generate_desc.outputs.description }}
58+
with:
59+
github-token: ${{ secrets.GITHUB_TOKEN }}
60+
script: |
61+
const { owner, repo } = context.repo;
62+
await github.rest.pulls.update({
63+
owner,
64+
repo,
65+
pull_number: context.payload.pull_request.number,
66+
body: process.env.PR_DESCRIPTION
67+
});

frontend/components/partial/ProfileButton.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727
class="dark:hover:text-white"
2828
@click="
2929
close();
30-
goToMembership();
30+
goToProfileSettings();
3131
"
3232
>
33-
<Icon name="IconDollarSignCircle" class="h-4.5 w-4.5 shrink-0 ltr:mr-2 rtl:ml-2" />
34-
{{ t('subscription.title') }}
33+
<Icon name="IconUser" class="h-4.5 w-4.5 shrink-0 ltr:mr-2 rtl:ml-2" />
34+
{{ t('profile.profile') }}
3535
</a>
3636
</li>
3737
<li class="cursor-pointer border-t border-white-light dark:border-white-light/10">
@@ -88,7 +88,7 @@
8888
logout();
8989
}
9090
91-
function goToMembership() {
92-
router.push('/settings/subscription');
91+
function goToProfileSettings() {
92+
router.push('/settings/profile');
9393
}
9494
</script>

frontend/locales/en.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,15 @@
174174
"start-new-session": "Start New Session"
175175
},
176176
"profile": {
177-
"profile": "Profile"
177+
"profile": "Profile",
178+
"receive-daily-practice-email-reminders": "Receive daily practice email reminders?",
179+
"full-name": "Full Name",
180+
"email": "Email",
181+
"password": "Password",
182+
"reset-password": "Reset Password",
183+
"uploading": "Uploading...",
184+
"profile-updated": "Profile updated successfully",
185+
"profile-update-failed": "Failed to update profile"
178186
},
179187
"billing": {
180188
"billing": "Billing",
@@ -209,5 +217,7 @@
209217
"logout": "Log out",
210218
"sign-out": "Sign Out",
211219
"confirm-sign-out": "Confirm Sign Out",
212-
"confirm-sign-out-message": "Are you sure you want to sign out of your account?"
220+
"confirm-sign-out-message": "Are you sure you want to sign out of your account?",
221+
"save-changes": "Save Changes",
222+
"coming-soon": "Coming soon"
213223
}

frontend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"vue-tsc": "^0.40.4"
3131
},
3232
"dependencies": {
33-
"@codebridger/lib-vue-components": "^1.19.0",
33+
"@codebridger/lib-vue-components": "^1.20.0",
3434
"@modular-rest/client": "^1.14.0",
3535
"@pinia/nuxt": "^0.4.6",
3636
"apexcharts": "^4.4.0",
@@ -46,4 +46,4 @@
4646
"vue3-popper": "^1.5.0",
4747
"yup": "^1.6.1"
4848
}
49-
}
49+
}
Lines changed: 209 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,111 @@
11
<template>
22
<div class="p-4">
3-
<div class="flex gap-8">
4-
<h1 class="text-2xl font-bold">{{ t('profile.profile') }}</h1>
5-
</div>
3+
<!-- <h1 class="mb-6 text-lg font-bold">{{ t('profile.profile') }}</h1> -->
4+
<section class="mx-auto max-w-3xl space-y-4 p-4">
5+
<!-- User Details Section -->
6+
<Card class="shadow-none">
7+
<form @submit.prevent="handleSubmit" class="flex flex-col items-center p-4">
8+
<!-- Avatar Section -->
9+
<div class="mb-6">
10+
<div class="group relative mx-auto h-24 w-24 md:h-32 md:w-32">
11+
<img
12+
:src="profilePhotoPreview"
13+
alt="Profile Photo"
14+
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"
15+
/>
16+
<div
17+
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"
18+
>
19+
<svg
20+
class="h-6 w-6 text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100"
21+
fill="none"
22+
stroke="currentColor"
23+
viewBox="0 0 24 24"
24+
>
25+
<path
26+
stroke-linecap="round"
27+
stroke-linejoin="round"
28+
stroke-width="2"
29+
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"
30+
></path>
31+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
32+
</svg>
33+
</div>
34+
<input
35+
ref="fileInput"
36+
type="file"
37+
accept="image/*"
38+
class="absolute inset-0 h-full w-full cursor-pointer opacity-0"
39+
@change="handleFileUpload"
40+
:disabled="true"
41+
/>
42+
</div>
43+
<div v-if="isUploading" class="mt-2 text-center text-sm text-gray-500">
44+
{{ t('profile.uploading') }}
45+
</div>
46+
</div>
47+
48+
<!-- User Info Section -->
49+
<div class="mb-6 text-center">
50+
<h2 class="mb-1 text-xl font-bold text-gray-800">{{ name || t('profile.full-name') }}</h2>
51+
</div>
52+
53+
<!-- Personal Information Section -->
54+
<div class="w-full">
55+
<div class="mb-4 border-t border-gray-200"></div>
56+
<h3 class="mb-4 text-lg font-bold text-gray-800">Personal Information</h3>
57+
58+
<!-- Form Fields Section -->
59+
60+
<div class="mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2">
61+
<Input
62+
:label="t('profile.full-name')"
63+
v-model="name"
64+
type="text"
65+
:placeholder="t('profile.full-name')"
66+
required
67+
:disabled="isSubmitting"
68+
/>
69+
<Input :label="t('profile.email')" :model-value="email" type="email" :placeholder="t('profile.email')" required disabled />
70+
</div>
71+
<div class="pointer-events-none mb-6">
72+
<CheckboxInput
73+
v-for="option in options"
74+
:key="option.value"
75+
v-model="selectedValues[option.value]"
76+
:text="`${option.label} (${t('coming-soon')})`"
77+
:value="option.value"
78+
:disabled="true"
79+
/>
80+
</div>
81+
82+
<div class="border-t border-gray-200 pt-4">
83+
<div class="flex justify-end">
84+
<Button
85+
:label="t('save-changes')"
86+
type="submit"
87+
size="lg"
88+
:shadow="!(isSubmitting || isUploading || !hasChanges)"
89+
color="primary"
90+
:loading="isSubmitting"
91+
:disabled="isSubmitting || isUploading || !hasChanges"
92+
/>
93+
</div>
94+
</div>
95+
</div>
96+
</form>
97+
</Card>
98+
</section>
699
</div>
7100
</template>
8101

9102
<script lang="ts" setup>
103+
import { ref, computed, onMounted } from 'vue';
104+
import { useProfileStore } from '~/stores/profile';
105+
import { Card, Input, Button, CheckboxInput } from '@codebridger/lib-vue-components/elements.ts';
106+
import { toastSuccess, toastError } from '@codebridger/lib-vue-components/toast.ts';
107+
108+
const profileStore = useProfileStore();
10109
const { t } = useI18n();
11110
12111
definePageMeta({
@@ -15,4 +114,111 @@
15114
// @ts-ignore
16115
middleware: ['auth'],
17116
});
117+
118+
const name = ref(profileStore.userDetail?.name || '');
119+
const email = ref(profileStore.email);
120+
const profilePicture = ref(profileStore.profilePicture);
121+
const selectedFile = ref<File | null>(null);
122+
const filePreviewUrl = ref<string | null>(null);
123+
const options = [{ label: t('profile.receive-daily-practice-email-reminders'), value: 'dailyReminders' }];
124+
const selectedValues = ref<Record<string, boolean>>({});
125+
const isSubmitting = ref(false);
126+
const isUploading = ref(false);
127+
const fileInput = ref<HTMLInputElement>();
128+
129+
// Track initial values for change detection
130+
const initialName = ref('');
131+
const initialSelectedValues = ref<Record<string, boolean>>({});
132+
const hasChanges = computed(() => {
133+
const nameChanged = name.value !== initialName.value;
134+
const preferencesChanged = JSON.stringify(selectedValues.value) !== JSON.stringify(initialSelectedValues.value);
135+
const fileChanged = selectedFile.value !== null;
136+
137+
return nameChanged || preferencesChanged || fileChanged;
138+
});
139+
140+
const handleFileUpload = (event: Event) => {
141+
const target = event.target as HTMLInputElement;
142+
const file = target.files?.[0];
143+
144+
if (file) {
145+
selectedFile.value = file;
146+
147+
// Create preview URL for immediate UI feedback
148+
const reader = new FileReader();
149+
reader.onload = (e) => {
150+
filePreviewUrl.value = e.target?.result as string;
151+
};
152+
reader.readAsDataURL(file);
153+
}
154+
};
155+
156+
const profilePhotoPreview = computed(() => {
157+
// Show local preview if available
158+
if (filePreviewUrl.value) {
159+
return filePreviewUrl.value;
160+
}
161+
return profilePicture.value || '/assets/images/user.png';
162+
});
163+
164+
const handleSubmit = async () => {
165+
try {
166+
isSubmitting.value = true;
167+
168+
const profileData: {
169+
name?: string;
170+
profileImage?: File;
171+
preferences?: Record<string, boolean>;
172+
} = {};
173+
174+
// Include name if it has changed
175+
if (name.value !== profileStore.userDetail?.name) {
176+
profileData.name = name.value;
177+
}
178+
179+
// Include profile image if a new one was selected
180+
if (selectedFile.value) {
181+
profileData.profileImage = selectedFile.value;
182+
}
183+
184+
// Include preferences
185+
profileData.preferences = { ...selectedValues.value };
186+
187+
// Call the store function
188+
await profileStore.updateProfile(profileData);
189+
190+
toastSuccess(t('profile.profile-updated'));
191+
} catch (error) {
192+
console.error('Error updating profile:', error);
193+
toastError(t('profile.profile-update-failed'));
194+
} finally {
195+
isSubmitting.value = false;
196+
}
197+
};
198+
199+
onMounted(async () => {
200+
//profile
201+
await profileStore.getProfileInfo();
202+
203+
// Initialize initial values for change detection
204+
initialName.value = profileStore.userDetail?.name || '';
205+
initialSelectedValues.value = { ...selectedValues.value };
206+
});
18207
</script>
208+
209+
<style scoped>
210+
/* Override hover styles for disabled checkboxes */
211+
:deep(.checkbox-input:has(input:disabled)) {
212+
cursor: not-allowed;
213+
}
214+
215+
:deep(.checkbox-input:has(input:disabled) .checkbox-label) {
216+
cursor: not-allowed;
217+
pointer-events: none;
218+
}
219+
220+
:deep(.checkbox-input:has(input:disabled):hover .checkbox-label) {
221+
color: inherit;
222+
text-decoration: none;
223+
}
224+
</style>

frontend/pages/statistic.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="p-6">
2+
<div class="p-4">
33
<h1 class="mb-6 text-lg font-bold">{{ t('statistic.your-statistic') }}</h1>
44
<section class="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-4">
55
<Card class="col-span-1 rounded-md shadow-none lg:col-span-3">

0 commit comments

Comments
 (0)