@braintwopoint0/playback-commons 0.1.21 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ import { Resend } from 'resend';
2
+
3
+ interface SendEmailResult {
4
+ success: boolean;
5
+ error?: string;
6
+ }
7
+ /**
8
+ * Returns a memoized Resend client for the given API key. Callers should hold the
9
+ * returned client at module scope so one instance is reused across requests within
10
+ * a Next.js runtime.
11
+ */
12
+ declare function createResendClient(apiKey: string | undefined): Resend | null;
13
+ type AudienceSyncOptions = {
14
+ role?: string | null;
15
+ source?: string | null;
16
+ firstName?: string | null;
17
+ lastName?: string | null;
18
+ };
19
+ /**
20
+ * Adds a subscriber to a Resend audience. Returns the Resend contact id on success,
21
+ * null on any failure (misconfigured / API error / network). Callers should persist
22
+ * success/failure and rely on a reconciliation job to retry nulls.
23
+ *
24
+ * Intentionally does NOT throw — failures are operational, not business logic, and
25
+ * the caller's primary persistence (Supabase row) is already successful by the time
26
+ * this is invoked.
27
+ */
28
+ declare function syncToResendAudience(params: {
29
+ client: Resend | null;
30
+ audienceId: string | undefined;
31
+ email: string;
32
+ options?: AudienceSyncOptions;
33
+ }): Promise<string | null>;
34
+ /**
35
+ * Remove a contact from a Resend audience (e.g. on unsubscribe). Returns true on
36
+ * success; false if not configured or the call fails. Safe to retry.
37
+ */
38
+ declare function removeFromResendAudience(params: {
39
+ client: Resend | null;
40
+ audienceId: string | undefined;
41
+ contactId: string;
42
+ }): Promise<boolean>;
43
+
44
+ export { type AudienceSyncOptions, type SendEmailResult, createResendClient, removeFromResendAudience, syncToResendAudience };
@@ -0,0 +1,46 @@
1
+ // src/email/index.ts
2
+ import { Resend } from "resend";
3
+ var clientCache = /* @__PURE__ */ new Map();
4
+ function createResendClient(apiKey) {
5
+ if (!apiKey) return null;
6
+ const cached = clientCache.get(apiKey);
7
+ if (cached) return cached;
8
+ const client = new Resend(apiKey);
9
+ clientCache.set(apiKey, client);
10
+ return client;
11
+ }
12
+ async function syncToResendAudience(params) {
13
+ const { client, audienceId, email, options } = params;
14
+ if (!client || !audienceId) return null;
15
+ try {
16
+ const res = await client.contacts.create({
17
+ audienceId,
18
+ email,
19
+ unsubscribed: false,
20
+ ...options?.firstName ? { firstName: options.firstName } : {},
21
+ ...options?.lastName ? { lastName: options.lastName } : {}
22
+ });
23
+ const id = res.data?.id ?? res.id ?? null;
24
+ return id;
25
+ } catch (err) {
26
+ console.error("[commons/email] audience sync failed", err);
27
+ return null;
28
+ }
29
+ }
30
+ async function removeFromResendAudience(params) {
31
+ const { client, audienceId, contactId } = params;
32
+ if (!client || !audienceId) return false;
33
+ try {
34
+ await client.contacts.remove({ audienceId, id: contactId });
35
+ return true;
36
+ } catch (err) {
37
+ console.error("[commons/email] audience remove failed", err);
38
+ return false;
39
+ }
40
+ }
41
+ export {
42
+ createResendClient,
43
+ removeFromResendAudience,
44
+ syncToResendAudience
45
+ };
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/email/index.ts"],"sourcesContent":["// Shared email primitives for PLAYBACK and PLAYHUB.\n//\n// Transactional templates (welcome, invoice, admin invite, etc.) stay in each app's\n// `src/lib/email/` — they're brand/context specific. What lives here are the pieces\n// that both apps need identically:\n//\n// - Resend client factory (memoized per API key)\n// - Marketing-list audience sync (for footer signups, campaign lists)\n// - Shared result type\n//\n// `escapeHtml` for safe interpolation already lives in `@braintwopoint0/playback-commons/security`.\n\nimport { Resend } from 'resend'\n\nexport interface SendEmailResult {\n success: boolean\n error?: string\n}\n\nconst clientCache = new Map<string, Resend>()\n\n/**\n * Returns a memoized Resend client for the given API key. Callers should hold the\n * returned client at module scope so one instance is reused across requests within\n * a Next.js runtime.\n */\nexport function createResendClient(apiKey: string | undefined): Resend | null {\n if (!apiKey) return null\n const cached = clientCache.get(apiKey)\n if (cached) return cached\n const client = new Resend(apiKey)\n clientCache.set(apiKey, client)\n return client\n}\n\nexport type AudienceSyncOptions = {\n role?: string | null\n source?: string | null\n firstName?: string | null\n lastName?: string | null\n}\n\n/**\n * Adds a subscriber to a Resend audience. Returns the Resend contact id on success,\n * null on any failure (misconfigured / API error / network). Callers should persist\n * success/failure and rely on a reconciliation job to retry nulls.\n *\n * Intentionally does NOT throw — failures are operational, not business logic, and\n * the caller's primary persistence (Supabase row) is already successful by the time\n * this is invoked.\n */\nexport async function syncToResendAudience(params: {\n client: Resend | null\n audienceId: string | undefined\n email: string\n options?: AudienceSyncOptions\n}): Promise<string | null> {\n const { client, audienceId, email, options } = params\n if (!client || !audienceId) return null\n\n try {\n const res = await client.contacts.create({\n audienceId,\n email,\n unsubscribed: false,\n ...(options?.firstName ? { firstName: options.firstName } : {}),\n ...(options?.lastName ? { lastName: options.lastName } : {}),\n })\n // Resend's typing varies across versions — defensively read the id.\n const id =\n (res as { data?: { id?: string } }).data?.id ??\n (res as { id?: string }).id ??\n null\n return id\n } catch (err) {\n console.error('[commons/email] audience sync failed', err)\n return null\n }\n}\n\n/**\n * Remove a contact from a Resend audience (e.g. on unsubscribe). Returns true on\n * success; false if not configured or the call fails. Safe to retry.\n */\nexport async function removeFromResendAudience(params: {\n client: Resend | null\n audienceId: string | undefined\n contactId: string\n}): Promise<boolean> {\n const { client, audienceId, contactId } = params\n if (!client || !audienceId) return false\n try {\n await client.contacts.remove({ audienceId, id: contactId })\n return true\n } catch (err) {\n console.error('[commons/email] audience remove failed', err)\n return false\n }\n}\n"],"mappings":";AAYA,SAAS,cAAc;AAOvB,IAAM,cAAc,oBAAI,IAAoB;AAOrC,SAAS,mBAAmB,QAA2C;AAC5E,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,YAAY,IAAI,MAAM;AACrC,MAAI,OAAQ,QAAO;AACnB,QAAM,SAAS,IAAI,OAAO,MAAM;AAChC,cAAY,IAAI,QAAQ,MAAM;AAC9B,SAAO;AACT;AAkBA,eAAsB,qBAAqB,QAKhB;AACzB,QAAM,EAAE,QAAQ,YAAY,OAAO,QAAQ,IAAI;AAC/C,MAAI,CAAC,UAAU,CAAC,WAAY,QAAO;AAEnC,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,SAAS,OAAO;AAAA,MACvC;AAAA,MACA;AAAA,MACA,cAAc;AAAA,MACd,GAAI,SAAS,YAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;AAAA,MAC7D,GAAI,SAAS,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IAC5D,CAAC;AAED,UAAM,KACH,IAAmC,MAAM,MACzC,IAAwB,MACzB;AACF,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,YAAQ,MAAM,wCAAwC,GAAG;AACzD,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,yBAAyB,QAI1B;AACnB,QAAM,EAAE,QAAQ,YAAY,UAAU,IAAI;AAC1C,MAAI,CAAC,UAAU,CAAC,WAAY,QAAO;AACnC,MAAI;AACF,UAAM,OAAO,SAAS,OAAO,EAAE,YAAY,IAAI,UAAU,CAAC;AAC1D,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,YAAQ,MAAM,0CAA0C,GAAG;AAC3D,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -404,6 +404,78 @@ declare function FadeIn({ children, className, delay, direction, ...props }: Fad
404
404
 
