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 [saving, setSaving] = useState(false);
const [saveMsg, setSaveMsg] = useState("");
const [profileExpanded, setProfileExpanded] = useState(true);
const [showShareSheet, setShowShareSheet] = useState(false);
const [copied, setCopied] = useState(false);
const [slug, setSlug] = useState("");
const [displayName, setDisplayName] = useState("");
@ -58,12 +61,15 @@ export default function ProfileSettingsPage() {
setDisplayName(pd.profile.display_name);
setBio(pd.profile.bio ?? "");
setIsPublic(pd.profile.is_public);
setProfileExpanded(false); // already set up — start collapsed
}
setProducts(prods.items || []);
setLoading(false);
}).catch(() => setLoading(false));
}, []);
const profileUrl = typeof window !== "undefined" && slug ? `${window.location.origin}/m/${slug}` : "";
async function saveProfile() {
setSaving(true);
setSaveMsg("");
@ -75,13 +81,32 @@ export default function ProfileSettingsPage() {
const d = await r.json();
if (r.ok) {
setProfile(d.profile);
setSaveMsg(`Saved! View at /m/${d.profile.slug}`);
setSaveMsg("Saved!");
setProfileExpanded(false); // collapse after save
} else {
setSaveMsg(d.error || "Save failed");
}
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() {
setPTitle(""); setPUrl(""); setPDesc(""); setPImageUrl(""); setPCategory("general");
setShowAddProduct(false); setEditingProduct(null);
@ -162,20 +187,43 @@ export default function ProfileSettingsPage() {
</div>
<div className="px-4 space-y-4">
{/* Profile section */}
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-sm">
<h2 className="font-semibold text-gray-800 dark:text-white mb-4">Profile</h2>
{/* Profile section — collapsible */}
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm overflow-hidden">
{/* Always-visible header row */}
<button
onClick={() => setProfileExpanded(v => !v)}
className="w-full flex items-center justify-between p-4"
>
<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>
{/* Expanded form */}
{profileExpanded && (
<div className="px-4 pb-4 space-y-3 border-t border-gray-100 dark:border-gray-700 pt-3">
<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} mb-3`}
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 mb-1 bg-white dark:bg-gray-700 focus-within:ring-2 focus-within:ring-rose-300">
<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}
@ -185,21 +233,24 @@ export default function ProfileSettingsPage() {
/>
</div>
{slug && !slugValid && (
<p className="text-xs text-red-500 mb-2">340 chars, lowercase letters, numbers, hyphens only</p>
<p className="text-xs text-red-500 mt-1">340 chars, lowercase letters, numbers, hyphens only</p>
)}
</div>
<label className="block text-sm text-gray-500 dark:text-gray-400 mb-1 mt-3">
<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} mb-3 resize-none`}
className={`${inputClass} resize-none`}
placeholder="New mama sharing what works for us 💕"
/>
</div>
<label className="flex items-center gap-3 cursor-pointer mb-4">
<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"}`}
@ -218,23 +269,71 @@ export default function ProfileSettingsPage() {
</button>
{saveMsg && (
<p className={`text-xs mt-2 text-center ${saveMsg.startsWith("Saved") ? "text-green-600 dark:text-green-400" : "text-red-500"}`}>
<p className={`text-xs text-center ${saveMsg.startsWith("Saved") ? "text-green-600 dark:text-green-400" : "text-red-500"}`}>
{saveMsg}
</p>
)}
</div>
)}
</div>
{/* Products section */}
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h2 className="font-semibold text-gray-800 dark:text-white">Product Recommendations</h2>
<div className="flex items-center gap-2">
{/* Share button — only visible when profile has a slug */}
{slug && slugValid && (
<div className="relative">
<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"
className="text-sm text-rose-500 font-medium px-2.5 py-1.5"
>
+ Add
</button>
</div>
</div>
{/* Add/Edit form */}
{showAddProduct && (