diff --git a/src/components/profile/ProfileView.tsx b/src/components/profile/ProfileView.tsx index 1d2744a..7c463a2 100644 --- a/src/components/profile/ProfileView.tsx +++ b/src/components/profile/ProfileView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import { useUIStore } from "../../stores/ui"; import { useUserStore } from "../../stores/user"; @@ -6,9 +6,121 @@ import { useMuteStore } from "../../stores/mute"; import { useProfile, invalidateProfileCache } from "../../hooks/useProfile"; import { fetchUserNotes, publishProfile } from "../../lib/nostr"; import { shortenPubkey } from "../../lib/utils"; +import { uploadImage } from "../../lib/upload"; import { NoteCard } from "../feed/NoteCard"; import { ZapModal } from "../zap/ZapModal"; +// ── Profile helper sub-components ──────────────────────────────────────────── + +function ImageField({ label, value, onChange }: { label: string; value: string; onChange: (v: string) => void }) { + const [uploading, setUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); + const fileRef = useRef(null); + + const handleFile = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setUploading(true); + setUploadError(null); + try { + const url = await uploadImage(file); + onChange(url); + } catch (err) { + setUploadError(String(err)); + } finally { + setUploading(false); + if (fileRef.current) fileRef.current.value = ""; + } + }; + + return ( +
+ +
+ onChange(e.target.value)} + placeholder="https://… or click upload →" + className="flex-1 bg-bg border border-border px-3 py-1.5 text-text text-[12px] focus:outline-none focus:border-accent/50" + style={{ WebkitUserSelect: "text", userSelect: "text" } as React.CSSProperties} + /> + +
+ {uploadError &&

{uploadError}

} + +
+ ); +} + +type Nip05Status = "idle" | "checking" | "valid" | "mismatch" | "notfound"; + +function Nip05Field({ value, onChange, pubkey }: { value: string; onChange: (v: string) => void; pubkey: string }) { + const [status, setStatus] = useState("idle"); + + useEffect(() => { + if (!value.includes("@")) { setStatus("idle"); return; } + setStatus("checking"); + const t = setTimeout(async () => { + const [name, domain] = value.trim().split("@"); + if (!name || !domain) { setStatus("notfound"); return; } + try { + const resp = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`); + const data = await resp.json(); + const resolved = data.names?.[name]; + if (!resolved) setStatus("notfound"); + else if (resolved === pubkey) setStatus("valid"); + else setStatus("mismatch"); + } catch { + setStatus("notfound"); + } + }, 900); + return () => clearTimeout(t); + }, [value, pubkey]); + + const badge = { + idle: null, + checking: checking…, + valid: ✓ verified, + mismatch: ✗ pubkey mismatch, + notfound: ✗ not found, + }[status]; + + return ( +
+
+ + {badge} +
+ onChange(e.target.value)} + placeholder="you@domain.com" + className="w-full bg-bg border border-border px-3 py-1.5 text-text text-[12px] focus:outline-none focus:border-accent/50" + style={{ WebkitUserSelect: "text", userSelect: "text" } as React.CSSProperties} + /> +

+ Proves your identity via a domain you control.{" "} + + How to get verified ↗ + +

+
+ ); +} + function EditProfileForm({ pubkey, onSaved }: { pubkey: string; onSaved: () => void }) { const { profile, fetchOwnProfile } = useUserStore(); const [name, setName] = useState(profile?.name || ""); @@ -66,10 +178,10 @@ function EditProfileForm({ pubkey, onSaved }: { pubkey: string; onSaved: () => v
{field("Display name", displayName, setDisplayName, "Square that Circle")} {field("Username", name, setName, "squarethecircle")} - {field("NIP-05 (verified name)", nip05, setNip05, "you@domain.com")} + {field("Lightning address (lud16)", lud16, setLud16, "you@walletofsatoshi.com")} {field("Website", website, setWebsite, "https://…")} - {field("Profile picture URL", picture, setPicture, "https://…")} +
@@ -83,7 +195,7 @@ function EditProfileForm({ pubkey, onSaved }: { pubkey: string; onSaved: () => v />
- {field("Banner image URL", banner, setBanner, "https://…")} +
{error &&

{error}

}
diff --git a/src/lib/upload.ts b/src/lib/upload.ts new file mode 100644 index 0000000..f3620e2 --- /dev/null +++ b/src/lib/upload.ts @@ -0,0 +1,23 @@ +/** + * Upload an image file to nostr.build and return the hosted URL. + * nostr.build offers free public image hosting for the Nostr ecosystem. + */ +export async function uploadImage(file: File): Promise { + const form = new FormData(); + form.append("fileToUpload", file); + + const resp = await fetch("https://nostr.build/api/v2/upload/files", { + method: "POST", + body: form, + }); + + if (!resp.ok) { + throw new Error(`Upload failed (HTTP ${resp.status})`); + } + + const data = await resp.json(); + if (data.status === "success" && data.data?.[0]?.url) { + return data.data[0].url as string; + } + throw new Error(data.message || "Upload failed — no URL returned"); +}