Add EditProfileForm

This commit is contained in:
Alex Gleason 2025-05-14 15:56:53 -05:00
parent d05a78880a
commit f77a6a4af3
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
3 changed files with 274 additions and 2 deletions

View File

@ -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 (
<div>
{/* you may want to wrap this in a layout or include other components depending on the project ... */}
<EditProfileForm />
</div>
);
}
```
The `EditProfileForm` component displays just the form. It requires no props, and will "just work" automatically.
## Development Practices ## Development Practices
- Uses React Query for data fetching and caching - Uses React Query for data fetching and caching

View File

@ -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<NostrMetadata>({
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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormDescription>
This is your display name that will be displayed to others.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="about"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell others about yourself"
className="resize-none"
{...field}
/>
</FormControl>
<FormDescription>
A short description about yourself.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField
control={form.control}
name="picture"
render={({ field }) => (
<FormItem>
<FormLabel>Profile Picture URL</FormLabel>
<FormControl>
<Input placeholder="https://example.com/profile.jpg" {...field} />
</FormControl>
<FormDescription>
URL to your profile picture.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="banner"
render={({ field }) => (
<FormItem>
<FormLabel>Banner Image URL</FormLabel>
<FormControl>
<Input placeholder="https://example.com/banner.jpg" {...field} />
</FormControl>
<FormDescription>
URL to a wide banner image for your profile.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>Website</FormLabel>
<FormControl>
<Input placeholder="https://yourwebsite.com" {...field} />
</FormControl>
<FormDescription>
Your personal website or social media link.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="nip05"
render={({ field }) => (
<FormItem>
<FormLabel>NIP-05 Identifier</FormLabel>
<FormControl>
<Input placeholder="you@example.com" {...field} />
</FormControl>
<FormDescription>
Your verified Nostr identifier.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="bot"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Bot Account</FormLabel>
<FormDescription>
Mark this account as automated or a bot.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<Button
type="submit"
className="w-full md:w-auto"
disabled={isPending}
>
{isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Save Profile
</Button>
</form>
</Form>
);
};

View File

@ -38,11 +38,11 @@ export function useCurrentUser() {
}, [logins, loginToUser]); }, [logins, loginToUser]);
const user = users[0] as NUser | undefined; const user = users[0] as NUser | undefined;
const data = useAuthor(user?.pubkey); const author = useAuthor(user?.pubkey);
return { return {
user, user,
data,
users, users,
...author.data,
}; };
} }