Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | 6x 6x 6x 6x 6x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 90x 90x 1x 1x 1x 1x 1x | import React from "react";
import { Drawer } from "vaul";
import { Link, useRouterState } from "@tanstack/react-router";
import {
BarChart3,
User,
Users,
Globe,
FileText,
Settings,
MessageSquare,
LogOut,
Sun,
Moon,
Monitor,
Clapperboard,
Store,
} from "lucide-react";
import { useAuth } from "../../lib/auth-context";
import { useTheme, type Theme } from "../../lib/theme-context";
import { usePro } from "../../lib/pro-context";
import { useSocialDrafts } from "../../hooks/queries/useSocialDraftQueries";
import { MARKETPLACE_URL } from "../../lib/env";
type MoreSheetProps = {
open: boolean;
onClose: () => void;
};
const themeIcons: Record<Theme, typeof Sun> = {
light: Sun,
dark: Moon,
system: Monitor,
};
const themeLabels: Record<Theme, string> = {
light: "Light",
dark: "Dark",
system: "System",
};
const themeCycle: Record<Theme, Theme> = {
light: "dark",
dark: "system",
system: "light",
};
const MORE_ITEMS = [
{ to: "/analytics", icon: BarChart3, label: "Analytics" },
{ to: "/profile", icon: User, label: "Profile" },
{ to: "/team", icon: Users, label: "Team" },
{ to: "/website", icon: Globe, label: "Website" },
{ to: "/blogs", icon: FileText, label: "Blog" },
{ to: "/settings", icon: Settings, label: "Settings" },
] as const;
// vaul 1.x PortalProps derives from ComponentPropsWithoutRef which drops
// children in React 19 — cast once here rather than suppressing at every site.
const DrawerPortal = Drawer.Portal as React.ComponentType<React.PropsWithChildren>;
export function MoreSheet({ open, onClose }: MoreSheetProps) {
const routerState = useRouterState();
const { signOut, hasProAccess } = useAuth();
const { theme, setTheme } = useTheme();
const ThemeIcon = themeIcons[theme];
const { pro, proId, isLoading: isProLoading } = usePro();
const { data: socialDraftsData } = useSocialDrafts(proId ?? null);
const socialPendingCount = socialDraftsData?.pendingCount ?? 0;
// #372 + #425: mobile Pros need a back-link to their own public profile,
// not just the marketplace homepage. Same gate idiom as sidebar.tsx — only
// rendered after the pro is published AND has a slug. Same-tab navigation
// (rather than new-tab as on desktop) matches mobile expectations: tab
// switching is awkward on phones and the portal is reachable via Back.
const isProPublished = pro?.status === "published";
const marketplaceProfileUrl =
!isProLoading && isProPublished && pro?.slug
? `${MARKETPLACE_URL}/pros/${pro.slug}`
: null;
return (
<Drawer.Root
open={open}
onOpenChange={(o) => {
if (!o) onClose();
}}
>
<DrawerPortal>
<Drawer.Overlay className="fixed inset-0 z-[55] bg-black/50" />
<Drawer.Content className="fixed inset-x-0 bottom-0 z-[55] rounded-t-2xl bg-background-elevated pb-safe">
<div className="mx-auto mt-3 mb-2 h-1 w-10 flex-shrink-0 rounded-full bg-border-default" />
<Drawer.Title className="sr-only">More options</Drawer.Title>
<nav className="px-2 pb-4">
<ul className="grid grid-cols-3 gap-1">
{MORE_ITEMS.map(({ to, icon: Icon, label }) => {
const isActive =
routerState.location.pathname.startsWith(to);
return (
<li key={to}>
<Link
to={to}
onClick={onClose}
className={`flex flex-col items-center gap-1.5 rounded-xl px-3 py-3 text-xs transition-colors ${
isActive
? "bg-primary-100 text-primary-700"
: "text-foreground-muted hover:bg-background-muted"
}`}
>
<Icon className="h-5 w-5" />
{label}
</Link>
</li>
);
})}
{/* Social Studio — fixed slot keeps the 3-col grid balanced
(6 base items + Social Studio = 7 → row 3 has 1 tile;
pair with Marketplace below when published to fill row 3). */}
<li>
<Link
to="/social-studio"
onClick={onClose}
className={`relative flex flex-col items-center gap-1.5 rounded-xl px-3 py-3 text-xs transition-colors ${
routerState.location.pathname.startsWith("/social-studio")
? "bg-primary-100 text-primary-700"
: "text-foreground-muted hover:bg-background-muted"
}`}
>
<div className="relative">
<Clapperboard className="h-5 w-5" />
{socialPendingCount > 0 && (
<span className="absolute -top-1.5 -right-2 bg-primary-600 text-white text-[8px] font-bold px-1 py-0.5 rounded-full min-w-[14px] text-center leading-tight">
{socialPendingCount}
</span>
)}
</div>
Social Studio
</Link>
</li>
{/* Marketplace — deep-link to the pro's public profile.
External anchor, not a TanStack <Link>, because the
marketplace is a separate Astro app. Same gate idiom
as desktop sidebar (hasProAccess + published + slug)
so the link surface is identical across platforms. */}
{hasProAccess && marketplaceProfileUrl && (
<li>
<a
href={marketplaceProfileUrl}
onClick={onClose}
className="flex flex-col items-center gap-1.5 rounded-xl px-3 py-3 text-xs text-foreground-muted hover:bg-background-muted transition-colors"
>
<Store className="h-5 w-5" />
Marketplace
</a>
</li>
)}
</ul>
<div className="mt-3 border-t border-border-default pt-3 px-1 flex gap-1">
<button
type="button"
onClick={() => {
onClose();
window.dispatchEvent(new CustomEvent("open-feedback"));
}}
className="flex flex-1 flex-col items-center gap-1.5 rounded-xl px-2 py-3 text-xs text-foreground-muted hover:bg-background-muted transition-colors"
>
<MessageSquare className="h-5 w-5" />
Feedback
</button>
<button
type="button"
onClick={() => setTheme(themeCycle[theme])}
className="flex flex-1 flex-col items-center gap-1.5 rounded-xl px-2 py-3 text-xs text-foreground-muted hover:bg-background-muted transition-colors"
>
<ThemeIcon className="h-5 w-5" />
{themeLabels[theme]}
</button>
<button
type="button"
onClick={() => {
onClose();
signOut();
}}
className="flex flex-1 flex-col items-center gap-1.5 rounded-xl px-2 py-3 text-xs text-error hover:bg-error/10 transition-colors"
>
<LogOut className="h-5 w-5" />
Logout
</button>
</div>
</nav>
</Drawer.Content>
</DrawerPortal>
</Drawer.Root>
);
}
|