diff --git a/CONTEXT.md b/CONTEXT.md index c2bded0..113f885 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -292,6 +292,30 @@ function EditProfilePage() { The `EditProfileForm` component displays just the form. It requires no props, and will "just work" automatically. +## Uploading Files + +Use the `useUploadFile` hook to upload files. + +```tsx +import { useUploadFile } from "@/hooks/useUploadFile"; + +function MyComponent() { + const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); + + const handleUpload = async (file: File) => { + try { + // The first tuple in the array contains the URL + const [[_, url]] = await uploadFile(file); + // ...use the url + } catch (error) { + // ...handle errors + } + }; + + // ...rest of component +} +``` + ## Development Practices - Uses React Query for data fetching and caching diff --git a/package-lock.json b/package-lock.json index 485ffed..15137ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^3.9.0", - "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.45.2", - "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.3", + "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.1", + "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.5", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.0", @@ -821,13 +821,12 @@ } }, "node_modules/@jsr/nostrify__nostrify": { - "version": "0.45.2", - "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.45.2.tgz", - "integrity": "sha512-0eN+ceK79FcLDN6gJQd8GhM2UG09RVZBUDlveQqQMmNjuakvqLSE9Emg2i12EyEPOBa/yzCSiwkFFK2XzknH9Q==", + "version": "0.46.1", + "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.46.1.tgz", + "integrity": "sha512-7XSP4+kjcPgw937jQPUxf+qo1mWx7rbKUuA5ma0CofRQZa2v01IA4f+OfVGJbABxbJ8SC+8VdPkLqqUOJxeVPg==", "dependencies": { "@jsr/nostrify__types": "^0.36.0", "@jsr/scure__base": "^1.2.4", - "@jsr/std__crypto": "^0.224.0", "@jsr/std__encoding": "^0.224.1", "lru-cache": "^10.2.0", "nostr-tools": "^2.10.4", @@ -845,42 +844,11 @@ "resolved": "https://npm.jsr.io/~/11/@jsr/scure__base/1.2.5.tgz", "integrity": "sha512-ino21k5s2kamz+uhAL/N3iI5vJ7x3zTtrzhM52iXBth41KxkFSIDiOlQNbMj4/jTih27jmS6Mi4Xke5vKTdNIA==" }, - "node_modules/@jsr/std__assert": { - "version": "0.224.0", - "resolved": "https://npm.jsr.io/~/11/@jsr/std__assert/0.224.0.tgz", - "integrity": "sha512-RB0p0ydybgKSfTba6kHWytfpEJ0CBPi+byxZikLYa51L9uLINW52/j6n4KuiLFoh2cdFfpNZSNMY/dzQPW90DQ==", - "dependencies": { - "@jsr/std__fmt": "^0.224.0", - "@jsr/std__internal": "^0.224.0" - } - }, - "node_modules/@jsr/std__crypto": { - "version": "0.224.0", - "resolved": "https://npm.jsr.io/~/11/@jsr/std__crypto/0.224.0.tgz", - "integrity": "sha512-qzZWI8VnH215FS7hmQsAeNafjLMkmSl1OOvexorVUEf1Zl9omHSN87MwIjtmyVXGLtpGRLzIhKXbeup1xO69Zw==", - "dependencies": { - "@jsr/std__assert": "^0.224.0", - "@jsr/std__encoding": "^0.224.0" - } - }, "node_modules/@jsr/std__encoding": { "version": "0.224.3", "resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/0.224.3.tgz", "integrity": "sha512-zAuX2QV1zwJ5RSmrnDGVerAtN3pBXpYYNlGzhERW9AiQ1UJd2/xruyB3i5NdTWy2OK2pjETswOj+0+prYTPlxQ==" }, - "node_modules/@jsr/std__fmt": { - "version": "0.224.0", - "resolved": "https://npm.jsr.io/~/11/@jsr/std__fmt/0.224.0.tgz", - "integrity": "sha512-lyrH5LesMB897QW0NIbZlGp72Ucopj2hMZW2wqB0NyZhuXfLH2sPBIUpCSf87kRKTGnx90JV905w4iTp0TD+Sg==" - }, - "node_modules/@jsr/std__internal": { - "version": "0.224.0", - "resolved": "https://npm.jsr.io/~/11/@jsr/std__internal/0.224.0.tgz", - "integrity": "sha512-inYzKOGAFK2tyy1D4NfwlbPiqEcSaXfOms3Tm4Y+1LmKSYOeB9wjqWHF4y/BJuYj8XUv61F7eaHaIw6NIlhBWg==", - "dependencies": { - "@jsr/std__fmt": "^0.224.0" - } - }, "node_modules/@noble/ciphers": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", @@ -963,13 +931,12 @@ }, "node_modules/@nostrify/nostrify": { "name": "@jsr/nostrify__nostrify", - "version": "0.45.2", - "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.45.2.tgz", - "integrity": "sha512-0eN+ceK79FcLDN6gJQd8GhM2UG09RVZBUDlveQqQMmNjuakvqLSE9Emg2i12EyEPOBa/yzCSiwkFFK2XzknH9Q==", + "version": "0.46.1", + "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.46.1.tgz", + "integrity": "sha512-7XSP4+kjcPgw937jQPUxf+qo1mWx7rbKUuA5ma0CofRQZa2v01IA4f+OfVGJbABxbJ8SC+8VdPkLqqUOJxeVPg==", "dependencies": { "@jsr/nostrify__types": "^0.36.0", "@jsr/scure__base": "^1.2.4", - "@jsr/std__crypto": "^0.224.0", "@jsr/std__encoding": "^0.224.1", "lru-cache": "^10.2.0", "nostr-tools": "^2.10.4", @@ -979,11 +946,11 @@ }, "node_modules/@nostrify/react": { "name": "@jsr/nostrify__react", - "version": "0.2.3", - "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__react/0.2.3.tgz", - "integrity": "sha512-XIa9aBhaSOe6AQWvKHYG3rq3+hTheWLd8kQM9SeyUQRpHM5RMlrmdrBKAXHCwt+/vZ9eOxM6nUq38C0hp4X6Bw==", + "version": "0.2.5", + "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__react/0.2.5.tgz", + "integrity": "sha512-Hyi1N4hXa89gYsuk7fqGrWEzIzTp7md8hKog+D2DgpSqVwFQM1vw267Uv3rOieE/hPereD3gen9X88XkeAXP+Q==", "dependencies": { - "@jsr/nostrify__nostrify": "^0.45.2", + "@jsr/nostrify__nostrify": "^0.46.1", "nostr-tools": "^2.10.4", "react": "^18.0.0" } diff --git a/package.json b/package.json index b6db67f..4c227b0 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.0", - "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.45.2", - "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.3", + "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.1", + "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.5", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.0", diff --git a/src/components/EditProfileForm.tsx b/src/components/EditProfileForm.tsx index 39a37bf..1219ae9 100644 --- a/src/components/EditProfileForm.tsx +++ b/src/components/EditProfileForm.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -17,15 +17,17 @@ import { import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; -import { Loader2 } from 'lucide-react'; +import { Loader2, Upload } from 'lucide-react'; import { NSchema as n, type NostrMetadata } from '@nostrify/nostrify'; import { useQueryClient } from '@tanstack/react-query'; +import { useUploadFile } from '@/hooks/useUploadFile'; export const EditProfileForm: React.FC = () => { const queryClient = useQueryClient(); const { user, metadata } = useCurrentUser(); const { mutateAsync: publishEvent, isPending } = useNostrPublish(); + const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const { toast } = useToast(); // Initialize the form with default values @@ -57,6 +59,26 @@ export const EditProfileForm: React.FC = () => { } }, [metadata, form]); + // Handle file uploads for profile picture and banner + const uploadPicture = async (file: File, field: 'picture' | 'banner') => { + try { + // The first tuple in the array contains the URL + const [[_, url]] = await uploadFile(file); + form.setValue(field, url); + toast({ + title: 'Success', + description: `${field === 'picture' ? 'Profile picture' : 'Banner'} uploaded successfully`, + }); + } catch (error) { + console.error(`Failed to upload ${field}:`, error); + toast({ + title: 'Error', + description: `Failed to upload ${field === 'picture' ? 'profile picture' : 'banner'}. Please try again.`, + variant: 'destructive', + }); + } + }; + const onSubmit = async (values: NostrMetadata) => { if (!user) { toast({ @@ -148,16 +170,14 @@ export const EditProfileForm: React.FC = () => { control={form.control} name="picture" render={({ field }) => ( - - Profile Picture URL - - - - - URL to your profile picture. - - - + uploadPicture(file, 'picture')} + /> )} /> @@ -165,16 +185,14 @@ export const EditProfileForm: React.FC = () => { control={form.control} name="banner" render={({ field }) => ( - - Banner Image URL - - - - - URL to a wide banner image for your profile. - - - + uploadPicture(file, 'banner')} + /> )} /> @@ -239,9 +257,9 @@ export const EditProfileForm: React.FC = () => { + {field.value && ( +
+ {`${label} +
+ )} + + + + {description} + + + + ); +}; diff --git a/src/hooks/useUploadFile.ts b/src/hooks/useUploadFile.ts new file mode 100644 index 0000000..a3c9793 --- /dev/null +++ b/src/hooks/useUploadFile.ts @@ -0,0 +1,26 @@ +import { useMutation } from "@tanstack/react-query"; +import { BlossomUploader } from '@nostrify/nostrify/uploaders'; + +import { useCurrentUser } from "./useCurrentUser"; + +export function useUploadFile() { + const { user } = useCurrentUser(); + + return useMutation({ + mutationFn: async (file: File) => { + if (!user) { + throw new Error('Must be logged in to upload files'); + } + + const uploader = new BlossomUploader({ + servers: [ + 'https://blossom.primal.net/', + ], + signer: user.signer, + }); + + const tags = await uploader.upload(file); + return tags; + }, + }); +} \ No newline at end of file