feat(settings/profile): collapsible profile card + share sheet

Collapsible profile card:
- Starts collapsed if profile already exists (shows name + URL slug)
- Expands/collapses via chevron toggle row
- Auto-collapses after Save Profile succeeds

Share sheet on Product Recommendations:
- ↗ Share button appears once a valid slug is set
- Options: Copy link (clipboard, shows  feedback), WhatsApp deep link,
  and native Web Share API ("More options") when browser supports it
- Backdrop click closes the sheet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-23 19:34:52 +05:30
parent 3b62841bd4
commit 8598259fb1

View file

@ -33,6 +33,9 @@ export default function ProfileSettingsPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [saveMsg, setSaveMsg] = useState(""); const [saveMsg, setSaveMsg] = useState("");
const [profileExpanded, setProfileExpanded] = useState(true);
const [showShareSheet, setShowShareSheet] = useState(false);
const [copied, setCopied] = useState(false);
const [slug, setSlug] = useState(""); const [slug, setSlug] = useState("");
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState("");
@ -58,12 +61,15 @@ export default function ProfileSettingsPage() {
setDisplayName(pd.profile.display_name); setDisplayName(pd.profile.display_name);
setBio(pd.profile.bio ?? ""); setBio(pd.profile.bio ?? "");
setIsPublic(pd.profile.is_public); setIsPublic(pd.profile.is_public);
setProfileExpanded(false); // already set up — start collapsed
} }
setProducts(prods.items || []); setProducts(prods.items || []);
setLoading(false); setLoading(false);
}).catch(() => setLoading(false)); }).catch(() => setLoading(false));
}, []); }, []);
const profileUrl = typeof window !== "undefined" && slug ? `${window.location.origin}/m/${slug}` : "";
async function saveProfile() { async function saveProfile() {
setSaving(true); setSaving(true);
setSaveMsg(""); setSaveMsg("");
@ -75,13 +81,32 @@ export default function ProfileSettingsPage() {
const d = await r.json(); const d = await r.json();
if (r.ok) { if (r.ok) {
setProfile(d.profile); setProfile(d.profile);
setSaveMsg(`Saved! View at /m/${d.profile.slug}`); setSaveMsg("Saved!");
setProfileExpanded(false); // collapse after save
} else { } else {
setSaveMsg(d.error || "Save failed"); setSaveMsg(d.error || "Save failed");
} }
setSaving(false); setSaving(false);
} }
async function copyLink() {
if (!profileUrl) return;
await navigator.clipboard.writeText(profileUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
function shareViaWhatsApp() {
const text = encodeURIComponent(`Check out my baby product recommendations! ${profileUrl}`);
window.open(`https://wa.me/?text=${text}`, "_blank");
}
async function shareNative() {
if (navigator.share) {
await navigator.share({ title: `${displayName}'s baby picks`, url: profileUrl });
}
}
function resetProductForm() { function resetProductForm() {
setPTitle(""); setPUrl(""); setPDesc(""); setPImageUrl(""); setPCategory("general"); setPTitle(""); setPUrl(""); setPDesc(""); setPImageUrl(""); setPCategory("general");
setShowAddProduct(false); setEditingProduct(null); setShowAddProduct(false); setEditingProduct(null);
@ -162,65 +187,93 @@ export default function ProfileSettingsPage() {
</div> </div>
<div className="px-4 space-y-4"> <div className="px-4 space-y-4">
{/* Profile section */} {/* Profile section — collapsible */}
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-sm"> <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm overflow-hidden">
<h2 className="font-semibold text-gray-800 dark:text-white mb-4">Profile</h2> {/* Always-visible header row */}
<label className="block text-sm text-gray-500 dark:text-gray-400 mb-1">Display Name</label>
<input
value={displayName}
onChange={e => setDisplayName(e.target.value)}
className={`${inputClass} mb-3`}
placeholder="Priya Sharma"
/>
<label className="block text-sm text-gray-500 dark:text-gray-400 mb-1">Your Page URL</label>
<div className="flex items-center gap-1 border border-gray-200 dark:border-gray-600 rounded-xl px-3 py-2.5 text-sm mb-1 bg-white dark:bg-gray-700 focus-within:ring-2 focus-within:ring-rose-300">
<span className="text-gray-400 text-xs whitespace-nowrap">{baseUrl}/m/</span>
<input
value={slug}
onChange={e => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))}
className="flex-1 bg-transparent outline-none dark:text-white text-sm"
placeholder="priya-sharma"
/>
</div>
{slug && !slugValid && (
<p className="text-xs text-red-500 mb-2">340 chars, lowercase letters, numbers, hyphens only</p>
)}
<label className="block text-sm text-gray-500 dark:text-gray-400 mb-1 mt-3">
Bio <span className="text-gray-400">({bio.length}/200)</span>
</label>
<textarea
value={bio}
onChange={e => setBio(e.target.value.slice(0, 200))}
rows={3}
className={`${inputClass} mb-3 resize-none`}
placeholder="New mama sharing what works for us 💕"
/>
<label className="flex items-center gap-3 cursor-pointer mb-4">
<div
onClick={() => setIsPublic(v => !v)}
className={`w-10 h-6 rounded-full transition-colors relative cursor-pointer ${isPublic ? "bg-rose-400" : "bg-gray-300 dark:bg-gray-600"}`}
>
<div className={`absolute top-1 w-4 h-4 bg-white rounded-full shadow transition-transform ${isPublic ? "translate-x-5" : "translate-x-1"}`} />
</div>
<span className="text-sm text-gray-700 dark:text-gray-300">Make my page public</span>
</label>
<button <button
onClick={saveProfile} onClick={() => setProfileExpanded(v => !v)}
disabled={saving || !displayName || !slugValid} className="w-full flex items-center justify-between p-4"
className="w-full bg-rose-400 text-white rounded-xl py-2.5 font-medium text-sm disabled:opacity-50 active:scale-95 transition-transform"
> >
{saving ? "Saving…" : "Save Profile"} <div className="flex items-center gap-3">
<span className="text-xl">👤</span>
<div className="text-left">
<p className="font-semibold text-gray-800 dark:text-white text-sm">
{displayName || "Set up your profile"}
</p>
{!profileExpanded && slug && (
<p className="text-xs text-gray-400">{baseUrl}/m/{slug}</p>
)}
</div>
</div>
<span className={`text-gray-400 text-sm transition-transform ${profileExpanded ? "rotate-180" : ""}`}></span>
</button> </button>
{saveMsg && ( {/* Expanded form */}
<p className={`text-xs mt-2 text-center ${saveMsg.startsWith("Saved") ? "text-green-600 dark:text-green-400" : "text-red-500"}`}> {profileExpanded && (
{saveMsg} <div className="px-4 pb-4 space-y-3 border-t border-gray-100 dark:border-gray-700 pt-3">
</p> <div>
<label className="block text-sm text-gray-500 dark:text-gray-400 mb-1">Display Name</label>
<input
value={displayName}
onChange={e => setDisplayName(e.target.value)}
className={inputClass}
placeholder="Priya Sharma"
/>
</div>
<div>
<label className="block text-sm text-gray-500 dark:text-gray-400 mb-1">Your Page URL</label>
<div className="flex items-center gap-1 border border-gray-200 dark:border-gray-600 rounded-xl px-3 py-2.5 text-sm bg-white dark:bg-gray-700 focus-within:ring-2 focus-within:ring-rose-300">
<span className="text-gray-400 text-xs whitespace-nowrap">{baseUrl}/m/</span>
<input
value={slug}
onChange={e => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))}
className="flex-1 bg-transparent outline-none dark:text-white text-sm"
placeholder="priya-sharma"
/>
</div>
{slug && !slugValid && (
<p className="text-xs text-red-500 mt-1">340 chars, lowercase letters, numbers, hyphens only</p>
)}
</div>
<div>
<label className="block text-sm text-gray-500 dark:text-gray-400 mb-1">
Bio <span className="text-gray-400">({bio.length}/200)</span>
</label>
<textarea
value={bio}
onChange={e => setBio(e.target.value.slice(0, 200))}
rows={3}
className={`${inputClass} resize-none`}
placeholder="New mama sharing what works for us 💕"
/>
</div>
<label className="flex items-center gap-3 cursor-pointer">
<div
onClick={() => setIsPublic(v => !v)}
className={`w-10 h-6 rounded-full transition-colors relative cursor-pointer ${isPublic ? "bg-rose-400" : "bg-gray-300 dark:bg-gray-600"}`}
>
<div className={`absolute top-1 w-4 h-4 bg-white rounded-full shadow transition-transform ${isPublic ? "translate-x-5" : "translate-x-1"}`} />
</div>
<span className="text-sm text-gray-700 dark:text-gray-300">Make my page public</span>
</label>
<button
onClick={saveProfile}
disabled={saving || !displayName || !slugValid}
className="w-full bg-rose-400 text-white rounded-xl py-2.5 font-medium text-sm disabled:opacity-50 active:scale-95 transition-transform"
>
{saving ? "Saving…" : "Save Profile"}
</button>
{saveMsg && (
<p className={`text-xs text-center ${saveMsg.startsWith("Saved") ? "text-green-600 dark:text-green-400" : "text-red-500"}`}>
{saveMsg}
</p>
)}
</div>
)} )}
</div> </div>
@ -228,12 +281,58 @@ export default function ProfileSettingsPage() {
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-sm"> <div className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-sm">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="font-semibold text-gray-800 dark:text-white">Product Recommendations</h2> <h2 className="font-semibold text-gray-800 dark:text-white">Product Recommendations</h2>
<button <div className="flex items-center gap-2">
onClick={() => { resetProductForm(); setShowAddProduct(true); }} {/* Share button — only visible when profile has a slug */}
className="text-sm text-rose-500 font-medium" {slug && slugValid && (
> <div className="relative">
+ Add <button
</button> onClick={() => setShowShareSheet(v => !v)}
className="flex items-center gap-1 px-2.5 py-1.5 rounded-xl bg-rose-50 dark:bg-rose-900/20 text-rose-500 text-sm font-medium"
>
<span></span>
<span>Share</span>
</button>
{showShareSheet && (
<>
<div className="fixed inset-0 z-30" onClick={() => setShowShareSheet(false)} />
<div className="absolute right-0 top-9 z-40 bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-700 p-3 w-52 space-y-1">
<p className="text-xs text-gray-400 px-2 pb-1">Share your page</p>
<button
onClick={() => { copyLink(); setShowShareSheet(false); }}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 text-sm text-gray-700 dark:text-gray-200"
>
<span className="text-lg">{copied ? "✅" : "📋"}</span>
{copied ? "Copied!" : "Copy link"}
</button>
<button
onClick={() => { shareViaWhatsApp(); setShowShareSheet(false); }}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 text-sm text-gray-700 dark:text-gray-200"
>
<span className="text-lg">💬</span>
WhatsApp
</button>
{typeof navigator !== "undefined" && "share" in navigator && (
<button
onClick={() => { shareNative(); setShowShareSheet(false); }}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 text-sm text-gray-700 dark:text-gray-200"
>
<span className="text-lg">📤</span>
More options
</button>
)}
</div>
</>
)}
</div>
)}
<button
onClick={() => { resetProductForm(); setShowAddProduct(true); }}
className="text-sm text-rose-500 font-medium px-2.5 py-1.5"
>
+ Add
</button>
</div>
</div> </div>
{/* Add/Edit form */} {/* Add/Edit form */}