@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.
- package/dist/email/index.d.ts +44 -0
- package/dist/email/index.js +46 -0
- package/dist/email/index.js.map +1 -0
- package/dist/ui/index.d.ts +74 -2
- package/dist/ui/index.js +350 -57
- package/dist/ui/index.js.map +1 -1
- package/package.json +14 -3
|
@@ -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":[]}
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
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
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
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
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
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,
|