From 6ece47e3edfea17aca7cf844e386ed32986dd328 Mon Sep 17 00:00:00 2001 From: SatsFaucet Date: Mon, 6 Apr 2026 20:23:39 +0200 Subject: [PATCH] feat(frontend): allow sponsors to edit ads from My Ads Add patchSponsor API, SponsorEditModal for title/description/URLs, and Edit actions on desktop and mobile. Server PATCH /sponsor/:id already enforces creator-only edits. Made-with: Cursor --- frontend/src/api.ts | 17 ++ frontend/src/components/SponsorEditModal.tsx | 158 +++++++++++++++++++ frontend/src/pages/MyAdsPage.tsx | 37 ++++- frontend/src/styles/global.css | 32 ++++ 4 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/SponsorEditModal.tsx diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 18781de..ebd018f 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -445,3 +445,20 @@ export async function postSponsorRegenerateInvoice(sponsorId: number): Promise<{ } return requestWithNip98("POST", `/sponsor/${sponsorId}/regenerate-invoice`); } + +export async function patchSponsor( + sponsorId: number, + body: { + title?: string; + description?: string; + link_url?: string; + image_url?: string; + category?: string; + lightning_address?: string; + } +): Promise { + if (getToken()) { + return requestWithBearer("PATCH", `/sponsor/${sponsorId}`, body); + } + return requestWithNip98("PATCH", `/sponsor/${sponsorId}`, body); +} diff --git a/frontend/src/components/SponsorEditModal.tsx b/frontend/src/components/SponsorEditModal.tsx new file mode 100644 index 0000000..4f3eea6 --- /dev/null +++ b/frontend/src/components/SponsorEditModal.tsx @@ -0,0 +1,158 @@ +import { useState, useEffect, useCallback } from "react"; +import { Modal } from "./Modal"; +import { patchSponsor, type ApiError, type SponsorMyAd } from "../api"; + +interface SponsorEditModalProps { + open: boolean; + ad: SponsorMyAd | null; + onClose: () => void; + /** Called after a successful save with the updated row from the server. */ + onSaved?: (updated: SponsorMyAd) => void; +} + +export function SponsorEditModal({ open, ad, onClose, onSaved }: SponsorEditModalProps) { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [linkUrl, setLinkUrl] = useState(""); + const [imageUrl, setImageUrl] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!open || !ad) return; + setTitle(ad.title); + setDescription(ad.description); + setLinkUrl(ad.link_url); + setImageUrl(ad.image_url ?? ""); + setError(null); + }, [open, ad]); + + const handleSubmit = useCallback( + async (e: React.FormEvent) => { + e.preventDefault(); + if (!ad) return; + setError(null); + if (!title.trim()) { + setError("Title is required"); + return; + } + if (!description.trim()) { + setError("Description is required"); + return; + } + if (!linkUrl.trim() || !/^https?:\/\/.+/.test(linkUrl)) { + setError("Valid URL (https://...) is required"); + return; + } + setLoading(true); + try { + const updated = await patchSponsor(ad.id, { + title: title.trim(), + description: description.trim(), + link_url: linkUrl.trim(), + image_url: imageUrl.trim() || undefined, + }); + onSaved?.(updated); + onClose(); + } catch (e) { + const msg = + e && typeof e === "object" && "message" in e + ? String((e as ApiError).message) + : "Failed to save changes"; + setError(msg); + } finally { + setLoading(false); + } + }, + [ad, title, description, linkUrl, imageUrl, onSaved, onClose] + ); + + if (!ad) return null; + + return ( + +
+
+ + setTitle(e.target.value)} + placeholder="Your project or product name" + maxLength={100} + required + disabled={loading} + /> +
+
+ +