Repository Public
1"use client";2import { useLocale, useT } from "@/i18n";3import { publicDict } from "@/i18n/public";4import { Empty, Icon } from "@/components/ui";5import { tzCity } from "@/lib/format";6import { LOCATION_ICON } from "./booking-flow";7import type { PublicProfile } from "@/lib/api/public";89/** Owner portrait inside the accent ring; initials when no picture was uploaded. */10export function Portrait({ name, hasAvatar, size }: { name: string; hasAvatar: boolean; size: number }) {11 return (12 <span className="ring-accent shrink-0">13 {hasAvatar ? <img src="/api/v1/public/avatar" alt="" width={size} height={size} className="object-cover bg-elev" style={{ width: size, height: size }} />14 : <span className="avatar" style={{ width: size, height: size, fontSize: size * 0.4, background: "var(--elev)", color: "var(--ink)" }} aria-hidden>{name.slice(0, 1).toUpperCase()}</span>}15 </span>16 );17}1819export function Home({ data }: { data: PublicProfile }) {20 const t = useT(publicDict);21 const locale = useLocale();22 const en = locale === "en";23 if (!data.configured) return <div className="card p-6"><Empty title={t("notConfiguredTitle")}><p>{t("notConfiguredBody")}</p></Empty></div>;24 const p = data.profile;25 const headline = (en && p.headlineEn) || p.headline;26 const bio = (en && p.bioEn) || p.bio;27 // The sentence comes from the dictionary; the name inside it is painted with the accent.28 const [before, after] = t("bookWith", { name: "\u0000" }).split("\u0000");29 return (30 <div className="grid gap-8 sm:gap-10">31 <section className="stagger flex flex-col items-center text-center pt-3 sm:pt-6">32 <div style={{ "--i": 0 } as React.CSSProperties}><Portrait name={p.name} hasAvatar={p.hasAvatar} size={104} /></div>33 <h1 className="mt-5 max-w-2xl" style={{ "--i": 1 } as React.CSSProperties}><span className="block text-[19px] sm:text-[22px] font-semibold t-mute tracking-tight">{before.trim()}</span>{" "}<span className="t-title t-grad block !text-[40px] sm:!text-[56px] mt-1 pb-1">{p.name}</span>{after}</h1>34 {headline ? <p className="mt-3 text-[18px] sm:text-[20px] t-mute max-w-xl text-balance" style={{ "--i": 2 } as React.CSSProperties}>{headline}</p> : null}35 {bio ? <p className="mt-3 whitespace-pre-line max-w-xl text-pretty" style={{ "--i": 3 } as React.CSSProperties}>{bio}</p> : null}36 <p className="chip mt-4 !bg-surface border border-line shadow-e1" style={{ "--i": 4 } as React.CSSProperties}><Icon name="globe" size={15} className="t-mute" />{tzCity(p.timezone)}</p>37 </section>38 <section aria-labelledby="types-title">39 <h2 id="types-title" className="t-eyebrow mb-3 px-1 text-center sm:text-left">{t("chooseType")}</h2>40 {data.types.length === 0 ? <div className="card p-6"><Empty title={t("noTypesTitle")}><p>{t("noTypesBody")}</p></Empty></div> : (41 <ul className="stagger grid gap-3.5 sm:grid-cols-2">42 {data.types.map((ty, i) => (43 <li key={ty.slug} style={{ "--i": i + 4 } as React.CSSProperties}>44 <a href={`/book/${ty.slug}`} className="card card-press p-5 flex flex-col gap-3.5 no-underline h-full group relative overflow-hidden"45 style={{ backgroundImage: `radial-gradient(130% 90% at 0% 0%, color-mix(in srgb, ${ty.color} 13%, transparent), transparent 62%)` }}>46 <span className="flex items-center gap-3.5">47 <span className="w-[52px] h-[52px] rounded-[17px] flex items-center justify-center shrink-0 text-white shadow-e2" style={{ background: `linear-gradient(140deg, ${ty.color}, color-mix(in srgb, ${ty.color} 62%, #000))` }} aria-hidden><Icon name={LOCATION_ICON[ty.locationKind]} size={24} /></span>48 <span className="min-w-0 flex-1">49 <span className="t-headline !text-[19px] block leading-snug">{(en && ty.titleEn) || ty.title}</span>50 <span className="mt-0.5 flex flex-wrap items-center gap-x-2 text-[14px] t-mute"><span>{t("minutes", { n: ty.durationMin })}</span><span aria-hidden>·</span><span>{t(ty.locationKind)}</span></span>51 </span>52 <span className="w-10 h-10 rounded-full bg-elev flex items-center justify-center shrink-0 transition-all duration-300 group-hover:translate-x-0.5 group-hover:bg-primary group-hover:text-onprimary" aria-hidden><Icon name="right" size={18} /></span>53 </span>54 {((en && ty.descriptionEn) || ty.description) ? <span className="block text-[15px] line-clamp-3">{(en && ty.descriptionEn) || ty.description}</span> : null}55 <span className="mt-auto flex"><span className={`badge badge-dot ${ty.approval === "auto" ? "badge-ok" : "badge-mute"}`}>{ty.approval === "auto" ? t("approvalAuto") : t("approvalManual")}</span></span>56 </a>57 </li>58 ))}59 </ul>60 )}61 </section>62 </div>63 );64}65