import type {ComponentType, ReactNode} from 'react'; import {Link, NavLink, useLocation, useNavigate} from 'react-router'; import {LayoutGrid, LogIn, LogOut, Receipt, Search, Store, Wand2} from 'lucide-react'; import {cn} from '@/lib/utils'; import {userLabel, useAuthStore} from '@/stores/auth'; export type NavItem = { to: string; /** 활성 표시용 경로. 쿼리스트링이 붙은 `to` 로는 pathname 을 비교할 수 없다. */ match: string; label: string; icon: ComponentType<{className?: string}>; }; /** * ★ 메뉴를 이 파일에 하드코딩하지 않는다 — 앱마다 다르고, **섞이면 새어 나간다.** * 내부 메뉴(`/places`, `/local-content`)를 여기 두면 그 경로 이름이 사장님 번들에 * 문자열로 남는다(실측: 앱을 가른 뒤에도 dist 에서 `local-content` 가 나왔다). * 앱을 가른 이유가 그거라 메뉴도 앱이 들고 온다. 내부 메뉴는 `admin/src/app/router.tsx`. * * ★ 빌더는 `?new=1` 로 간다. 그냥 `/builder` 로 보내면 저장된 위저드 상태가 복원돼 * 지난번에 편집하던 가게의 에디터가 뜬다(BuilderPage 주석) — 새로 만들러 누른 사람에게는 * 남의 화면이다. */ const OWNER_NAV: NavItem[] = [ {to: '/sites', match: '/sites', label: '내 사이트', icon: Store}, {to: '/builder?new=1', match: '/builder', label: '새 사이트', icon: Wand2}, /* * ★ 요금은 **로그인한 뒤에도** 갈 길이 있어야 한다 (2026-09-04, 사장님 지적) * /pricing 은 살아 있는데 링크가 MarketingShell 과 랜딩에만 있었다. 둘 다 로그인하면 * 안 보이는 화면이라(로그인하면 / 가 /sites 로 간다), 사장님은 주소를 직접 쳐야 했다. * 요금은 쓰는 도중에 확인하는 값이지 가입 전에만 보는 값이 아니다. * ★ 목적지가 MarketingShell(사이드바 없는 문서형)이라 사이드바가 사라진다. 그래도 갇히지 * 않는다 — 저쪽 헤더가 로그인 상태를 알아보고 [내 사이트] 버튼을 세운다. */ {to: '/pricing', match: '/pricing', label: '요금', icon: Receipt}, ]; export function AppShell({children, nav = OWNER_NAV}: {children: ReactNode; nav?: NavItem[]}) { const user = useAuthStore((s) => s.user); const signOut = useAuthStore((s) => s.signOut); const location = useLocation(); const navigate = useNavigate(); return (
{children}
); } export function PageContainer({ title, description, actions, children, }: { title: string; description?: string; actions?: ReactNode; children: ReactNode; }) { return (

{title}

{description &&

{description}

}
{actions &&
{actions}
}
{children}
); } export function EmptyState({ icon: Icon = LayoutGrid, title, description, action, }: { icon?: typeof Search; title: string; description?: string; action?: ReactNode; }) { return (

{title}

{description &&

{description}

} {action &&
{action}
}
); }