405
405
  declare function LumaSpin(): react_jsx_runtime.JSX.Element;
406
406
 
407
- declare function Footer(): react_jsx_runtime.JSX.Element;
407
+ type FooterLinkDef = {
408
+ label: string;
409
+ href: string;
410
+ external?: boolean;
411
+ };
412
+ type FooterColumnDef = {
413
+ title: string;
414
+ links: FooterLinkDef[];
415
+ };
416
+ type FooterSocialDef = {
417
+ label: string;
418
+ href: string;
419
+ src: string;
420
+ };
421
+ type FooterProps = {
422
+ columns?: FooterColumnDef[];
423
+ socials?: FooterSocialDef[];
424
+ newsletter?: boolean;
425
+ tagline?: string;
426
+ showTagline?: boolean;
427
+ logoSrc?: string;
428
+ logoAlt?: string;
429
+ siteName?: string;
430
+ creditHref?: string;
431
+ creditLabel?: string;
432
+ newsletterAction?: (email: string) => void | Promise<void>;
433
+ className?: string;
434
+ };
435
+ declare function Footer({ columns, socials, newsletter, tagline, showTagline, logoSrc, logoAlt, siteName, creditHref, creditLabel, newsletterAction, className, }?: FooterProps): react_jsx_runtime.JSX.Element;
408
436
 
