mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-08-27 13:09:22 +00:00
Add EditProfileForm
This commit is contained in:
parent
d05a78880a
commit
f77a6a4af3
20
CONTEXT.md
20
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 (
|
||||||
|
<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
|
||||||
|
252
src/components/EditProfileForm.tsx
Normal file
252
src/components/EditProfileForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user