@braintwopoint0/playback-commons 0.1.20 → 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":[]}
@@ -402,6 +402,80 @@ interface FadeInProps extends React$1.HTMLAttributes<HTMLDivElement> {
402
402
  }
403
403
  declare function FadeIn({ children, className, delay, direction, ...props }: FadeInProps): react_jsx_runtime.JSX.Element;
404
404
 
405
- declare function Footer(): react_jsx_runtime.JSX.Element;
405
+ declare function LumaSpin(): react_jsx_runtime.JSX.Element;
406
406
 
407
- 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, 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 };
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;
436
+
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
@@ -2310,71 +2310,416 @@ function FadeIn({ children, className, delay = 0, direction = "up", ...props })
2310
2310
  );
2311
2311
  }
2312
2312
 
2313
- // src/ui/footer.tsx
2314
- import Image2 from "next/image";
2315
- import Link2 from "next/link";
2316
- import { jsx as jsx34, jsxs as jsxs18 } from "react/jsx-runtime";
2317
- function Footer() {
2318
- return /* @__PURE__ */ jsxs18("footer", { className: "container mx-auto flex p-5 items-center justify-between border-t border-[var(--timberwolf)]", children: [
2319
- /* @__PURE__ */ jsxs18("div", { className: "flex gap-4", children: [
2320
- /* @__PURE__ */ jsx34(Link2, { href: "https://www.instagram.com/playback_global", target: "_blank", children: /* @__PURE__ */ jsx34(
2321
- Image2,
2322
- {
2323
- src: "/assets/instagram.png",
2324
- alt: "PLAYBACK Instagram",
2325
- height: 50,
2326
- width: 50,
2327
- className: "h-9 w-9"
2328
- }
2329
- ) }),
2330
- /* @__PURE__ */ jsx34(Link2, { href: "https://youtube.com/@playback_global", target: "_blank", children: /* @__PURE__ */ jsx34(
2331
- Image2,
2332
- {
2333
- src: "/assets/youtube.png",
2334
- alt: "PLAYBACK YouTube",
2335
- height: 50,
2336
- width: 50,
2337
- className: "h-9 w-9"
2313
+ // src/ui/luma-spin.tsx
2314
+ import { Fragment as Fragment4, jsx as jsx34, jsxs as jsxs18 } from "react/jsx-runtime";
2315
+ function LumaSpin() {
2316
+ return /* @__PURE__ */ jsxs18(Fragment4, { children: [
2317
+ /* @__PURE__ */ jsx34(
2318
+ "style",
2319
+ {
2320
+ dangerouslySetInnerHTML: {
2321
+ __html: `
2322
+ @keyframes pb-loader {
2323
+ 0% { inset: 0 35px 35px 0; }
2324
+ 12.5% { inset: 0 35px 0 0; }
2325
+ 25% { inset: 35px 35px 0 0; }
2326
+ 37.5% { inset: 35px 0 0 0; }
2327
+ 50% { inset: 35px 0 0 35px; }
2328
+ 62.5% { inset: 0 0 0 35px; }
2329
+ 75% { inset: 0 0 35px 35px; }
2330
+ 87.5% { inset: 0 0 35px 0; }
2331
+ 100% { inset: 0 35px 35px 0; }
2332
+ }
2333
+ `
2338
2334
  }
2339
- ) }),
2340
- /* @__PURE__ */ jsx34(Link2, { href: "https://www.tiktok.com/@playback_global", target: "_blank", children: /* @__PURE__ */ jsx34(
2341
- Image2,
2335
+ }
2336
+ ),
2337
+ /* @__PURE__ */ jsxs18("div", { style: { position: "relative", width: 65, aspectRatio: "1" }, children: [
2338
+ /* @__PURE__ */ jsx34(
2339
+ "span",
2342
2340
  {
2343
- src: "/assets/tiktok.png",
2344
- alt: "PLAYBACK TikTok",
2345
- height: 50,
2346
- width: 50,
2347
- className: "h-9 w-9"
2341
+ style: {
2342
+ position: "absolute",
2343
+ inset: "0 35px 35px 0",
2344
+ borderRadius: 50,
2345
+ border: "3px solid #B8B9A2",
2346
+ animation: "pb-loader 2.5s infinite"
2347
+ }
2348
2348
  }
2349
- ) }),
2349
+ ),
2350
2350
  /* @__PURE__ */ jsx34(
2351
- Link2,
2351
+ "span",
2352
2352
  {
2353
- href: "https://www.linkedin.com/company/playbacksports/",
2354
- target: "_blank",
2355
- children: /* @__PURE__ */ jsx34(
2356
- Image2,
2357
- {
2358
- src: "/assets/linkedin.png",
2359
- alt: "PLAYBACK LinkedIn",
2360
- height: 50,
2361
- width: 50,
2362
- className: "h-9 w-9"
2363
- }
2364
- )
2353
+ style: {
2354
+ position: "absolute",
2355
+ inset: "0 35px 35px 0",
2356
+ borderRadius: 50,
2357
+ border: "3px solid #B8B9A2",
2358
+ animation: "pb-loader 2.5s -1.25s infinite"
2359
+ }
2365
2360
  }
2366
2361
  )
2367
- ] }),
2368
- /* @__PURE__ */ jsxs18("h2", { className: "text-lg", children: [
2369
- "by",
2370
- " ",
2371
- /* @__PURE__ */ jsx34(Link2, { href: "https://www.braintwopoint0.com", children: /* @__PURE__ */ jsxs18("span", { style: { fontFamily: "AvertaStd-Semibold" }, children: [
2372
- "BRAIN",
2373
- /* @__PURE__ */ jsx34("span", { style: { fontFamily: "AvertaStd-Thin" }, children: "2.0" })
2374
- ] }) })
2375
2362
  ] })
2376
2363
  ] });
2377
2364
  }
2365
+
2366
+ // src/ui/footer.tsx
2367
+ import * as React26 from "react";
2368
+ import Image2 from "next/image";
2369
+ import Link2 from "next/link";
2370
+ import { jsx as jsx35, jsxs as jsxs19 } from "react/jsx-runtime";
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",
2432
+ {
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"
2459
+ }
2460
+ )
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
+ );
2722
+ }
2378
2723
  export {
2379
2724
  AnimatedTooltip,
2380
2725
  Avatar,
@@ -2417,6 +2762,7 @@ export {
2417
2762
  FadeIn,
2418
2763
  FlipWords,
2419
2764
  Footer,
2765
+ FooterCreditsBar,
2420
2766
  HeroHighlight,
2421
2767
  Highlight,
2422
2768
  HoverCard,
@@ -2425,6 +2771,7 @@ export {
2425
2771
  HoverEffect,
2426
2772
  Input,
2427
2773
  Label,
2774
+ LumaSpin,
2428
2775
  PageShell,
2429
2776
  Popover,
2430
2777
  PopoverAnchor,