409
- export { AnimatedTooltip, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, Button, type ButtonProps, Calendar, CalendarDayButton, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, DataRow, type DataRowProps, DatePicker, DateTimePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, EmptyState, type EmptyStateProps, FadeIn, type FadeInProps, FlipWords, Footer, HeroHighlight, Highlight, HoverCard, HoverCardDescription, HoverCardTitle, HoverEffect, Input, type InputProps, Label, LumaSpin, PageShell, type PageShellProps, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, SearchBar, type SearchBarProps, SectionCard, type SectionCardProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Skeleton, type StatItem, StatsGrid, type StatsGridProps, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, type TextareaProps, TimePicker, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, easeSmooth, fadeInDown, fadeInUp, hoverLift, hoverScale, pageTransition, springBounce, staggerContainer, staggerItem };
437
+ type FooterSocialLink = {
438
+ label: string;
439
+ href: string;
440
+ src: string;
441
+ };
442
+ type FooterCreditsBarProps = {
443
+ /**
444
+ * Social links to render on the left. Defaults to the PLAYBACK brand socials.
445
+ * Note: `src` paths are resolved by the consuming app's Next.js static file
446
+ * server, so the assets must exist in the app's /public directory.
447
+ */
448
+ socials?: FooterSocialLink[];
449
+ /**
450
+ * Company name used in the copyright line. Defaults to "PLAYBACK".
451
+ */
452
+ companyName?: string;
453
+ /**
454
+ * Credit link href. Defaults to BRAIN2.0's homepage.
455
+ */
456
+ creditHref?: string;
457
+ /**
458
+ * Show the "Built by BRAIN2.0" credit next to the socials. Defaults to true.
459
+ * The credit uses the Averta Semibold + Thin split-weight wordmark for
460
+ * "BRAIN2.0". Those @font-face declarations must be loaded by the consuming
461
+ * app's globals.css (AvertaStd-Semibold.ttf + AvertaStd-Thin.ttf).
462
+ */
463
+ showCredit?: boolean;
464
+ /**
465
+ * Starting year for the copyright range. Defaults to current year.
466
+ */
467
+ copyrightYear?: number;
468
+ /**
469
+ * Additional classes on the outer container.
470
+ */
471
+ className?: string;
472
+ };
473
+ /**
474
+ * Shared footer bottom-bar: social icons + BRAIN2.0 credit on the left,
475
+ * copyright right-aligned on desktop and centered on mobile.
476
+ *
477
+ * Use inside an existing footer container. Provides its own top hairline.
478
+ */
479
+ declare function FooterCreditsBar({ socials, companyName, creditHref, showCredit, copyrightYear, className, }?: FooterCreditsBarProps): react_jsx_runtime.JSX.Element;
480
+
481
+ export { AnimatedTooltip, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, Button, type ButtonProps, Calendar, CalendarDayButton, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, DataRow, type DataRowProps, DatePicker, DateTimePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, EmptyState, type EmptyStateProps, FadeIn, type FadeInProps, FlipWords, Footer, type FooterColumnDef, FooterCreditsBar, type FooterCreditsBarProps, type FooterLinkDef, type FooterProps, type FooterSocialDef, type FooterSocialLink, HeroHighlight, Highlight, HoverCard, HoverCardDescription, HoverCardTitle, HoverEffect, Input, type InputProps, Label, LumaSpin, PageShell, type PageShellProps, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, SearchBar, type SearchBarProps, SectionCard, type SectionCardProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Skeleton, type StatItem, StatsGrid, type StatsGridProps, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, type TextareaProps, TimePicker, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, easeSmooth, fadeInDown, fadeInUp, hoverLift, hoverScale, pageTransition, springBounce, staggerContainer, staggerItem };
package/dist/ui/index.js CHANGED
@@ -2364,69 +2364,361 @@ function LumaSpin() {
2364
2364
  }
