Skip to content

Commit b3e3530

Browse files
committed
feat: add user profile fields and improve form components
- Add address, contact_number, gender, and birth_date to UserProfile model - Update ProfileController to handle new profile fields - Add validation rules for new profile fields in ProfileUpdateRequest - Enhance Profile.vue with new form fields (address, contact, gender, birth date) - Update Select and Textarea components styling
1 parent 95ba9b4 commit b3e3530

7 files changed

Lines changed: 96 additions & 20 deletions

File tree

app/Http/Controllers/Settings/ProfileController.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public function edit(Request $request): Response
2121
return Inertia::render('settings/Profile', [
2222
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
2323
'status' => $request->session()->get('status'),
24+
'profile' => $request->user()->profile,
2425
]);
2526
}
2627

@@ -29,14 +30,36 @@ public function edit(Request $request): Response
2930
*/
3031
public function update(ProfileUpdateRequest $request): RedirectResponse
3132
{
32-
$request->user()->fill($request->validated());
33+
$validated = $request->validated();
34+
35+
// Update user models fields (name, email)
36+
$userFields = ['name', 'email'];
37+
$userData = array_intersect_key($validated, array_flip($userFields));
38+
39+
$request->user()->fill($userData);
3340

3441
if ($request->user()->isDirty('email')) {
3542
$request->user()->email_verified_at = null;
3643
}
3744

3845
$request->user()->save();
3946

47+
// Update UserProfile model fields
48+
$profileFields = [
49+
'address',
50+
'contact_number',
51+
'gender',
52+
'birth_date'
53+
];
54+
$profileData = array_intersect_key($validated, array_flip($profileFields));
55+
56+
if (! empty($profileData)) {
57+
$request->user()->profile()->updateOrCreate(
58+
['user_id' => $request->user()->id],
59+
$profileData
60+
);
61+
}
62+
4063
// Dynamically redirect based on the current route prefix
4164
$currentRouteName = $request->route()->getName();
4265
$routePrefix = str_starts_with($currentRouteName, 'admin.') ? 'admin.' : 'customer.';

app/Http/Requests/Settings/ProfileUpdateRequest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ public function rules(): array
2525
'max:255',
2626
Rule::unique(User::class)->ignore($this->user()->id),
2727
],
28+
'profile_picture' => ['nullable', 'image', 'max:2048'],
29+
'address' => ['nullable', 'string', 'max:500'],
30+
'contact_number' => ['nullable', 'string', 'max:20'],
31+
'gender' => ['nullable', 'string', 'in:male,female,other'],
32+
'birth_date' => ['nullable', 'date', 'before:today'],
2833
];
2934
}
3035
}

app/Models/UserProfile.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class UserProfile extends Model
1313
'address',
1414
'contact_number',
1515
'gender',
16+
'birth_date',
1617
];
1718

