@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.
- 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 +76 -2
- package/dist/ui/index.js +402 -55
- 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
|
@@ -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
|
|
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;
|
|
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/
|
|
2314
|
-
import
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
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
|
-
|
|
2341
|
-
|
|
2335
|
+
}
|
|
2336
|
+
),
|
|
2337
|
+
/* @__PURE__ */ jsxs18("div", { style: { position: "relative", width: 65, aspectRatio: "1" }, children: [
|
|
2338
|
+
/* @__PURE__ */ jsx34(
|
|
2339
|
+
"span",
|
|
2342
2340
|
{
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
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
|
-
|
|
2351
|
+
"span",
|
|
2352
2352
|
{
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
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,
|