Fix: Implement Phase 1 and 2 for Account & Profile tab

This commit is contained in:
gpt-engineer-app[bot]
2025-10-14 18:44:14 +00:00
parent d4b859f637
commit 3833ba9748
9 changed files with 318 additions and 35 deletions

View File

@@ -23,16 +23,18 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { toast as sonnerToast } from 'sonner';
import { AccountDeletionDialog } from './AccountDeletionDialog';
import { DeletionStatusBanner } from './DeletionStatusBanner';
import { usernameSchema, displayNameSchema, bioSchema } from '@/lib/validation';
import { usernameSchema, displayNameSchema, bioSchema, personalLocationSchema, preferredPronounsSchema } from '@/lib/validation';
import { z } from 'zod';
import { AccountDeletionRequest } from '@/types/database';
const profileSchema = z.object({
username: usernameSchema,
display_name: displayNameSchema,
bio: bioSchema,
preferred_pronouns: z.string().max(20).optional(),
preferred_pronouns: preferredPronounsSchema,
show_pronouns: z.boolean(),
preferred_language: z.string()
preferred_language: z.string(),
personal_location: personalLocationSchema
});
type ProfileFormData = z.infer<typeof profileSchema>;
@@ -50,7 +52,7 @@ export function AccountProfileTab() {
const [avatarUrl, setAvatarUrl] = useState<string>(profile?.avatar_url || '');
const [avatarImageId, setAvatarImageId] = useState<string>(profile?.avatar_image_id || '');
const [showDeletionDialog, setShowDeletionDialog] = useState(false);
const [deletionRequest, setDeletionRequest] = useState<any>(null);
const [deletionRequest, setDeletionRequest] = useState<AccountDeletionRequest | null>(null);
const form = useForm<ProfileFormData>({
resolver: zodResolver(profileSchema),
@@ -60,7 +62,8 @@ export function AccountProfileTab() {
bio: profile?.bio || '',
preferred_pronouns: profile?.preferred_pronouns || '',
show_pronouns: profile?.show_pronouns || false,
preferred_language: profile?.preferred_language || 'en'
preferred_language: profile?.preferred_language || 'en',
personal_location: profile?.personal_location || ''
}
});
@@ -89,29 +92,28 @@ export function AccountProfileTab() {
setLoading(true);
try {
const usernameChanged = profile?.username !== data.username;
const { error } = await supabase
.from('profiles')
.update({
username: data.username,
display_name: data.display_name || null,
bio: data.bio || null,
preferred_pronouns: data.preferred_pronouns || null,
show_pronouns: data.show_pronouns,
preferred_language: data.preferred_language,
updated_at: new Date().toISOString()
})
.eq('user_id', user.id);
// Use the update_profile RPC function with server-side validation
const { data: result, error } = await supabase.rpc('update_profile', {
p_username: data.username,
p_display_name: data.display_name || null,
p_bio: data.bio || null,
p_preferred_pronouns: data.preferred_pronouns || null,
p_show_pronouns: data.show_pronouns,
p_preferred_language: data.preferred_language,
p_personal_location: data.personal_location || null
});
if (error) throw error;
// Type the RPC result
const rpcResult = result as unknown as { success: boolean; username_changed: boolean; changes_count: number };
// Update Novu subscriber if username changed
if (usernameChanged && notificationService.isEnabled()) {
if (rpcResult?.username_changed && notificationService.isEnabled()) {
await notificationService.updateSubscriber({
subscriberId: user.id,
email: user.email,
firstName: data.username, // Send username as firstName to Novu
firstName: data.username,
});
}
@@ -137,19 +139,17 @@ export function AccountProfileTab() {
const newAvatarUrl = urls[0];
const newImageId = imageId || '';
// Update local state immediately
// Update local state immediately for optimistic UI
setAvatarUrl(newAvatarUrl);
setAvatarImageId(newImageId);
try {
const { error } = await supabase
.from('profiles')
.update({
avatar_url: newAvatarUrl,
avatar_image_id: newImageId,
updated_at: new Date().toISOString()
})
.eq('user_id', user.id);
// Use update_profile RPC for avatar updates
const { error } = await supabase.rpc('update_profile', {
p_username: profile?.username || '',
p_avatar_url: newAvatarUrl,
p_avatar_image_id: newImageId
});
if (error) throw error;
@@ -245,7 +245,7 @@ export function AccountProfileTab() {
setDeletionRequest(null);
};
const isDeactivated = (profile as any)?.deactivated || false;
const isDeactivated = profile?.deactivated || false;
return (
<div className="space-y-8">
@@ -337,6 +337,11 @@ export function AccountProfileTab() {
{...form.register('preferred_pronouns')}
placeholder="e.g., they/them, she/her, he/him"
/>
{form.formState.errors.preferred_pronouns && (
<p className="text-sm text-destructive">
{form.formState.errors.preferred_pronouns.message}
</p>
)}
</div>
<div className="space-y-2">
@@ -359,6 +364,20 @@ export function AccountProfileTab() {
</div>
</div>
<div className="space-y-2">
<Label htmlFor="personal_location">Personal Location</Label>
<Input
id="personal_location"
{...form.register('personal_location')}
placeholder="e.g., Los Angeles, CA"
/>
{form.formState.errors.personal_location && (
<p className="text-sm text-destructive">
{form.formState.errors.personal_location.message}
</p>
)}
</div>
<Button type="submit" disabled={loading || isDeactivated}>
{loading ? 'Saving...' : 'Save Changes'}
</Button>