"use client" import { useState, useRef, useTransition } from "react" import { useRouter } from "next/navigation" import { toast } from "sonner" import { Camera, Loader2, X, Home } from "lucide-react" import { updateBranding } from "@/app/actions/branding" const HEX_RE = /^#[0-9a-fA-F]{6}$/ interface Props { brandName: string | null brandLogoUrl: string | null brandColor: string | null hidePoweredBy: boolean } export function BrandingForm({ brandName, brandLogoUrl, brandColor, hidePoweredBy }: Props) { const router = useRouter() const fileRef = useRef(null) const [isPending, startTransition] = useTransition() const [name, setName] = useState(brandName ?? "") const [logoUrl, setLogoUrl] = useState(brandLogoUrl) const [color, setColor] = useState(brandColor ?? "#4f46e5") const [hidePb, setHidePb] = useState(hidePoweredBy) const [uploading, setUploading] = useState(false) const [error, setError] = useState("") const validColor = HEX_RE.test(color) const previewColor = validColor ? color : "#4f46e5" const inputClass = "w-full rounded-lg border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-white/30 outline-none ring-indigo-500 transition focus:border-indigo-500/50 focus:ring-1" const labelClass = "mb-1.5 block text-sm font-medium text-white/70" async function handleLogo(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return if (!file.type.startsWith("image/")) { toast.error("Please select an image file") return } if (file.size > 5 * 1024 * 1024) { toast.error("Logo must be under 5MB") return } setUploading(true) try { const fd = new FormData() fd.append("file", file) fd.append("scope", "misc") const res = await fetch("/api/upload", { method: "POST", body: fd }) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(typeof data.error === "string" ? data.error : "Upload failed") } const { url } = await res.json() setLogoUrl(url) toast.success("Logo uploaded") } catch (err: any) { toast.error(err?.message ?? "Upload failed") } finally { setUploading(false) if (fileRef.current) fileRef.current.value = "" } } function handleSubmit(e: React.FormEvent) { e.preventDefault() setError("") if (color.trim() && !validColor) { setError("Accent color must be a hex value like #4f46e5") return } const fd = new FormData() fd.set("brand_name", name) fd.set("brand_logo_url", logoUrl ?? "") fd.set("brand_color", color.trim() && validColor ? color : "") if (hidePb) fd.set("hide_powered_by", "on") startTransition(async () => { try { await updateBranding(fd) toast.success("Branding saved") router.refresh() } catch (err: any) { const msg = err?.message ?? "Failed to save branding" setError(msg) toast.error(msg) } }) } return (
{error && (
{error}
)}
setName(e.target.value)} maxLength={60} placeholder="Acme Property Management" className={inputClass} />

Shown in the tenant portal header instead of “Tenant Portal”.

{logoUrl ? (
{/* eslint-disable-next-line @next/next/no-img-element */} Brand logo
{uploading && (
)}
) : ( )}

PNG or SVG works best. Max 5MB.

setColor(e.target.value)} className="h-10 w-12 shrink-0 cursor-pointer rounded-lg border border-white/10 bg-white/5 p-1" aria-label="Accent color picker" /> setColor(e.target.value)} placeholder="#4f46e5" className={inputClass + (color.trim() && !validColor ? " border-red-500/40" : "")} />

Used for highlights in the tenant portal. Hex like #RRGGBB.

{/* Live preview */}

Preview

{logoUrl ? ( // eslint-disable-next-line @next/next/no-img-element Logo ) : ( )}

{name.trim() || "Tenant Portal"}

Jane Tenant

Rent History
{!hidePb && (

Powered by Property Management Network

)}
) }