Add react hooks & components to split up the profile page. Learning how to separate hooks
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
@@ -3,7 +3,7 @@
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui';
|
||||
import { getProfile } from '@/lib/actions';
|
||||
import AvatarDropdown from './avatar';
|
||||
import AvatarDropdown from './AvatarDropdown';
|
||||
|
||||
const NavigationAuth = async () => {
|
||||
try {
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui';
|
||||
import NavigationAuth from '@/components/navigation/auth';
|
||||
import NavigationAuth from './auth';
|
||||
import { ThemeToggle } from '@/components/context/theme';
|
||||
|
||||
const Navigation = () => {
|
||||
71
src/components/default/profile/AvatarUpload.tsx
Normal file
71
src/components/default/profile/AvatarUpload.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useFileUpload } from '@/lib/hooks/useFileUpload';
|
||||
import { useAvatar } from '@/lib/hooks/useAvatar';
|
||||
import type { Profile } from '@/utils/supabase';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui';
|
||||
import { Pencil, User, Loader2 } from 'lucide-react';
|
||||
|
||||
type AvatarUploadProps = {
|
||||
profile?: Profile;
|
||||
onAvatarUploaded: (path: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const AvatarUpload = ({ profile, onAvatarUploaded }: AvatarUploadProps) => {
|
||||
const { avatarUrl } = useAvatar(profile);
|
||||
const { isUploading, fileInputRef, uploadToStorage } = useFileUpload();
|
||||
|
||||
const handleAvatarClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const result = await uploadToStorage(file, 'avatars');
|
||||
if (result.success && result.path) {
|
||||
await onAvatarUploaded(result.path);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative group cursor-pointer mb-4" onClick={handleAvatarClick}>
|
||||
<Avatar className="h-32 w-32">
|
||||
{avatarUrl ? (
|
||||
<AvatarImage src={avatarUrl} alt={profile?.full_name ?? 'User'} />
|
||||
) : (
|
||||
<AvatarFallback className="text-2xl">
|
||||
{profile?.full_name
|
||||
? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase()
|
||||
: <User size={32} />}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<div className="absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/50
|
||||
transition-all flex items-center justify-center"
|
||||
>
|
||||
<Pencil className="text-white opacity-0 group-hover:opacity-100
|
||||
transition-opacity" size={24}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
</div>
|
||||
{isUploading && (
|
||||
<div className="flex items-center text-sm text-gray-500 mt-2">
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Uploading...
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
Click on the avatar to upload a new image
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
110
src/components/default/profile/ProfileForm.tsx
Normal file
110
src/components/default/profile/ProfileForm.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { Profile } from '@/utils/supabase';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from '@/components/ui';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const formSchema = z.object({
|
||||
full_name: z.string().min(5, {
|
||||
message: 'Full name is required & must be at least 5 characters.'
|
||||
}),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
type ProfileFormProps = {
|
||||
profile?: Profile;
|
||||
isLoading: boolean;
|
||||
onSubmit: (values: z.infer<typeof formSchema>) => Promise<void>;
|
||||
};
|
||||
|
||||
export function ProfileForm({ profile, isLoading, onSubmit }: ProfileFormProps) {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
full_name: profile?.full_name ?? '',
|
||||
email: profile?.email ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
// Update form values when profile changes
|
||||
useEffect(() => {
|
||||
if (profile) {
|
||||
form.reset({
|
||||
full_name: profile.full_name ?? '',
|
||||
email: profile.email ?? '',
|
||||
});
|
||||
}
|
||||
}, [profile, form]);
|
||||
|
||||
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
await onSubmit(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className='space-y-6'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='full_name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Full Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your email address associated with your account.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type='submit' disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save Changes'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
2
src/components/default/profile/index.tsx
Normal file
2
src/components/default/profile/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './AvatarUpload';
|
||||
export * from './ProfileForm';
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Button } from '@/components/ui';
|
||||
import { type ComponentProps } from 'react';
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TutorialStep, CodeBlock } from '@/components/tutorial';
|
||||
import { TutorialStep, CodeBlock } from '@/components/default/tutorial';
|
||||
|
||||
const create = `create table notes (
|
||||
id bigserial primary key,
|
||||
3
src/components/default/tutorial/index.tsx
Normal file
3
src/components/default/tutorial/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CodeBlock } from './code-block';
|
||||
export { FetchDataSteps } from './fetch-data-steps';
|
||||
export { TutorialStep } from './tutorial-step';
|
||||
@@ -1,3 +0,0 @@
|
||||
export { CodeBlock } from '@/components/tutorial/code-block';
|
||||
export { FetchDataSteps } from '@/components/tutorial/fetch-data-steps';
|
||||
export { TutorialStep } from '@/components/tutorial/tutorial-step';
|
||||
Reference in New Issue
Block a user