diff --git a/CONTEXT.md b/CONTEXT.md index abeac90..c2bded0 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -272,6 +272,26 @@ const events = await nostr.query( ); ``` +## Edit Profile + +To include an Edit Profile form, place the `EditProfileForm` component in the project: + +```tsx +import { EditProfileForm } from "@/components/EditProfileForm"; + +function EditProfilePage() { + return ( +
+ {/* you may want to wrap this in a layout or include other components depending on the project ... */} + + +
+ ); +} +``` + +The `EditProfileForm` component displays just the form. It requires no props, and will "just work" automatically. + ## Development Practices - Uses React Query for data fetching and caching diff --git a/src/components/EditProfileForm.tsx b/src/components/EditProfileForm.tsx new file mode 100644 index 0000000..39a37bf --- /dev/null +++ b/src/components/EditProfileForm.tsx @@ -0,0 +1,252 @@ +import React, { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useNostrPublish } from '@/hooks/useNostrPublish'; +import { useToast } from '@/hooks/useToast'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; +import { Loader2 } from 'lucide-react'; +import { NSchema as n, type NostrMetadata } from '@nostrify/nostrify'; +import { useQueryClient } from '@tanstack/react-query'; + +export const EditProfileForm: React.FC = () => { + const queryClient = useQueryClient(); + + const { user, metadata } = useCurrentUser(); + const { mutateAsync: publishEvent, isPending } = useNostrPublish(); + const { toast } = useToast(); + + // Initialize the form with default values + const form = useForm({ + resolver: zodResolver(n.metadata()), + defaultValues: { + name: '', + about: '', + picture: '', + banner: '', + website: '', + nip05: '', + bot: false, + }, + }); + + // Update form values when user data is loaded + useEffect(() => { + if (metadata) { + form.reset({ + name: metadata.name || '', + about: metadata.about || '', + picture: metadata.picture || '', + banner: metadata.banner || '', + website: metadata.website || '', + nip05: metadata.nip05 || '', + bot: metadata.bot || false, + }); + } + }, [metadata, form]); + + const onSubmit = async (values: NostrMetadata) => { + if (!user) { + toast({ + title: 'Error', + description: 'You must be logged in to update your profile', + variant: 'destructive', + }); + return; + } + + try { + // Combine existing metadata with new values + const data = { ...metadata, ...values }; + + // Clean up empty values + for (const key in data) { + if (data[key] === '') { + delete data[key]; + } + } + + // Publish the metadata event (kind 0) + await publishEvent({ + kind: 0, + content: JSON.stringify(data), + }); + + // Invalidate queries to refresh the data + queryClient.invalidateQueries({ queryKey: ['logins'] }); + queryClient.invalidateQueries({ queryKey: ['author', user.pubkey] }); + + toast({ + title: 'Success', + description: 'Your profile has been updated', + }); + } catch (error) { + console.error('Failed to update profile:', error); + toast({ + title: 'Error', + description: 'Failed to update your profile. Please try again.', + variant: 'destructive', + }); + } + }; + + return ( +
+ + ( + + Name + + + + + This is your display name that will be displayed to others. + + + + )} + /> + + ( + + Bio + +