@thewhatmatters/wmds 0.2.0 → 0.3.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/CHANGELOG.md +10 -0
- package/CONSUMING.md +4 -4
- package/README.md +3 -3
- package/dist/styles.css +1 -1
- package/docs/component-contracts.md +1 -1
- package/docs/components.md +6 -6
- package/docs/exports.json +1 -13
- package/docs/patterns/components-avatar--dropdown-row.tsx +1 -1
- package/docs/patterns/components-badge--with-avatar.tsx +1 -1
- package/docs/patterns/components-button-button--destructive-action.tsx +1 -1
- package/docs/patterns/components-button-button--ghost-action.tsx +1 -1
- package/docs/patterns/components-button-button--inverse-action.tsx +1 -1
- package/docs/patterns/components-button-button--link-render.tsx +1 -1
- package/docs/patterns/components-button-button--multi-state-badge.tsx +1 -1
- package/docs/patterns/components-button-button--outline-mono.tsx +1 -1
- package/docs/patterns/components-button-button--primary-action.tsx +1 -1
- package/docs/patterns/components-button-button--row-hug.tsx +1 -1
- package/docs/patterns/components-button-button--row-layout.tsx +1 -1
- package/docs/patterns/components-button-button--secondary-action.tsx +1 -1
- package/docs/patterns/components-button-button--suggestion-pills.tsx +1 -1
- package/docs/patterns/components-button-button--with-count.tsx +1 -1
- package/docs/patterns/components-button-button--with-icon.tsx +1 -1
- package/docs/patterns/components-button-floatingactionbutton--vertical-actions.tsx +1 -1
- package/docs/patterns/components-calembed--pattern-skip-in-footer.tsx +1 -1
- package/docs/patterns/components-card-card--body-slot-occupancy-history.tsx +1 -1
- package/docs/patterns/components-card-card--headerless-media.tsx +1 -1
- package/docs/patterns/components-card-card--outlined-layout.tsx +1 -1
- package/docs/patterns/components-card-card--pinned-height.tsx +1 -1
- package/docs/patterns/components-card-selectablecard--selectable-cards-pattern.tsx +1 -1
- package/docs/patterns/components-chart--audience-activity-heatmap.tsx +1 -1
- package/docs/patterns/components-chart--cartesian-no-data-gaps.tsx +1 -1
- package/docs/patterns/components-chart--occupancy-history-in-card.tsx +1 -1
- package/docs/patterns/components-chart--occupancy-in-card.tsx +1 -1
- package/docs/patterns/components-chart--post-reach-distribution.tsx +1 -1
- package/docs/patterns/components-chart--ranked-audience-breakdown.tsx +1 -1
- package/docs/patterns/components-chart--unit-grid-composition.tsx +1 -1
- package/docs/patterns/components-chatqa--pattern-qa-with-reply.tsx +1 -1
- package/docs/patterns/components-checkbox-checkbox--default.tsx +1 -1
- package/docs/patterns/components-checkbox-checkbox--with-description.tsx +1 -1
- package/docs/patterns/components-checkbox-checkboxgroup--default.tsx +1 -1
- package/docs/patterns/components-dialog--alert-dialog-destructive.tsx +1 -1
- package/docs/patterns/components-dialog--default-dialog.tsx +1 -1
- package/docs/patterns/components-dialog--form-dialog.tsx +1 -1
- package/docs/patterns/components-displaycontrols--page-utilities.tsx +1 -1
- package/docs/patterns/components-field--horizontal-input.tsx +1 -1
- package/docs/patterns/components-field--horizontal-text-area.tsx +1 -1
- package/docs/patterns/components-field--multi-control.tsx +1 -1
- package/docs/patterns/components-footerreveal--marketing-hero-pattern.tsx +1 -1
- package/docs/patterns/components-footerreveal--marketing-hero-ruled-pattern.tsx +1 -1
- package/docs/patterns/components-footerreveal--marketing-hero-with-gallery-intro.tsx +1 -1
- package/docs/patterns/components-footerreveal--marketing-page-pattern.tsx +1 -1
- package/docs/patterns/components-footerreveal--ruled-grid-footer-pattern.tsx +1 -1
- package/docs/patterns/components-footerreveal--sequenced-marketing-hero.tsx +1 -1
- package/docs/patterns/components-herointro--two-line-intro.tsx +1 -1
- package/docs/patterns/components-herotilestack--marketing-hero-pattern.tsx +1 -1
- package/docs/patterns/components-herotilestack--marketing-hero-text-sequence.tsx +1 -1
- package/docs/patterns/components-herotilestack--marketing-hero-with-gallery-intro.tsx +1 -1
- package/docs/patterns/components-intakeconfirmation--booked-pattern.tsx +1 -1
- package/docs/patterns/components-intakeconfirmation--emailed-pattern.tsx +1 -1
- package/docs/patterns/components-intakeform--about-you-pattern.tsx +1 -1
- package/docs/patterns/components-intakemodal--intake-modal-pattern.tsx +1 -1
- package/docs/patterns/components-kbd--command-row.tsx +1 -1
- package/docs/patterns/components-kbd--numbered-choices.tsx +1 -1
- package/docs/patterns/components-kbd--shortcut.tsx +1 -1
- package/docs/patterns/components-moremenu--action-menu.tsx +1 -1
- package/docs/patterns/components-moremenu--in-card-header.tsx +1 -1
- package/docs/patterns/components-navlist--side-nav-settings.tsx +1 -1
- package/docs/patterns/components-pageheader--app-header-pattern.tsx +1 -1
- package/docs/patterns/components-pageheader--page-section-header.tsx +1 -1
- package/docs/patterns/components-pageheader--toolbar-header.tsx +1 -1
- package/docs/patterns/components-panel--end-detail-rail.tsx +1 -1
- package/docs/patterns/components-pillgroup--pill-group-pattern.tsx +1 -1
- package/docs/patterns/components-promptbar--pattern-marketing-composer.tsx +3 -3
- package/docs/patterns/components-promptbar--pattern-prompt-bar.tsx +1 -1
- package/docs/patterns/components-radio-radiogroup--default.tsx +1 -1
- package/docs/patterns/components-scrollhorizontal--project-gallery-pattern.tsx +1 -1
- package/docs/patterns/components-scrollhorizontal--with-intro.tsx +1 -1
- package/docs/patterns/components-sheet--bottom-sheet.tsx +1 -1
- package/docs/patterns/components-sheet--end-sheet.tsx +1 -1
- package/docs/patterns/components-sitenav--brand-and-cta.tsx +1 -1
- package/docs/patterns/components-sitenav--centered-links.tsx +1 -1
- package/docs/patterns/components-sitenav--compact.tsx +1 -1
- package/docs/patterns/components-sitenav--marketing-header.tsx +1 -1
- package/docs/patterns/components-sitenav--mega-menu.tsx +1 -1
- package/docs/patterns/components-stat--insights-row.tsx +1 -1
- package/docs/patterns/components-stepprogress--step-progress-pattern.tsx +1 -1
- package/docs/patterns/components-switch--default.tsx +1 -1
- package/docs/patterns/components-switch--settings-row.tsx +1 -1
- package/docs/patterns/components-tab--settings-pages.tsx +1 -1
- package/docs/patterns/components-taskrows--action-details.tsx +1 -1
- package/docs/patterns/components-taskrows--capsules.tsx +1 -1
- package/docs/patterns/components-taskrows--card-with-status-rows.tsx +1 -1
- package/docs/patterns/components-taskrows--controlled-expand.tsx +1 -1
- package/docs/patterns/components-taskrows--custom-slots.tsx +1 -1
- package/docs/patterns/components-taskrows--status-list.tsx +1 -1
- package/docs/patterns/components-taskrows--tag-chips.tsx +1 -1
- package/docs/patterns/components-textlink--external-destination.tsx +1 -1
- package/docs/patterns/components-textlink--inline-prose.tsx +1 -1
- package/docs/patterns/components-textlink--supporting-copy.tsx +1 -1
- package/docs/patterns/components-textsequence--default.tsx +1 -1
- package/docs/patterns/components-textsequence--reduced-motion.tsx +1 -1
- package/docs/patterns/components-textsequence--shapes.tsx +1 -1
- package/docs/patterns/components-toast--stacked-notifications.tsx +1 -1
- package/docs/patterns/components-tooltip--controlled.tsx +1 -1
- package/docs/patterns/components-tooltip--disabled-control.tsx +1 -1
- package/docs/patterns/components-tooltip--icon-action.tsx +1 -1
- package/docs/patterns/components-tooltip--keyboard-shortcut.tsx +1 -1
- package/docs/patterns/components-tooltip--placement.tsx +1 -1
- package/docs/patterns/foundations-scroll-fade--horizontal-rail.tsx +1 -1
- package/docs/patterns/foundations-scroll-fade--overflowing-list.tsx +1 -1
- package/docs/patterns/guides-chart-explorations--comparison-gallery.tsx +1 -1
- package/docs/patterns/guides-overlay-flows--notification-preferences.tsx +1 -1
- package/docs/patterns/guides-profile-typography--editorial-profile.tsx +1 -1
- package/docs/patterns/index.json +1 -153
- package/package.json +1 -1
- package/docs/patterns/sites-pitchkit-account-settings--account-settings-owner.tsx +0 -265
- package/docs/patterns/sites-pitchkit-creator-identity--creator-identity-owner-settings.tsx +0 -154
- package/docs/patterns/sites-pitchkit-creator-identity--creator-identity-public.tsx +0 -96
- package/docs/patterns/sites-pitchkit-insights-and-kit--creator-insights-loading.tsx +0 -1006
- package/docs/patterns/sites-pitchkit-insights-and-kit--creator-insights.tsx +0 -1031
- package/docs/patterns/sites-pitchkit-insights-and-kit--insufficient-audience-data.tsx +0 -1015
- package/docs/patterns/sites-pitchkit-insights-and-kit--insufficient-reach-and-audience-data.tsx +0 -1017
- package/docs/patterns/sites-pitchkit-insights-and-kit--insufficient-reach-data.tsx +0 -1033
- package/docs/patterns/sites-pitchkit-insights-and-kit--owner-pitch-kit.tsx +0 -838
- package/docs/patterns/sites-pitchkit-insights-and-kit--shareable-pitch-kit.tsx +0 -608
- package/docs/patterns/sites-pitchkit-intro--intro-owner.tsx +0 -161
- package/docs/patterns/sites-pitchkit-intro--intro-public.tsx +0 -111
- package/docs/patterns/sites-pitchkit-past-brands--past-brands-owner.tsx +0 -611
- package/docs/patterns/sites-pitchkit-past-brands--past-brands-public.tsx +0 -299
- package/docs/patterns/sites-pitchkit-theme-picker--theme-picker-owner.tsx +0 -832
- package/docs/patterns/sites-whatmatters-intake--start-a-project-pattern.tsx +0 -234
- package/docs/patterns/sites-whatmatters-marketing-landing--marketing-landing.tsx +0 -87
- package/docs/patterns/sites-whatmatters-prompt-chat--landing-to-chat.tsx +0 -899
- package/docs/patterns/sites-whatmatters-rfp-submitted--rfp-submitted.tsx +0 -302
|
@@ -1,611 +0,0 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.2.0 · Pattern — past brands (owner)
|
|
2
|
-
// Storybook: Sites/PitchKit/Past brands → Pattern — past brands (owner) (?path=/story/sites-pitchkit-past-brands--past-brands-owner)
|
|
3
|
-
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
|
-
|
|
5
|
-
import { useLayoutEffect, useRef, useState, type CSSProperties } from "react";
|
|
6
|
-
import { ChevronDown, ChevronUp, GripVertical, Pencil, Plus, Trash2 } from "lucide-react";
|
|
7
|
-
import {
|
|
8
|
-
Avatar,
|
|
9
|
-
Button,
|
|
10
|
-
ButtonIcon,
|
|
11
|
-
Card,
|
|
12
|
-
Chip,
|
|
13
|
-
Dialog,
|
|
14
|
-
IconButton,
|
|
15
|
-
Input,
|
|
16
|
-
MoreMenu,
|
|
17
|
-
Select,
|
|
18
|
-
cardTitleClasses,
|
|
19
|
-
dialogFooterActionsClasses,
|
|
20
|
-
} from "@thewhatmatters/wmds";
|
|
21
|
-
|
|
22
|
-
const compactNumber = new Intl.NumberFormat("en", {
|
|
23
|
-
notation: "compact",
|
|
24
|
-
maximumFractionDigits: 1,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export interface PitchKitCreatorIdentity {
|
|
29
|
-
displayName?: string;
|
|
30
|
-
/** Without the @ — shown as @handle, shared as /k/[handle]. */
|
|
31
|
-
handle: string;
|
|
32
|
-
profilePictureUrl?: string;
|
|
33
|
-
followersCount?: number;
|
|
34
|
-
professionalAccount?: "Business" | "Creator";
|
|
35
|
-
connected?: boolean;
|
|
36
|
-
lastSyncedLabel?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function CreatorIdentityStrip({
|
|
40
|
-
identity,
|
|
41
|
-
nameAs = "h1",
|
|
42
|
-
showProfessionalChip = false,
|
|
43
|
-
}: {
|
|
44
|
-
identity: PitchKitCreatorIdentity;
|
|
45
|
-
nameAs?: "h1" | "p";
|
|
46
|
-
showProfessionalChip?: boolean;
|
|
47
|
-
}) {
|
|
48
|
-
const NameTag = nameAs;
|
|
49
|
-
const avatarName = identity.displayName ?? identity.handle;
|
|
50
|
-
const handleLabel = `@${identity.handle}`;
|
|
51
|
-
const followerLabel =
|
|
52
|
-
identity.followersCount == null
|
|
53
|
-
? null
|
|
54
|
-
: `${compactNumber.format(identity.followersCount)} followers`;
|
|
55
|
-
const meta = [handleLabel, followerLabel].filter(Boolean).join(" · ");
|
|
56
|
-
const showTitleRow =
|
|
57
|
-
identity.displayName != null ||
|
|
58
|
-
(showProfessionalChip && identity.professionalAccount != null);
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<div className="flex flex-wrap items-center gap-4">
|
|
62
|
-
<Avatar
|
|
63
|
-
name={avatarName}
|
|
64
|
-
src={identity.profilePictureUrl}
|
|
65
|
-
size="lg"
|
|
66
|
-
/>
|
|
67
|
-
<div className="flex min-w-0 flex-col gap-1">
|
|
68
|
-
{showTitleRow ? (
|
|
69
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
70
|
-
{identity.displayName != null ? (
|
|
71
|
-
<NameTag className="type-heading-2 text-fg">
|
|
72
|
-
{identity.displayName}
|
|
73
|
-
</NameTag>
|
|
74
|
-
) : null}
|
|
75
|
-
{showProfessionalChip && identity.professionalAccount != null ? (
|
|
76
|
-
<Chip readOnly size="sm">
|
|
77
|
-
{identity.professionalAccount}
|
|
78
|
-
</Chip>
|
|
79
|
-
) : null}
|
|
80
|
-
</div>
|
|
81
|
-
) : null}
|
|
82
|
-
<p className="type-body text-fg text-muted">{meta}</p>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
export interface PitchKitPastBrand {
|
|
90
|
-
id: string;
|
|
91
|
-
name: string;
|
|
92
|
-
/** Curated mark key — missing or unknown keys use a letter Avatar. */
|
|
93
|
-
logo_key?: string;
|
|
94
|
-
result_label?: string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const PITCHKIT_BRAND_RESULT_MAX = 24;
|
|
98
|
-
const PITCHKIT_BRAND_LOGO_LETTER = "letter";
|
|
99
|
-
|
|
100
|
-
const pitchKitBrandLogoKeys = [
|
|
101
|
-
"nike", "adidas", "apple", "google", "meta", "amazon", "spotify", "netflix",
|
|
102
|
-
"sephora", "glossier", "nordstrom", "target", "walmart", "starbucks",
|
|
103
|
-
"coca-cola", "pepsi", "samsung", "microsoft", "adobe", "shopify",
|
|
104
|
-
"uber", "airbnb", "disney", "lululemon", "reebok", "puma",
|
|
105
|
-
"dior", "chanel", "bmw", "ford", "chase", "visa",
|
|
106
|
-
];
|
|
107
|
-
|
|
108
|
-
const pastBrandLogoKeySet = new Set(pitchKitBrandLogoKeys);
|
|
109
|
-
|
|
110
|
-
function resolvePastBrandLogoKey(logoKey: string | null | undefined) {
|
|
111
|
-
if (logoKey == null || logoKey === "" || logoKey === PITCHKIT_BRAND_LOGO_LETTER) {
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
114
|
-
return pastBrandLogoKeySet.has(logoKey) ? logoKey : undefined;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function pastBrandLogoMonogram(logoKey: string) {
|
|
118
|
-
const parts = logoKey.split("-");
|
|
119
|
-
if (parts.length > 1) {
|
|
120
|
-
return parts.map((part) => part.charAt(0).toUpperCase()).join("").slice(0, 2);
|
|
121
|
-
}
|
|
122
|
-
return logoKey.slice(0, 1).toUpperCase();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function normalizePastBrandResult(value: string | null | undefined) {
|
|
126
|
-
const trimmed = value?.trim() ?? "";
|
|
127
|
-
if (trimmed.length === 0) return undefined;
|
|
128
|
-
return trimmed.slice(0, PITCHKIT_BRAND_RESULT_MAX);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function BrandMark({ name, logoKey }: { name: string; logoKey?: string }) {
|
|
132
|
-
const resolved = resolvePastBrandLogoKey(logoKey);
|
|
133
|
-
if (resolved == null) {
|
|
134
|
-
return <Avatar name={name} size="sm" />;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-fg type-supporting text-muted font-medium uppercase leading-none text-bg" role="img" aria-label={name}>
|
|
139
|
-
<span aria-hidden>{pastBrandLogoMonogram(resolved)}</span>
|
|
140
|
-
</span>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function BrandResultChip({ label }: { label?: string }) {
|
|
145
|
-
const result = normalizePastBrandResult(label);
|
|
146
|
-
if (result == null) return null;
|
|
147
|
-
return <Chip readOnly size="sm">{result}</Chip>;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function BrandLockup({ brand }: { brand: PitchKitPastBrand }) {
|
|
151
|
-
return (
|
|
152
|
-
<div className="flex min-w-0 items-center gap-3">
|
|
153
|
-
<BrandMark name={brand.name} logoKey={brand.logo_key} />
|
|
154
|
-
<h3 className="type-label text-fg">{brand.name}</h3>
|
|
155
|
-
<BrandResultChip label={brand.result_label} />
|
|
156
|
-
</div>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function PastBrandCard({ brand, className }: { brand: PitchKitPastBrand; className: string }) {
|
|
161
|
-
return (
|
|
162
|
-
<Card variant="outlined" shape="rounded" className={className}>
|
|
163
|
-
<Card.Header start={<BrandLockup brand={brand} />} />
|
|
164
|
-
</Card>
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function PastBrandsRail({ brands }: { brands: readonly PitchKitPastBrand[] }) {
|
|
169
|
-
const hostRef = useRef<HTMLDivElement>(null);
|
|
170
|
-
const measureRef = useRef<HTMLUListElement>(null);
|
|
171
|
-
const [overflows, setOverflows] = useState(false);
|
|
172
|
-
const [reduceMotion, setReduceMotion] = useState(false);
|
|
173
|
-
|
|
174
|
-
useLayoutEffect(() => {
|
|
175
|
-
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
176
|
-
const updateMotion = () => setReduceMotion(media.matches);
|
|
177
|
-
updateMotion();
|
|
178
|
-
media.addEventListener("change", updateMotion);
|
|
179
|
-
return () => media.removeEventListener("change", updateMotion);
|
|
180
|
-
}, []);
|
|
181
|
-
|
|
182
|
-
useLayoutEffect(() => {
|
|
183
|
-
const host = hostRef.current;
|
|
184
|
-
const measure = measureRef.current;
|
|
185
|
-
if (host == null || measure == null) return;
|
|
186
|
-
|
|
187
|
-
const update = () => {
|
|
188
|
-
setOverflows(measure.scrollWidth > host.clientWidth + 1);
|
|
189
|
-
};
|
|
190
|
-
update();
|
|
191
|
-
const observer = new ResizeObserver(update);
|
|
192
|
-
observer.observe(host);
|
|
193
|
-
observer.observe(measure);
|
|
194
|
-
return () => observer.disconnect();
|
|
195
|
-
}, [brands]);
|
|
196
|
-
|
|
197
|
-
const duration = Math.max(16, brands.length * 4);
|
|
198
|
-
const showMarquee = overflows && !reduceMotion;
|
|
199
|
-
|
|
200
|
-
return (
|
|
201
|
-
<div
|
|
202
|
-
ref={hostRef}
|
|
203
|
-
className="relative col-span-full min-w-0"
|
|
204
|
-
data-overflow={showMarquee ? "true" : "false"}
|
|
205
|
-
>
|
|
206
|
-
<ul ref={measureRef} className="pointer-events-none invisible absolute flex w-max items-center gap-3" aria-hidden>
|
|
207
|
-
{brands.map((brand) => (
|
|
208
|
-
<li key={`${brand.id}-measure`}>
|
|
209
|
-
<PastBrandCard brand={brand} className="w-max max-w-full shrink-0" />
|
|
210
|
-
</li>
|
|
211
|
-
))}
|
|
212
|
-
</ul>
|
|
213
|
-
{reduceMotion ? (
|
|
214
|
-
<ul className="flex min-w-0 flex-wrap items-center gap-3" aria-label="Past brands">
|
|
215
|
-
{brands.map((brand) => (
|
|
216
|
-
<li key={brand.id}>
|
|
217
|
-
<PastBrandCard brand={brand} className="w-max max-w-full shrink-0" />
|
|
218
|
-
</li>
|
|
219
|
-
))}
|
|
220
|
-
</ul>
|
|
221
|
-
) : showMarquee ? (
|
|
222
|
-
<>
|
|
223
|
-
<ul className="sr-only" aria-label="Past brands">
|
|
224
|
-
{brands.map((brand) => {
|
|
225
|
-
const result = normalizePastBrandResult(brand.result_label);
|
|
226
|
-
return (
|
|
227
|
-
<li key={brand.id}>
|
|
228
|
-
{result == null ? brand.name : `${brand.name}, ${result}`}
|
|
229
|
-
</li>
|
|
230
|
-
);
|
|
231
|
-
})}
|
|
232
|
-
</ul>
|
|
233
|
-
<div className="group overflow-hidden" aria-hidden>
|
|
234
|
-
<div
|
|
235
|
-
className="marquee-track flex w-max items-center"
|
|
236
|
-
style={{ "--marquee-duration": `${duration}s` } as CSSProperties}
|
|
237
|
-
>
|
|
238
|
-
<ul className="flex min-w-0 items-center gap-3">
|
|
239
|
-
{brands.map((brand) => (
|
|
240
|
-
<li key={`${brand.id}-loop-a`}>
|
|
241
|
-
<PastBrandCard brand={brand} className="w-max max-w-full shrink-0" />
|
|
242
|
-
</li>
|
|
243
|
-
))}
|
|
244
|
-
</ul>
|
|
245
|
-
<ul className="flex min-w-0 items-center gap-3">
|
|
246
|
-
{brands.map((brand) => (
|
|
247
|
-
<li key={`${brand.id}-loop-b`}>
|
|
248
|
-
<PastBrandCard brand={brand} className="w-max max-w-full shrink-0" />
|
|
249
|
-
</li>
|
|
250
|
-
))}
|
|
251
|
-
</ul>
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
</>
|
|
255
|
-
) : (
|
|
256
|
-
<ul className="flex min-w-0 items-center gap-3" aria-label="Past brands">
|
|
257
|
-
{brands.map((brand) => (
|
|
258
|
-
<li key={brand.id}>
|
|
259
|
-
<PastBrandCard brand={brand} className="w-max max-w-full shrink-0" />
|
|
260
|
-
</li>
|
|
261
|
-
))}
|
|
262
|
-
</ul>
|
|
263
|
-
)}
|
|
264
|
-
</div>
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function PublicPastBrands({ brands }: { brands: readonly PitchKitPastBrand[] }) {
|
|
269
|
-
if (brands.length === 0) return null;
|
|
270
|
-
|
|
271
|
-
return (
|
|
272
|
-
<section className="band min-w-0 gap-y-4">
|
|
273
|
-
<div className="col-span-full flex flex-wrap items-end justify-between gap-3">
|
|
274
|
-
<h2 className={cardTitleClasses}>Past brands</h2>
|
|
275
|
-
</div>
|
|
276
|
-
<PastBrandsRail brands={brands} />
|
|
277
|
-
</section>
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const PITCHKIT_BRANDS_MAX = 8;
|
|
283
|
-
const PITCHKIT_BRAND_NAME_MAX = 40;
|
|
284
|
-
const PITCHKIT_BRAND_RESULT_HINTS =
|
|
285
|
-
"+12% CTR · 3.2x ROAS · 1.4M views · Sold out in 48h · Series A launch";
|
|
286
|
-
|
|
287
|
-
function pastBrandLogoKeyLabel(logoKey: string) {
|
|
288
|
-
return logoKey
|
|
289
|
-
.split("-")
|
|
290
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
291
|
-
.join("-");
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function pastBrandResultIssues(value: string) {
|
|
295
|
-
if (/[\n\r]/.test(value)) return "Keep the result on one line.";
|
|
296
|
-
if (/https?:\/\/|www\./i.test(value)) return "Links are not allowed in a result.";
|
|
297
|
-
if (/(^|[\s])@[a-z0-9._]+/i.test(value)) return "@handles are not allowed in a result.";
|
|
298
|
-
const emojiCount = value.match(/\p{Extended_Pictographic}/gu)?.length ?? 0;
|
|
299
|
-
if (emojiCount >= 3) return "Skip emoji spam — use a short phrase.";
|
|
300
|
-
return undefined;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function OwnerPastBrands({
|
|
304
|
-
brands,
|
|
305
|
-
onBrandsChange,
|
|
306
|
-
}: {
|
|
307
|
-
brands: PitchKitPastBrand[];
|
|
308
|
-
onBrandsChange: (brands: PitchKitPastBrand[]) => void;
|
|
309
|
-
}) {
|
|
310
|
-
const [dialogOpen, setDialogOpen] = useState(false);
|
|
311
|
-
const [draftName, setDraftName] = useState("");
|
|
312
|
-
const [draftResult, setDraftResult] = useState("");
|
|
313
|
-
const [draftLogoKey, setDraftLogoKey] = useState(PITCHKIT_BRAND_LOGO_LETTER);
|
|
314
|
-
const [editingId, setEditingId] = useState<string | null>(null);
|
|
315
|
-
const [draggingId, setDraggingId] = useState<string | null>(null);
|
|
316
|
-
const canAdd = brands.length < PITCHKIT_BRANDS_MAX;
|
|
317
|
-
const trimmedDraft = draftName.trim();
|
|
318
|
-
const resultIssue = pastBrandResultIssues(draftResult);
|
|
319
|
-
const canSubmit = trimmedDraft.length > 0 && resultIssue == null;
|
|
320
|
-
|
|
321
|
-
function openAdd() {
|
|
322
|
-
if (!canAdd) return;
|
|
323
|
-
setEditingId(null);
|
|
324
|
-
setDraftName("");
|
|
325
|
-
setDraftResult("");
|
|
326
|
-
setDraftLogoKey(PITCHKIT_BRAND_LOGO_LETTER);
|
|
327
|
-
setDialogOpen(true);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function openEdit(brand: PitchKitPastBrand) {
|
|
331
|
-
setEditingId(brand.id);
|
|
332
|
-
setDraftName(brand.name);
|
|
333
|
-
setDraftResult(brand.result_label ?? "");
|
|
334
|
-
setDraftLogoKey(resolvePastBrandLogoKey(brand.logo_key) ?? PITCHKIT_BRAND_LOGO_LETTER);
|
|
335
|
-
setDialogOpen(true);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function closeDialog() {
|
|
339
|
-
setDialogOpen(false);
|
|
340
|
-
setEditingId(null);
|
|
341
|
-
setDraftName("");
|
|
342
|
-
setDraftResult("");
|
|
343
|
-
setDraftLogoKey(PITCHKIT_BRAND_LOGO_LETTER);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function nextBrandFields() {
|
|
347
|
-
const logo_key = resolvePastBrandLogoKey(draftLogoKey);
|
|
348
|
-
const result_label = normalizePastBrandResult(draftResult);
|
|
349
|
-
return {
|
|
350
|
-
name: trimmedDraft,
|
|
351
|
-
...(logo_key == null ? {} : { logo_key }),
|
|
352
|
-
...(result_label == null ? {} : { result_label }),
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function submitBrand() {
|
|
357
|
-
if (!canSubmit) return;
|
|
358
|
-
const fields = nextBrandFields();
|
|
359
|
-
if (editingId == null) {
|
|
360
|
-
if (!canAdd) return;
|
|
361
|
-
onBrandsChange([...brands, { id: crypto.randomUUID(), ...fields }]);
|
|
362
|
-
} else {
|
|
363
|
-
onBrandsChange(
|
|
364
|
-
brands.map((brand) =>
|
|
365
|
-
brand.id === editingId ? { ...brand, ...fields } : brand,
|
|
366
|
-
),
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
closeDialog();
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function moveBrand(id: string, direction: -1 | 1) {
|
|
373
|
-
const index = brands.findIndex((brand) => brand.id === id);
|
|
374
|
-
const nextIndex = index + direction;
|
|
375
|
-
if (index < 0 || nextIndex < 0 || nextIndex >= brands.length) return;
|
|
376
|
-
const next = [...brands];
|
|
377
|
-
const [item] = next.splice(index, 1);
|
|
378
|
-
next.splice(nextIndex, 0, item);
|
|
379
|
-
onBrandsChange(next);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function reorderBrand(sourceId: string, targetId: string) {
|
|
383
|
-
if (sourceId === targetId) return;
|
|
384
|
-
const from = brands.findIndex((brand) => brand.id === sourceId);
|
|
385
|
-
const to = brands.findIndex((brand) => brand.id === targetId);
|
|
386
|
-
if (from < 0 || to < 0) return;
|
|
387
|
-
const next = [...brands];
|
|
388
|
-
const [item] = next.splice(from, 1);
|
|
389
|
-
next.splice(to, 0, item);
|
|
390
|
-
onBrandsChange(next);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return (
|
|
394
|
-
<section className="band min-w-0 gap-y-4">
|
|
395
|
-
<div className="col-span-full flex flex-wrap items-end justify-between gap-3">
|
|
396
|
-
<h2 className={cardTitleClasses}>Past brands</h2>
|
|
397
|
-
{brands.length > 0 && canAdd ? (
|
|
398
|
-
<Button role="secondary" size="sm" onClick={openAdd}>
|
|
399
|
-
Add
|
|
400
|
-
</Button>
|
|
401
|
-
) : null}
|
|
402
|
-
</div>
|
|
403
|
-
{brands.length === 0 ? (
|
|
404
|
-
<Button role="ghost" onClick={openAdd}>
|
|
405
|
-
Add brands you've worked with
|
|
406
|
-
</Button>
|
|
407
|
-
) : (
|
|
408
|
-
<div className="flex col-span-full min-w-0 flex-col gap-3">
|
|
409
|
-
{brands.map((brand, index) => {
|
|
410
|
-
const hasResult = normalizePastBrandResult(brand.result_label) != null;
|
|
411
|
-
return (
|
|
412
|
-
<Card
|
|
413
|
-
key={brand.id}
|
|
414
|
-
variant="outlined"
|
|
415
|
-
shape="rounded"
|
|
416
|
-
className="col-span-full min-w-0"
|
|
417
|
-
draggable
|
|
418
|
-
onDragStart={() => setDraggingId(brand.id)}
|
|
419
|
-
onDragOver={(event) => event.preventDefault()}
|
|
420
|
-
onDrop={() => {
|
|
421
|
-
if (draggingId != null) reorderBrand(draggingId, brand.id);
|
|
422
|
-
setDraggingId(null);
|
|
423
|
-
}}
|
|
424
|
-
onDragEnd={() => setDraggingId(null)}
|
|
425
|
-
>
|
|
426
|
-
<Card.Header
|
|
427
|
-
start={
|
|
428
|
-
<div className="flex min-w-0 items-center gap-3">
|
|
429
|
-
<span className="flex shrink-0 items-center">
|
|
430
|
-
<ButtonIcon size="sm">
|
|
431
|
-
<GripVertical />
|
|
432
|
-
</ButtonIcon>
|
|
433
|
-
<IconButton
|
|
434
|
-
size="xs"
|
|
435
|
-
icon={<ChevronUp />}
|
|
436
|
-
aria-label={`Move ${brand.name} up`}
|
|
437
|
-
disabled={index === 0}
|
|
438
|
-
onClick={() => moveBrand(brand.id, -1)}
|
|
439
|
-
/>
|
|
440
|
-
<IconButton
|
|
441
|
-
size="xs"
|
|
442
|
-
icon={<ChevronDown />}
|
|
443
|
-
aria-label={`Move ${brand.name} down`}
|
|
444
|
-
disabled={index === brands.length - 1}
|
|
445
|
-
onClick={() => moveBrand(brand.id, 1)}
|
|
446
|
-
/>
|
|
447
|
-
</span>
|
|
448
|
-
<BrandMark name={brand.name} logoKey={brand.logo_key} />
|
|
449
|
-
<h3 className="type-label text-fg">{brand.name}</h3>
|
|
450
|
-
<BrandResultChip label={brand.result_label} />
|
|
451
|
-
</div>
|
|
452
|
-
}
|
|
453
|
-
end={
|
|
454
|
-
<MoreMenu
|
|
455
|
-
aria-label={`Manage ${brand.name}`}
|
|
456
|
-
size="xs"
|
|
457
|
-
items={[
|
|
458
|
-
{
|
|
459
|
-
id: "edit",
|
|
460
|
-
label: "Edit",
|
|
461
|
-
start: (
|
|
462
|
-
<ButtonIcon size="sm">
|
|
463
|
-
<Pencil />
|
|
464
|
-
</ButtonIcon>
|
|
465
|
-
),
|
|
466
|
-
},
|
|
467
|
-
hasResult
|
|
468
|
-
? { id: "clear-result", label: "Clear result" }
|
|
469
|
-
: {
|
|
470
|
-
id: "add-result",
|
|
471
|
-
label: "Add result",
|
|
472
|
-
start: (
|
|
473
|
-
<ButtonIcon size="sm">
|
|
474
|
-
<Plus />
|
|
475
|
-
</ButtonIcon>
|
|
476
|
-
),
|
|
477
|
-
},
|
|
478
|
-
{
|
|
479
|
-
id: "remove",
|
|
480
|
-
label: "Remove",
|
|
481
|
-
start: (
|
|
482
|
-
<ButtonIcon size="sm">
|
|
483
|
-
<Trash2 />
|
|
484
|
-
</ButtonIcon>
|
|
485
|
-
),
|
|
486
|
-
},
|
|
487
|
-
]}
|
|
488
|
-
onAction={(actionId) => {
|
|
489
|
-
if (actionId === "edit" || actionId === "add-result") openEdit(brand);
|
|
490
|
-
if (actionId === "clear-result") {
|
|
491
|
-
onBrandsChange(
|
|
492
|
-
brands.map((item) => {
|
|
493
|
-
if (item.id !== brand.id) return item;
|
|
494
|
-
return {
|
|
495
|
-
id: item.id,
|
|
496
|
-
name: item.name,
|
|
497
|
-
...(item.logo_key == null ? {} : { logo_key: item.logo_key }),
|
|
498
|
-
};
|
|
499
|
-
}),
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
if (actionId === "remove") {
|
|
503
|
-
onBrandsChange(brands.filter((item) => item.id !== brand.id));
|
|
504
|
-
}
|
|
505
|
-
}}
|
|
506
|
-
/>
|
|
507
|
-
}
|
|
508
|
-
/>
|
|
509
|
-
</Card>
|
|
510
|
-
);
|
|
511
|
-
})}
|
|
512
|
-
</div>
|
|
513
|
-
)}
|
|
514
|
-
<Dialog open={dialogOpen} onOpenChange={(open) => (open ? setDialogOpen(true) : closeDialog())}>
|
|
515
|
-
<Dialog.Content
|
|
516
|
-
title="Past brands"
|
|
517
|
-
footer={
|
|
518
|
-
<div className={dialogFooterActionsClasses}>
|
|
519
|
-
<Button size="sm" role="secondary" onClick={closeDialog}>
|
|
520
|
-
Cancel
|
|
521
|
-
</Button>
|
|
522
|
-
<Button
|
|
523
|
-
size="sm"
|
|
524
|
-
role="primary"
|
|
525
|
-
disabled={!canSubmit}
|
|
526
|
-
onClick={submitBrand}
|
|
527
|
-
>
|
|
528
|
-
{editingId == null ? "Add" : "Save"}
|
|
529
|
-
</Button>
|
|
530
|
-
</div>
|
|
531
|
-
}
|
|
532
|
-
>
|
|
533
|
-
<div className="flex w-full min-w-0 flex-col gap-4">
|
|
534
|
-
<Input
|
|
535
|
-
label="Brand name"
|
|
536
|
-
value={draftName}
|
|
537
|
-
maxLength={PITCHKIT_BRAND_NAME_MAX}
|
|
538
|
-
onChange={(event) => setDraftName(event.target.value)}
|
|
539
|
-
/>
|
|
540
|
-
<Input
|
|
541
|
-
label="Result"
|
|
542
|
-
description={PITCHKIT_BRAND_RESULT_HINTS}
|
|
543
|
-
placeholder="Optional short phrase"
|
|
544
|
-
value={draftResult}
|
|
545
|
-
maxLength={PITCHKIT_BRAND_RESULT_MAX}
|
|
546
|
-
status={resultIssue == null ? undefined : "error"}
|
|
547
|
-
message={resultIssue}
|
|
548
|
-
onChange={(event) => setDraftResult(event.target.value)}
|
|
549
|
-
/>
|
|
550
|
-
<Select
|
|
551
|
-
label="Logo"
|
|
552
|
-
description="Optional curated mark. Missing or unknown keys use a letter Avatar."
|
|
553
|
-
options={[
|
|
554
|
-
{ value: PITCHKIT_BRAND_LOGO_LETTER, label: "Letter avatar" },
|
|
555
|
-
...pitchKitBrandLogoKeys.map((key) => ({
|
|
556
|
-
value: key,
|
|
557
|
-
label: pastBrandLogoKeyLabel(key),
|
|
558
|
-
})),
|
|
559
|
-
]}
|
|
560
|
-
value={draftLogoKey}
|
|
561
|
-
onValueChange={setDraftLogoKey}
|
|
562
|
-
/>
|
|
563
|
-
</div>
|
|
564
|
-
</Dialog.Content>
|
|
565
|
-
</Dialog>
|
|
566
|
-
</section>
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
export function PastBrandsOwnerPage({
|
|
571
|
-
identity,
|
|
572
|
-
brands,
|
|
573
|
-
onBrandsChange,
|
|
574
|
-
}: {
|
|
575
|
-
identity: PitchKitCreatorIdentity;
|
|
576
|
-
brands: PitchKitPastBrand[];
|
|
577
|
-
onBrandsChange: (brands: PitchKitPastBrand[]) => void;
|
|
578
|
-
}) {
|
|
579
|
-
return (
|
|
580
|
-
<main className="grid-page min-h-screen bg-body [--grid-column-gap:8px] [--grid-max:1140px] [padding-bottom:44px]">
|
|
581
|
-
<div className="band pb-4">
|
|
582
|
-
<header className="col-span-full grid grid-cols-[1fr_auto_1fr] items-center gap-3">
|
|
583
|
-
<span className="type-heading-4 text-fg text-fg">PitchKit</span>
|
|
584
|
-
<span />
|
|
585
|
-
<span className="justify-self-end">
|
|
586
|
-
<Avatar
|
|
587
|
-
name={identity.displayName ?? identity.handle}
|
|
588
|
-
src={identity.profilePictureUrl}
|
|
589
|
-
size="md"
|
|
590
|
-
/>
|
|
591
|
-
</span>
|
|
592
|
-
</header>
|
|
593
|
-
</div>
|
|
594
|
-
|
|
595
|
-
<div className="band pt-6 sm:pt-8">
|
|
596
|
-
<div className="band min-w-0 gap-y-6 sm:gap-y-8">
|
|
597
|
-
<section className="col-span-full border-b border-border pb-6">
|
|
598
|
-
<div className="flex min-w-0 flex-col gap-3">
|
|
599
|
-
<CreatorIdentityStrip
|
|
600
|
-
identity={identity}
|
|
601
|
-
nameAs="h1"
|
|
602
|
-
showProfessionalChip
|
|
603
|
-
/>
|
|
604
|
-
</div>
|
|
605
|
-
</section>
|
|
606
|
-
<OwnerPastBrands brands={brands} onBrandsChange={onBrandsChange} />
|
|
607
|
-
</div>
|
|
608
|
-
</div>
|
|
609
|
-
</main>
|
|
610
|
-
);
|
|
611
|
-
}
|