2365
2365
 
2366
2366
  // src/ui/footer.tsx
2367
+ import * as React26 from "react";
2367
2368
  import Image2 from "next/image";
2368
2369
  import Link2 from "next/link";
2369
2370
  import { jsx as jsx35, jsxs as jsxs19 } from "react/jsx-runtime";
2370
- function Footer() {
2371
- return /* @__PURE__ */ jsxs19("footer", { className: "container mx-auto flex p-5 items-center justify-between border-t border-[var(--timberwolf)]", children: [
2372
- /* @__PURE__ */ jsxs19("div", { className: "flex gap-4", children: [
2373
- /* @__PURE__ */ jsx35(Link2, { href: "https://www.instagram.com/playback_global", target: "_blank", children: /* @__PURE__ */ jsx35(
2374
- Image2,
2375
- {
2376
- src: "/assets/instagram.png",
2377
- alt: "PLAYBACK Instagram",
2378
- height: 50,
2379
- width: 50,
2380
- className: "h-9 w-9"
2381
- }
2382
- ) }),
2383
- /* @__PURE__ */ jsx35(Link2, { href: "https://youtube.com/@playback_global", target: "_blank", children: /* @__PURE__ */ jsx35(
2384
- Image2,
2385
- {
2386
- src: "/assets/youtube.png",
2387
- alt: "PLAYBACK YouTube",
2388
- height: 50,
2389
- width: 50,
2390
- className: "h-9 w-9"
2391
- }
2392
- ) }),
2393
- /* @__PURE__ */ jsx35(Link2, { href: "https://www.tiktok.com/@playback_global", target: "_blank", children: /* @__PURE__ */ jsx35(
2394
- Image2,
2395
- {
2396
- src: "/assets/tiktok.png",
2397
- alt: "PLAYBACK TikTok",
2398
- height: 50,
2399
- width: 50,
2400
- className: "h-9 w-9"
2401
- }
2402
- ) }),
2403
- /* @__PURE__ */ jsx35(
2404
- Link2,
2405
- {
2406
- href: "https://www.linkedin.com/company/playbacksports/",
2407
- target: "_blank",
2408
- children: /* @__PURE__ */ jsx35(
2409
- Image2,
2371
+ var DEFAULT_SOCIALS = [
2372
+ {
2373
+ label: "Instagram",
2374
+ href: "https://www.instagram.com/playback_global",
2375
+ src: "/assets/instagram.png"
2376
+ },
2377
+ {
2378
+ label: "YouTube",
2379
+ href: "https://youtube.com/@playback_global",
2380
+ src: "/assets/youtube.png"
2381
+ },
2382
+ {
2383
+ label: "TikTok",
2384
+ href: "https://www.tiktok.com/@playback_global",
2385
+ src: "/assets/tiktok.png"
2386
+ },
2387
+ {
2388
+ label: "LinkedIn",
2389
+ href: "https://www.linkedin.com/company/playbacksports/",
2390
+ src: "/assets/linkedin.png"
2391
+ }
2392
+ ];
2393
+ function NewsletterForm({
2394
+ onSubmit
2395
+ }) {
2396
+ const [email, setEmail] = React26.useState("");
2397
+ const [state, setState] = React26.useState("idle");
2398
+ const handle = async (e) => {
2399
+ e.preventDefault();
2400
+ const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
2401
+ if (!ok) {
2402
+ setState("error");
2403
+ return;
2404
+ }
2405
+ try {
2406
+ if (onSubmit) await onSubmit(email.trim());
2407
+ setState("sent");
2408
+ setEmail("");
2409
+ } catch {
2410
+ setState("error");
2411
+ }
2412
+ };
2413
+ return /* @__PURE__ */ jsxs19(
2414
+ "form",
2415
+ {
2416
+ onSubmit: handle,
2417
+ noValidate: true,
2418
+ "aria-label": "Subscribe to updates",
2419
+ className: "w-full max-w-md",
2420
+ children: [
2421
+ /* @__PURE__ */ jsx35(
2422
+ "label",
2423
+ {
2424
+ htmlFor: "footer-newsletter",
2425
+ className: "block text-[12px] uppercase tracking-[0.14em] text-[rgba(214,213,201,0.44)] mb-3",
2426
+ children: "Stay in the loop"
2427
+ }
2428
+ ),
2429
+ /* @__PURE__ */ jsxs19("div", { className: "flex flex-col sm:flex-row gap-2", children: [
2430
+ /* @__PURE__ */ jsx35(
2431
+ "input",
2410
2432
  {
2411
- src: "/assets/linkedin.png",
2412
- alt: "PLAYBACK LinkedIn",
2413
- height: 50,
2414
- width: 50,
2415
- className: "h-9 w-9"
2433
+ id: "footer-newsletter",
2434
+ type: "email",
2435
+ inputMode: "email",
2436
+ autoComplete: "email",
2437
+ required: true,
2438
+ value: email,
2439
+ onChange: (e) => {
2440
+ setEmail(e.target.value);
2441
+ if (state !== "idle") setState("idle");
2442
+ },
2443
+ placeholder: "you@club.com",
2444
+ "aria-invalid": state === "error",
2445
+ "aria-describedby": "footer-newsletter-feedback",
2446
+ className: cn(
2447
+ "flex-1 h-11 rounded-full bg-[var(--surface-1,#0f1512)] border px-4 text-[14px] text-[var(--timberwolf)] placeholder:text-[rgba(214,213,201,0.44)]",
2448
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--timberwolf)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--night)]",
2449
+ state === "error" ? "border-[rgba(237,106,106,0.5)]" : "border-[rgba(214,213,201,0.08)]"
2450
+ )
2451
+ }
2452
+ ),
2453
+ /* @__PURE__ */ jsx35(
2454
+ "button",
2455
+ {
2456
+ type: "submit",
2457
+ className: "inline-flex items-center justify-center h-11 px-5 rounded-full bg-[var(--timberwolf)] text-[var(--night)] text-[14px] font-medium hover:bg-[var(--ash-grey)] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--timberwolf)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--night)]",
2458
+ children: "Subscribe"
2416
2459
  }
2417
2460
  )
2418
- }
2419
- )
2420
- ] }),
2421
- /* @__PURE__ */ jsxs19("h2", { className: "text-lg", children: [
2422
- "by",
2423
- " ",
2424
- /* @__PURE__ */ jsx35(Link2, { href: "https://www.braintwopoint0.com", children: /* @__PURE__ */ jsxs19("span", { style: { fontFamily: "AvertaStd-Semibold" }, children: [
2425
- "BRAIN",
2426
- /* @__PURE__ */ jsx35("span", { style: { fontFamily: "AvertaStd-Thin" }, children: "2.0" })
2427
- ] }) })
2428
- ] })
2429
- ] });
2461
+ ] }),
2462
+ /* @__PURE__ */ jsxs19(
2463
+ "p",
2464
+ {
2465
+ id: "footer-newsletter-feedback",
2466
+ className: cn(
2467
+ "mt-2 text-[13px] min-h-[1.25rem]",
2468
+ state === "sent" && "text-[var(--timberwolf)]",
2469
+ state === "error" && "text-[rgb(237,106,106)]",
2470
+ state === "idle" && "text-[rgba(214,213,201,0.44)]"
2471
+ ),
2472
+ role: state === "error" ? "alert" : void 0,
2473
+ children: [
2474
+ state === "sent" && "Thanks \u2014 we\u2019ll be in touch.",
2475
+ state === "error" && "Please enter a valid email address.",
2476
+ state === "idle" && "Monthly notes on product, network, and matches."
2477
+ ]
2478
+ }
2479
+ )
2480
+ ]
2481
+ }
2482
+ );
2483
+ }
2484
+ function FooterLinkItem({ link }) {
2485
+ const classes = "text-[14px] text-[rgba(214,213,201,0.64)] hover:text-[var(--timberwolf)] transition-colors rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--timberwolf)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-0,#0a100d)]";
2486
+ if (link.external) {
2487
+ return /* @__PURE__ */ jsx35(
2488
+ "a",
2489
+ {
2490
+ href: link.href,
2491
+ target: "_blank",
2492
+ rel: "noopener noreferrer",
2493
+ className: classes,
2494
+ children: link.label
2495
+ }
2496
+ );
2497
+ }
2498
+ return /* @__PURE__ */ jsx35(Link2, { href: link.href, className: classes, children: link.label });
2499
+ }
2500
+ function Footer({
2501
+ columns,
2502
+ socials = DEFAULT_SOCIALS,
2503
+ newsletter = false,
2504
+ tagline = "You PLAY. We BACK.",
2505
+ showTagline = true,
2506
+ logoSrc = "/branding/PLAYBACK-Text.png",
2507
+ logoAlt = "PLAYBACK",
2508
+ siteName = "PLAYBACK",
2509
+ creditHref = "https://www.braintwopoint0.com",
2510
+ creditLabel = "Built by BRAIN2.0",
2511
+ newsletterAction,
2512
+ className
2513
+ } = {}) {
2514
+ const hasColumns = columns && columns.length > 0;
2515
+ return /* @__PURE__ */ jsxs19(
2516
+ "footer",
2517
+ {
2518
+ id: "footer",
2519
+ className: cn(
2520
+ "mt-24 border-t border-[rgba(214,213,201,0.08)] bg-[var(--surface-0,#0a100d)]",
2521
+ className
2522
+ ),
2523
+ "aria-labelledby": "footer-heading",
2524
+ children: [
2525
+ /* @__PURE__ */ jsxs19("h2", { id: "footer-heading", className: "sr-only", children: [
2526
+ siteName,
2527
+ " site footer"
2528
+ ] }),
2529
+ /* @__PURE__ */ jsxs19("div", { className: "mx-auto max-w-[1400px] px-6 sm:px-10", children: [
2530
+ /* @__PURE__ */ jsxs19(
2531
+ "div",
2532
+ {
2533
+ className: cn(
2534
+ "grid grid-cols-1 gap-12 py-16",
2535
+ newsletter ? "lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:items-end" : ""
2536
+ ),
2537
+ children: [
2538
+ /* @__PURE__ */ jsxs19("div", { className: "flex flex-col gap-8", children: [
2539
+ /* @__PURE__ */ jsx35(
2540
+ Link2,
2541
+ {
2542
+ href: "/",
2543
+ className: "inline-flex items-center gap-3 rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--timberwolf)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--night)]",
2544
+ "aria-label": `${siteName} home`,
2545
+ children: /* @__PURE__ */ jsx35(
2546
+ Image2,
2547
+ {
2548
+ src: logoSrc,
2549
+ alt: logoAlt,
2550
+ width: 200,
2551
+ height: 40,
2552
+ className: "h-7 w-auto"
2553
+ }
2554
+ )
2555
+ }
2556
+ ),
2557
+ showTagline && tagline ? /* @__PURE__ */ jsx35("p", { className: "font-semibold text-[clamp(28px,3.6vw,44px)] leading-[1.05] tracking-[-0.035em] text-[var(--timberwolf)] max-w-[18ch]", children: tagline }) : null
2558
+ ] }),
2559
+ newsletter ? /* @__PURE__ */ jsx35("div", { className: "lg:justify-self-end w-full lg:max-w-md", children: /* @__PURE__ */ jsx35(NewsletterForm, { onSubmit: newsletterAction }) }) : null
2560
+ ]
2561
+ }
2562
+ ),
2563
+ hasColumns ? /* @__PURE__ */ jsx35("div", { className: "border-t border-[rgba(214,213,201,0.08)] py-14", children: /* @__PURE__ */ jsx35("div", { className: "grid grid-cols-2 gap-x-6 gap-y-10 md:grid-cols-4", children: columns.map((col) => /* @__PURE__ */ jsxs19("nav", { "aria-label": col.title, children: [
2564
+ /* @__PURE__ */ jsx35("p", { className: "text-[12px] uppercase tracking-[0.14em] text-[rgba(214,213,201,0.44)] mb-4", children: col.title }),
2565
+ /* @__PURE__ */ jsx35("ul", { className: "flex flex-col gap-3", children: col.links.map((link) => /* @__PURE__ */ jsx35("li", { children: /* @__PURE__ */ jsx35(FooterLinkItem, { link }) }, `${col.title}-${link.label}`)) })
2566
+ ] }, col.title)) }) }) : null,
2567
+ /* @__PURE__ */ jsxs19("div", { className: "border-t border-[rgba(214,213,201,0.08)] py-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between", children: [
2568
+ /* @__PURE__ */ jsx35("ul", { className: "flex items-center gap-2", children: socials.map(({ label, href, src }) => /* @__PURE__ */ jsx35("li", { children: /* @__PURE__ */ jsx35(
2569
+ "a",
2570
+ {
2571
+ href,
2572
+ target: "_blank",
2573
+ rel: "noopener noreferrer",
2574
+ "aria-label": `${siteName} on ${label}`,
2575
+ className: "inline-flex items-center justify-center h-9 w-9 rounded-full hover:bg-[var(--surface-1,#0f1512)] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--timberwolf)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--night)]",
2576
+ children: /* @__PURE__ */ jsx35(
2577
+ Image2,
2578
+ {
2579
+ src,
2580
+ alt: "",
2581
+ width: 20,
2582
+ height: 20,
2583
+ className: "h-[18px] w-[18px] object-contain opacity-80 hover:opacity-100 transition-opacity"
2584
+ }
2585
+ )
2586
+ }
2587
+ ) }, label)) }),
2588
+ /* @__PURE__ */ jsxs19("p", { className: "text-[13px] text-[rgba(214,213,201,0.44)] order-last sm:order-none", children: [
2589
+ "\xA9 ",
2590
+ (/* @__PURE__ */ new Date()).getFullYear(),
2591
+ " ",
2592
+ siteName,
2593
+ ". All rights reserved."
2594
+ ] }),
2595
+ /* @__PURE__ */ jsx35(
2596
+ "a",
2597
+ {
2598
+ href: creditHref,
2599
+ target: "_blank",
2600
+ rel: "noopener noreferrer",
2601
+ className: "text-[13px] text-[rgba(214,213,201,0.44)] hover:text-[var(--timberwolf)] transition-colors rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--timberwolf)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--night)]",
2602
+ children: creditLabel
2603
+ }
2604
+ )
2605
+ ] })
2606
+ ] })
2607
+ ]
2608
+ }
2609
+ );
2610
+ }
2611
+
2612
+ // src/ui/footer-credits-bar.tsx
2613
+ import Image3 from "next/image";
2614
+ import { Fragment as Fragment5, jsx as jsx36, jsxs as jsxs20 } from "react/jsx-runtime";
2615
+ var DEFAULT_SOCIALS2 = [
2616
+ {
2617
+ label: "Instagram",
2618
+ href: "https://www.instagram.com/playback_global",
2619
+ src: "/assets/instagram.png"
2620
+ },
2621
+ {
2622
+ label: "YouTube",
2623
+ href: "https://youtube.com/@playback_global",
2624
+ src: "/assets/youtube.png"
2625
+ },
2626
+ {
2627
+ label: "TikTok",
2628
+ href: "https://www.tiktok.com/@playback_global",
2629
+ src: "/assets/tiktok.png"
2630
+ },
2631
+ {
2632
+ label: "LinkedIn",
2633
+ href: "https://www.linkedin.com/company/playbacksports/",
2634
+ src: "/assets/linkedin.png"
2635
+ }
2636
+ ];
2637
+ function FooterCreditsBar({
2638
+ socials = DEFAULT_SOCIALS2,
2639
+ companyName = "PLAYBACK",
2640
+ creditHref = "https://www.braintwopoint0.com",
2641
+ showCredit = true,
2642
+ copyrightYear,
2643
+ className
2644
+ } = {}) {
2645
+ const year = copyrightYear ?? (/* @__PURE__ */ new Date()).getFullYear();
2646
+ return /* @__PURE__ */ jsxs20(
2647
+ "div",
2648
+ {
2649
+ className: cn(
2650
+ "border-t py-6 grid grid-cols-1 gap-4 sm:grid-cols-2 sm:items-center",
2651
+ "border-[rgba(214,213,201,0.08)]",
2652
+ className
2653
+ ),
2654
+ children: [
2655
+ /* @__PURE__ */ jsxs20("div", { className: "flex items-center gap-4 sm:justify-self-start", children: [
2656
+ /* @__PURE__ */ jsx36("ul", { className: "flex items-center gap-2", children: socials.map(({ label, href, src }) => /* @__PURE__ */ jsx36("li", { children: /* @__PURE__ */ jsx36(
2657
+ "a",
2658
+ {
2659
+ href,
2660
+ target: "_blank",
2661
+ rel: "noopener noreferrer",
2662
+ "aria-label": `${companyName} on ${label}`,
2663
+ className: cn(
2664
+ "inline-flex items-center justify-center h-10 w-10 rounded-full",
2665
+ "hover:bg-[var(--surface-1,#0f1512)] transition-colors",
2666
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--timberwolf)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--night)]"
2667
+ ),
2668
+ children: /* @__PURE__ */ jsx36(
2669
+ Image3,
2670
+ {
2671
+ src,
2672
+ alt: "",
2673
+ width: 24,
2674
+ height: 24,
2675
+ className: "h-[22px] w-[22px] object-contain opacity-80 hover:opacity-100 transition-opacity"
2676
+ }
2677
+ )
2678
+ }
2679
+ ) }, label)) }),
2680
+ showCredit ? /* @__PURE__ */ jsxs20(Fragment5, { children: [
2681
+ /* @__PURE__ */ jsx36(
2682
+ "span",
2683
+ {
2684
+ "aria-hidden": true,
2685
+ className: "h-5 w-px bg-[rgba(214,213,201,0.16)] flex-shrink-0"
2686
+ }
2687
+ ),
2688
+ /* @__PURE__ */ jsx36(
2689
+ "a",
2690
+ {
2691
+ href: creditHref,
2692
+ target: "_blank",
2693
+ rel: "noopener noreferrer",
2694
+ "aria-label": "Built by BRAIN2.0",
2695
+ className: cn(
2696
+ "text-[13px] text-[rgba(214,213,201,0.44)] hover:text-[var(--timberwolf)] transition-colors rounded-sm",
2697
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--timberwolf)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--night)]",
2698
+ "whitespace-nowrap"
2699
+ ),
2700
+ children: /* @__PURE__ */ jsxs20("span", { "aria-hidden": true, children: [
2701
+ "Built by",
2702
+ " ",
2703
+ /* @__PURE__ */ jsxs20("span", { style: { fontFamily: "AvertaStd-Semibold" }, children: [
2704
+ "BRAIN",
2705
+ /* @__PURE__ */ jsx36("span", { style: { fontFamily: "AvertaStd-Thin" }, children: "2.0" })
2706
+ ] })
2707
+ ] })
2708
+ }
2709
+ )
2710
+ ] }) : null
2711
+ ] }),
2712
+ /* @__PURE__ */ jsxs20("p", { className: "text-[13px] text-[rgba(214,213,201,0.44)] text-center sm:text-right sm:justify-self-end", children: [
2713
+ "\xA9 ",
2714
+ year,
2715
+ " ",
2716
+ companyName,
2717
+ ". All rights reserved."
2718
+ ] })
2719
+ ]
2720
+ }
2721
+ );
2430
2722
  }
2431
2723
  export {
2432
2724
  AnimatedTooltip,
@@ -2470,6 +2762,7 @@ export {
2470
2762
  FadeIn,
2471
2763
  FlipWords,
2472
2764
  Footer,
2765
+ FooterCreditsBar,
2473
2766
  HeroHighlight,
2474
2767
  Highlight,
2475
2768
  HoverCard,