InterstitialLayout.tsx144 lines · main
| 1 | import { motion } from 'framer-motion' |
| 2 | import { ArrowRightLeft } from 'lucide-react' |
| 3 | import type { PropsWithChildren, ReactNode } from 'react' |
| 4 | import { Card, CardContent, CardHeader, cn } from 'ui' |
| 5 | |
| 6 | import { ProfileImage } from '@/components/ui/ProfileImage' |
| 7 | import { BASE_PATH } from '@/lib/constants' |
| 8 | |
| 9 | const MotionCard = motion.create(Card) |
| 10 | |
| 11 | interface InterstitialLayoutProps { |
| 12 | logo?: ReactNode |
| 13 | title?: ReactNode |
| 14 | description?: ReactNode |
| 15 | containerClassName?: string |
| 16 | cardClassName?: string |
| 17 | titleClassName?: string |
| 18 | descriptionClassName?: string |
| 19 | } |
| 20 | |
| 21 | export const InterstitialLayout = ({ |
| 22 | logo, |
| 23 | title, |
| 24 | description, |
| 25 | containerClassName, |
| 26 | cardClassName, |
| 27 | titleClassName, |
| 28 | descriptionClassName, |
| 29 | children, |
| 30 | }: PropsWithChildren<InterstitialLayoutProps>) => { |
| 31 | const TitleElement = typeof title === 'string' ? 'h1' : 'div' |
| 32 | const DescriptionElement = typeof description === 'string' ? 'p' : 'div' |
| 33 | |
| 34 | return ( |
| 35 | <div |
| 36 | className={cn( |
| 37 | 'flex min-h-screen w-full items-center justify-center bg-studio px-2 py-6', |
| 38 | containerClassName |
| 39 | )} |
| 40 | > |
| 41 | <MotionCard |
| 42 | layout="size" |
| 43 | transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }} |
| 44 | className={cn('mx-auto w-full max-w-[400px] overflow-hidden', cardClassName)} |
| 45 | > |
| 46 | {(logo || title || description) && ( |
| 47 | <CardHeader className="items-center gap-0 space-y-0 border-0 px-6 py-6 text-center font-normal [--card-padding-x:1.5rem]"> |
| 48 | {logo && <div className="mb-4 flex justify-center">{logo}</div>} |
| 49 | {(title || description) && ( |
| 50 | <div className="flex flex-col items-center gap-1"> |
| 51 | {title && ( |
| 52 | <TitleElement |
| 53 | className={cn( |
| 54 | 'font-sans text-lg font-medium tracking-tight text-balance text-foreground', |
| 55 | titleClassName |
| 56 | )} |
| 57 | > |
| 58 | {title} |
| 59 | </TitleElement> |
| 60 | )} |
| 61 | {description && ( |
| 62 | <DescriptionElement |
| 63 | className={cn( |
| 64 | '!m-0 px-3 text-sm leading-tight !text-balance text-foreground-lighter', |
| 65 | descriptionClassName |
| 66 | )} |
| 67 | > |
| 68 | {description} |
| 69 | </DescriptionElement> |
| 70 | )} |
| 71 | </div> |
| 72 | )} |
| 73 | </CardHeader> |
| 74 | )} |
| 75 | {children} |
| 76 | </MotionCard> |
| 77 | </div> |
| 78 | ) |
| 79 | } |
| 80 | |
| 81 | export const LogoBox = ({ children, className }: { children: ReactNode; className?: string }) => ( |
| 82 | <div |
| 83 | className={cn( |
| 84 | 'flex size-12 items-center justify-center overflow-hidden rounded-xl border bg-muted', |
| 85 | className |
| 86 | )} |
| 87 | > |
| 88 | {children} |
| 89 | </div> |
| 90 | ) |
| 91 | |
| 92 | export const LogoPair = ({ left, right }: { left: ReactNode; right: ReactNode }) => ( |
| 93 | <div className="flex items-center justify-center gap-2.5"> |
| 94 | {left} |
| 95 | <ArrowRightLeft className="size-4 text-foreground-muted" /> |
| 96 | {right} |
| 97 | </div> |
| 98 | ) |
| 99 | |
| 100 | export const BrivenLogo = () => ( |
| 101 | <LogoBox> |
| 102 | <img alt="Briven" src={`${BASE_PATH}/img/briven-logo.svg`} className="size-7" /> |
| 103 | </LogoBox> |
| 104 | ) |
| 105 | |
| 106 | export const PartnerLogo = ({ src, alt }: { src: string; alt: string }) => ( |
| 107 | <LogoBox> |
| 108 | <img alt={alt} src={src} className="size-full object-cover" /> |
| 109 | </LogoBox> |
| 110 | ) |
| 111 | |
| 112 | export const InterstitialAccountRow = ({ |
| 113 | avatarUrl, |
| 114 | displayName, |
| 115 | action, |
| 116 | className, |
| 117 | }: { |
| 118 | avatarUrl?: string |
| 119 | displayName?: string |
| 120 | action?: ReactNode |
| 121 | className?: string |
| 122 | }) => ( |
| 123 | <Card className={cn('shadow-none', !action && 'border-muted bg-surface-200/50', className)}> |
| 124 | <CardContent |
| 125 | className={cn( |
| 126 | 'flex gap-3 border-none', |
| 127 | action ? 'items-center px-4 py-3' : 'items-start p-3' |
| 128 | )} |
| 129 | > |
| 130 | <ProfileImage |
| 131 | src={avatarUrl} |
| 132 | alt={displayName} |
| 133 | className="size-8 flex-shrink-0 rounded-full border border-muted" |
| 134 | /> |
| 135 | <div className="min-w-0 flex-1"> |
| 136 | <p className="text-xs text-foreground-light">Signed in as</p> |
| 137 | <p className="truncate text-sm text-foreground"> |
| 138 | {displayName || <span className="invisible">Loading account</span>} |
| 139 | </p> |
| 140 | </div> |
| 141 | {action} |
| 142 | </CardContent> |
| 143 | </Card> |
| 144 | ) |