1819
/**

database/migrations/2025_11_28_162810_create_user_profiles_table.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public function up(): void
1818
$table->text('address')->nullable();
1919
$table->string('contact_number', 20)->nullable();
2020
$table->enum('gender', ['male', 'female'])->nullable();
21+
$table->date('birth_date')->nullable();
2122
$table->timestamps();
2223
});
2324
}

resources/js/components/Select.vue

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,26 @@ const normalizedOptions = computed(() => {
4545
</script>
4646

4747
<template>
48-
<Select v-model="modelValue" :required="required" :disabled="disabled">
49-
<SelectTrigger data-slot="select" :class="cn(
50-
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
51-
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
52-
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
53-
'data-[placeholder]:text-muted-foreground',
54-
'items-center justify-between [&>span]:truncate text-start',
55-
props.class,
56-
)">
57-
<SelectValue :placeholder="placeholder || 'Select an option'" />
58-
</SelectTrigger>
59-
<SelectContent>
60-
<SelectItem v-for="option in normalizedOptions" :key="option.value" :value="option.value">
61-
{{ option.label }}
62-
</SelectItem>
63-
</SelectContent>
64-
</Select>
48+
<div class="relative">
49+
<!-- Hidden input for form submission -->
50+
<input type="hidden" v-bind="$attrs" :value="modelValue || ''" />
51+
52+
<Select v-model="modelValue" :required="required" :disabled="disabled">
53+
<SelectTrigger data-slot="select" :class="cn(
54+
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
55+
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
56+
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
57+
'data-[placeholder]:text-muted-foreground',
58+
'items-center justify-between [&>span]:truncate text-start',
59+
props.class,
60+
)">
61+
<SelectValue :placeholder="placeholder || 'Select an option'" />
62+
</SelectTrigger>
63+
<SelectContent>
64+
<SelectItem v-for="option in normalizedOptions" :key="option.value" :value="option.value">
65+
{{ option.label }}
66+
</SelectItem>
67+
</SelectContent>
68+
</Select>
69+
</div>
6570
</template>

resources/js/components/ui/textarea/Textarea.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const modelValue = useVModel(props, "modelValue", emits, {
2020
</script>
2121

2222
<template>
23-
<textarea v-model="modelValue" :class="cn(
23+
<textarea v-model="modelValue" v-bind="$attrs" :class="cn(
2424
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex min-h-20 w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none',
2525
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
2626
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',

resources/js/pages/settings/Profile.vue

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { Form, Head, Link, usePage } from '@inertiajs/vue3';
55
import DeleteUser from '@/components/DeleteUser.vue';
66
import HeadingSmall from '@/components/HeadingSmall.vue';
77
import InputError from '@/components/InputError.vue';
8+
import Select from '@/components/Select.vue';
89
import { Button } from '@/components/ui/button';
910
import { Input } from '@/components/ui/input';
1011
import { Label } from '@/components/ui/label';
12+
import { Textarea } from '@/components/ui/textarea';
1113
import AppLayout from '@/layouts/AppLayout.vue';
1214
import SettingsLayout from '@/layouts/settings/Layout.vue';
1315
import { type BreadcrumbItem } from '@/types';
@@ -16,6 +18,7 @@ import { useSettingsRoutes } from '@/composables/useSettingsRoutes';
1618
interface Props {
1719
mustVerifyEmail: boolean;
1820
status?: string;
21+
profile?: any;
1922
}
2023
2124
defineProps<Props>();
@@ -25,6 +28,11 @@ const user = page.props.auth.user;
2528
2629
const settingsRoutes = useSettingsRoutes();
2730
31+
const genderOptions = [
32+
{ value: 'male', label: 'Male' },
33+
{ value: 'female', label: 'Female' },
34+
];
35+
2836
const breadcrumbItems: BreadcrumbItem[] = [
2937
{
3038
title: 'Profile settings',
@@ -58,12 +66,45 @@ const breadcrumbItems: BreadcrumbItem[] = [
5866
<InputError class="mt-2" :message="errors.email" />
5967
</div>
6068

69+
<div class="grid gap-2">
70+
<Label for="address">Address</Label>
71+
<Textarea id="address" class="mt-1 block w-full" name="address"
72+
:default-value="profile?.address" placeholder="Your address" rows="3" />
73+
<InputError class="mt-2" :message="errors.address" />
74+
</div>
75+
76+
<!-- Two column grid for profile fields -->
77+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
78+
<div class="grid gap-2">
79+
<Label for="contact_number">Contact Number</Label>
80+
<Input id="contact_number" type="tel" class="mt-1 block w-full" name="contact_number"
81+
:default-value="profile?.contact_number" placeholder="Your contact number" />
82+
<InputError class="mt-2" :message="errors.contact_number" />
83+
</div>
84+
85+
<div class="grid gap-2">
86+
<Label for="gender">Gender</Label>
87+
<Select id="gender" class="mt-1" name="gender" :model-value="profile?.gender"
88+
:options="genderOptions" placeholder="Select gender" />
89+
<InputError class="mt-2" :message="errors.gender" />
90+
</div>
91+
</div>
92+
93+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
94+
<div class="grid gap-2">
95+
<Label for="birth_date">Birth Date</Label>
96+
<Input id="birth_date" type="date" class="mt-1 block w-full" name="birth_date"
97+
:default-value="profile?.birth_date" placeholder="Birth Date" />
98+
<InputError class="mt-2" :message="errors.birth_date" />
99+
</div>
100+
</div>
101+
61102
<div v-if="mustVerifyEmail && !user.email_verified_at">
62103
<p class="-mt-4 text-sm text-muted-foreground">
63104
Your email address is unverified.
64105
<Link :href="send()" as="button"
65106
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500">
66-
Click here to resend the verification email.
107+
Click here to resend the verification email.
67108
</Link>
68109
</p>
69110

0 commit comments

Comments
 (0)