Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 56 additions & 0 deletions src/app/api/user/profile/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { withRateLimit } from '@/lib/ratelimit';
import { edgeLog } from '@/../infra/edge-config';

export const runtime = 'edge';

const profileBodySchema = z.object({
firstName: z.string().min(1, 'First name is required').max(100),
lastName: z.string().min(1, 'Last name is required').max(100),
email: z.string().email('Invalid email address'),
bio: z.string().max(500, 'Bio must be at most 500 characters'),
location: z.string().max(200).optional(),
website: z.string().url('Invalid URL').max(500).optional().or(z.literal('')),
twitter: z.string().max(100).optional(),
github: z.string().max(100).optional(),
linkedin: z.string().max(100).optional(),
});

export async function PUT(request: Request) {
edgeLog('info', '/api/user/profile', 'PUT request received');
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'WRITE');
if (rateLimitResponse) return rateLimitResponse;

try {
const json = await request.json();
const parsed = profileBodySchema.safeParse(json);

if (!parsed.success) {
const fieldErrors = parsed.error.errors.map((e) => ({
field: e.path.join('.'),
message: e.message,
}));
return addHeaders(
NextResponse.json(
{ success: false, message: 'Validation failed', errors: fieldErrors },
{ status: 400 },
),
);
}

return addHeaders(
NextResponse.json({
success: true,
data: {
...parsed.data,
updatedAt: new Date().toISOString(),
},
}),
);
} catch {
return addHeaders(
NextResponse.json({ success: false, message: 'Bad request body' }, { status: 400 }),
);
}
}
15 changes: 12 additions & 3 deletions src/app/components/profile/ProfileEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ImageUploader from '@/components/shared/ImageUploader';
import PreferencesSection from '@/components/profile/PreferencesSection';
import { useProfileUpdate } from '@/app/hooks/useProfileUpdate';
import { FieldError } from '@/components/forms/FormError';
import { ApiError } from '@/utils/error-handler';

const profileSchema = z.object({
firstName: z.string().min(2, 'First name must be at least 2 characters'),
Expand Down Expand Up @@ -42,6 +43,7 @@ export default function ProfileEditForm() {
const {
register,
handleSubmit,
setError,
formState: { errors },
} = useForm<ProfileFormData>({
resolver: zodResolver(profileSchema),
Expand All @@ -53,11 +55,18 @@ export default function ProfileEditForm() {
try {
await updateProfile(data);
toast.success('Profile updated successfully!');
} catch {
toast.error('Failed to update profile. Please try again.');
} catch (error: unknown) {
if (error instanceof ApiError && error.errors) {
for (const fe of error.errors) {
setError(fe.field as keyof ProfileFormData, { message: fe.message });
}
toast.error('Please fix the highlighted fields.');
} else {
toast.error('Failed to update profile. Please try again.');
}
}
},
[updateProfile],
[updateProfile, setError],
);

const handleImageSelect = useCallback(() => {}, []);
Expand Down
33 changes: 18 additions & 15 deletions src/app/hooks/useProfileUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from 'react';
import { apiClient } from '@/lib/api';
import { createLogger } from '@/lib/logging';
const logger = createLogger('useProfileUpdate');

Expand All @@ -14,30 +15,32 @@ interface ProfileData {
linkedin?: string;
}

interface ProfileUpdateResponse {
success: boolean;
data: ProfileData & { updatedAt: string };
message?: string;
errors?: { field: string; message: string }[];
}

export function useProfileUpdate() {
const [isLoading, setIsLoading] = useState(false);

const updateProfile = useCallback(async (data: ProfileData) => {
setIsLoading(true);
try {
// TODO: Replace with actual API call
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulated API delay
const res = await apiClient.put<ProfileUpdateResponse>('/api/user/profile', data);

// Simulate API response
const response = {
success: true,
data: {
...data,
updatedAt: new Date().toISOString(),
},
};

if (!response.success) {
throw new Error('Failed to update profile');
if (!res.success) {
throw res;
}

return response.data;
} catch (error) {
return res.data;
} catch (error: unknown) {
if (error && typeof error === 'object' && 'errors' in error) {
const apiErr = error as ProfileUpdateResponse;
logger.error('Validation error updating profile', { errors: apiErr.errors });
throw apiErr;
}
logger.error('Error updating profile', { error });
throw error;
} finally {
Expand Down
Loading