mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 09:51:13 -05:00
Fix: Implement Phase 1 and 2 for Account & Profile tab
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useProfile(userId: string | undefined) {
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
// Type assertion since we know the structure from the RPC function
|
||||
// Type the JSONB response properly
|
||||
const profileData = data as unknown as Profile;
|
||||
|
||||
// Fetch location separately if location_id is present and visible
|
||||
|
||||
@@ -3080,6 +3080,20 @@ export type Database = {
|
||||
Args: { target_park_id: string }
|
||||
Returns: undefined
|
||||
}
|
||||
update_profile: {
|
||||
Args: {
|
||||
p_avatar_image_id?: string
|
||||
p_avatar_url?: string
|
||||
p_bio?: string
|
||||
p_display_name?: string
|
||||
p_personal_location?: string
|
||||
p_preferred_language?: string
|
||||
p_preferred_pronouns?: string
|
||||
p_show_pronouns?: boolean
|
||||
p_username: string
|
||||
}
|
||||
Returns: Json
|
||||
}
|
||||
update_ride_ratings: {
|
||||
Args: { target_ride_id: string }
|
||||
Returns: undefined
|
||||
|
||||
@@ -8,11 +8,11 @@ interface EmailValidationResult {
|
||||
|
||||
/**
|
||||
* Validates an email address against disposable email domains
|
||||
* Uses the validate-email edge function to check the backend blocklist
|
||||
* Uses the validate-email-backend edge function for server-side validation
|
||||
*/
|
||||
export async function validateEmailNotDisposable(email: string): Promise<EmailValidationResult> {
|
||||
try {
|
||||
const { data, error } = await supabase.functions.invoke('validate-email', {
|
||||
const { data, error } = await supabase.functions.invoke('validate-email-backend', {
|
||||
body: { email }
|
||||
});
|
||||
|
||||
|
||||
@@ -92,6 +92,13 @@ export const personalLocationSchema = z.string()
|
||||
)
|
||||
.optional();
|
||||
|
||||
// Preferred pronouns validation
|
||||
export const preferredPronounsSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.max(20, { message: "Pronouns must be less than 20 characters" })
|
||||
.optional();
|
||||
|
||||
export const profileEditSchema = z.object({
|
||||
username: usernameSchema,
|
||||
display_name: displayNameSchema,
|
||||
|
||||
@@ -186,6 +186,26 @@ export interface Profile {
|
||||
park_count: number;
|
||||
review_count: number;
|
||||
reputation_score: number;
|
||||
banned: boolean;
|
||||
deactivated: boolean;
|
||||
deactivated_at?: string;
|
||||
deactivation_reason?: string;
|
||||
oauth_provider?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AccountDeletionRequest {
|
||||
id: string;
|
||||
user_id: string;
|
||||
requested_at: string;
|
||||
scheduled_deletion_at: string;
|
||||
confirmation_code: string;
|
||||
confirmation_code_sent_at?: string;
|
||||
status: 'pending' | 'confirmed' | 'cancelled' | 'completed';
|
||||
cancelled_at?: string;
|
||||
completed_at?: string;
|
||||
cancellation_reason?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user