@fayz-ai/plugin-linkinbio 0.9.1 → 0.10.1
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/public/{data.d.ts → data/index.d.ts} +2 -2
- package/dist/public/data/index.d.ts.map +1 -0
- package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
- package/dist/public/data/supabase.d.ts.map +1 -0
- package/dist/public/index.d.ts +8 -8
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +2 -2
- package/dist/public/index.js.map +1 -1
- package/dist/public/{registry.d.ts → lib/registry.d.ts} +1 -1
- package/dist/public/lib/registry.d.ts.map +1 -0
- package/dist/public/{utils.d.ts → lib/utils.d.ts} +1 -1
- package/dist/public/lib/utils.d.ts.map +1 -0
- package/dist/public/views/BioPage.d.ts.map +1 -0
- package/dist/public/{BioPageRenderer.d.ts → views/BioPageRenderer.d.ts} +1 -1
- package/dist/public/views/BioPageRenderer.d.ts.map +1 -0
- package/package.json +12 -15
- package/dist/index.cjs +0 -33
- package/dist/index.cjs.map +0 -1
- package/dist/public/BioPage.d.ts.map +0 -1
- package/dist/public/BioPageRenderer.d.ts.map +0 -1
- package/dist/public/__tests__/manifest.test.d.ts +0 -2
- package/dist/public/__tests__/manifest.test.d.ts.map +0 -1
- package/dist/public/data.d.ts.map +0 -1
- package/dist/public/data.supabase.d.ts.map +0 -1
- package/dist/public/index.cjs +0 -1984
- package/dist/public/index.cjs.map +0 -1
- package/dist/public/registry.d.ts.map +0 -1
- package/dist/public/utils.d.ts.map +0 -1
- package/src/index.ts +0 -56
- package/src/public/BioPage.tsx +0 -123
- package/src/public/BioPageRenderer.tsx +0 -251
- package/src/public/__tests__/manifest.test.ts +0 -39
- package/src/public/blocks/BioSection.tsx +0 -66
- package/src/public/blocks/CTASection.tsx +0 -56
- package/src/public/blocks/EmbedSection.tsx +0 -53
- package/src/public/blocks/FeaturedBlock.tsx +0 -70
- package/src/public/blocks/GallerySection.tsx +0 -148
- package/src/public/blocks/HeroSection.tsx +0 -107
- package/src/public/blocks/LinksBlock.tsx +0 -80
- package/src/public/blocks/MusicReleasesBlock.tsx +0 -83
- package/src/public/blocks/ProfileHeader.tsx +0 -174
- package/src/public/blocks/ProjectSection.tsx +0 -158
- package/src/public/blocks/SocialLinksSection.tsx +0 -125
- package/src/public/blocks/StatsSection.tsx +0 -46
- package/src/public/blocks/TextSection.tsx +0 -29
- package/src/public/blocks/TourDatesBlock.tsx +0 -83
- package/src/public/blocks/VenueListSection.tsx +0 -91
- package/src/public/blocks/VideoGridSection.tsx +0 -105
- package/src/public/blocks/index.ts +0 -16
- package/src/public/components/Carousel.tsx +0 -90
- package/src/public/components/MediaKit.tsx +0 -249
- package/src/public/components/PlatformIcon.tsx +0 -56
- package/src/public/components/Reveal.tsx +0 -58
- package/src/public/context.tsx +0 -30
- package/src/public/createPublicLinkInBioPlugin.ts +0 -81
- package/src/public/data.supabase.ts +0 -31
- package/src/public/data.ts +0 -29
- package/src/public/index.ts +0 -75
- package/src/public/registry.ts +0 -40
- package/src/public/utils.ts +0 -102
- package/src/types.ts +0 -337
- /package/dist/public/{BioPage.d.ts → views/BioPage.d.ts} +0 -0
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState, type CSSProperties } from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
import { Download, X, Copy, Check, FileText, ExternalLink } from 'lucide-react';
|
|
4
|
-
import type { BioPage, BioMediaItem } from '../../types';
|
|
5
|
-
import { getBrandingVars } from '../utils';
|
|
6
|
-
|
|
7
|
-
interface Asset {
|
|
8
|
-
label: string;
|
|
9
|
-
url: string;
|
|
10
|
-
kind: 'image' | 'logo' | 'doc' | 'audio' | 'link';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Derive downloadable assets from the page (logo, photos, bio text) + explicit mediaKit. */
|
|
14
|
-
function derive(page: BioPage): { images: Asset[]; bioText: string; extras: Asset[]; driveUrl?: string } {
|
|
15
|
-
const seen = new Set<string>();
|
|
16
|
-
const images: Asset[] = [];
|
|
17
|
-
const pushImg = (label: string, url: string, kind: Asset['kind'] = 'image') => {
|
|
18
|
-
if (!url || seen.has(url) || !url.startsWith('/')) return; // only local (own) assets
|
|
19
|
-
seen.add(url);
|
|
20
|
-
images.push({ label, url, kind });
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
if (page.branding.logoUrl) pushImg('Logo', page.branding.logoUrl, 'logo');
|
|
24
|
-
|
|
25
|
-
let bioText = '';
|
|
26
|
-
for (const s of page.sections) {
|
|
27
|
-
if (s.type === 'profile-header' && s.imageUrl) pushImg('Foto de capa', s.imageUrl);
|
|
28
|
-
if (s.type === 'hero' && s.backgroundImageUrl) pushImg('Foto de capa', s.backgroundImageUrl);
|
|
29
|
-
if (s.type === 'gallery') s.media.forEach((m: BioMediaItem, i: number) => m.type === 'image' && pushImg(`Foto ${i + 1}`, m.url));
|
|
30
|
-
if (s.type === 'bio' || s.type === 'text') {
|
|
31
|
-
if (s.content) bioText += `${s.title ? `## ${s.title}\n\n` : ''}${s.content}\n\n`;
|
|
32
|
-
if (s.type === 'bio' && s.imageUrl) pushImg('Foto', s.imageUrl);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const extras = (page.mediaKit?.assets ?? []).map(
|
|
37
|
-
(a: { label: string; url: string; kind?: Asset['kind'] }): Asset => ({ label: a.label, url: a.url, kind: a.kind ?? 'link' }),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
return { images, bioText: bioText.trim(), extras, driveUrl: page.mediaKit?.driveUrl };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function fileName(url: string, fallback: string): string {
|
|
44
|
-
const base = url.split('/').pop()?.split('?')[0];
|
|
45
|
-
return base && base.includes('.') ? base : fallback;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function MediaKit({ page, variant = 'inline' }: { page: BioPage; variant?: 'inline' | 'fab' }) {
|
|
49
|
-
const [open, setOpen] = useState(false);
|
|
50
|
-
const [copied, setCopied] = useState(false);
|
|
51
|
-
const { images, bioText, extras, driveUrl } = useMemo(() => derive(page), [page]);
|
|
52
|
-
|
|
53
|
-
if (!images.length && !bioText && !extras.length && !driveUrl) return null;
|
|
54
|
-
|
|
55
|
-
const copyBio = async () => {
|
|
56
|
-
try {
|
|
57
|
-
await navigator.clipboard.writeText(bioText);
|
|
58
|
-
setCopied(true);
|
|
59
|
-
setTimeout(() => setCopied(false), 1800);
|
|
60
|
-
} catch {
|
|
61
|
-
/* clipboard unavailable */
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const downloadBio = () => {
|
|
66
|
-
const blob = new Blob([bioText], { type: 'text/plain;charset=utf-8' });
|
|
67
|
-
const url = URL.createObjectURL(blob);
|
|
68
|
-
const a = document.createElement('a');
|
|
69
|
-
a.href = url;
|
|
70
|
-
a.download = `${page.identity.slug || 'bio'}-bio.txt`;
|
|
71
|
-
a.click();
|
|
72
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const modal = (
|
|
76
|
-
<div
|
|
77
|
-
className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-0 sm:p-4"
|
|
78
|
-
style={{
|
|
79
|
-
...(getBrandingVars(page.branding) as CSSProperties),
|
|
80
|
-
background: 'rgba(0,0,0,0.6)',
|
|
81
|
-
backdropFilter: 'blur(4px)',
|
|
82
|
-
fontFamily: 'var(--pk-font-body)',
|
|
83
|
-
}}
|
|
84
|
-
onClick={() => setOpen(false)}
|
|
85
|
-
>
|
|
86
|
-
<div
|
|
87
|
-
className="w-full sm:max-w-lg max-h-[88vh] overflow-y-auto rounded-t-3xl sm:rounded-3xl"
|
|
88
|
-
style={{ backgroundColor: 'var(--pk-bg)', border: '1px solid rgba(255,255,255,0.1)' }}
|
|
89
|
-
onClick={(e) => e.stopPropagation()}
|
|
90
|
-
>
|
|
91
|
-
{/* Header */}
|
|
92
|
-
<div
|
|
93
|
-
className="sticky top-0 flex items-center justify-between px-5 py-4 z-10"
|
|
94
|
-
style={{ backgroundColor: 'var(--pk-bg)', borderBottom: '1px solid rgba(255,255,255,0.08)' }}
|
|
95
|
-
>
|
|
96
|
-
<div className="flex items-center gap-2">
|
|
97
|
-
<Download size={18} style={{ color: 'var(--pk-primary)' }} />
|
|
98
|
-
<h3 className="text-base font-bold" style={{ color: 'var(--pk-text)', fontFamily: 'var(--pk-font-heading)' }}>
|
|
99
|
-
Mídia Kit
|
|
100
|
-
</h3>
|
|
101
|
-
</div>
|
|
102
|
-
<button
|
|
103
|
-
type="button"
|
|
104
|
-
aria-label="Fechar"
|
|
105
|
-
onClick={() => setOpen(false)}
|
|
106
|
-
className="flex items-center justify-center w-8 h-8 rounded-full"
|
|
107
|
-
style={{ backgroundColor: 'rgba(255,255,255,0.08)', color: 'var(--pk-text)' }}
|
|
108
|
-
>
|
|
109
|
-
<X size={16} />
|
|
110
|
-
</button>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
<div className="px-5 py-5 space-y-6">
|
|
114
|
-
{/* Full kit (Drive) */}
|
|
115
|
-
{driveUrl && (
|
|
116
|
-
<a
|
|
117
|
-
href={driveUrl}
|
|
118
|
-
target="_blank"
|
|
119
|
-
rel="noopener noreferrer"
|
|
120
|
-
className="flex items-center gap-3 p-3 rounded-xl transition-transform hover:scale-[1.01]"
|
|
121
|
-
style={{ backgroundColor: 'var(--pk-primary)', color: 'var(--pk-bg)' }}
|
|
122
|
-
>
|
|
123
|
-
<ExternalLink size={18} />
|
|
124
|
-
<span className="text-sm font-semibold">Abrir kit completo (Drive)</span>
|
|
125
|
-
</a>
|
|
126
|
-
)}
|
|
127
|
-
|
|
128
|
-
{/* Bio */}
|
|
129
|
-
{bioText && (
|
|
130
|
-
<section>
|
|
131
|
-
<h4 className="text-xs font-bold uppercase tracking-wider mb-2 opacity-60" style={{ color: 'var(--pk-muted)' }}>
|
|
132
|
-
Bio / Release
|
|
133
|
-
</h4>
|
|
134
|
-
<p
|
|
135
|
-
className="text-sm leading-relaxed max-h-32 overflow-y-auto p-3 rounded-xl"
|
|
136
|
-
style={{ color: 'var(--pk-muted)', backgroundColor: 'rgba(255,255,255,0.04)' }}
|
|
137
|
-
>
|
|
138
|
-
{bioText.replace(/[*#]/g, '')}
|
|
139
|
-
</p>
|
|
140
|
-
<div className="flex gap-2 mt-2">
|
|
141
|
-
<button
|
|
142
|
-
type="button"
|
|
143
|
-
onClick={copyBio}
|
|
144
|
-
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold rounded-full"
|
|
145
|
-
style={{ backgroundColor: 'rgba(255,255,255,0.08)', color: 'var(--pk-text)' }}
|
|
146
|
-
>
|
|
147
|
-
{copied ? <Check size={13} /> : <Copy size={13} />} {copied ? 'Copiado' : 'Copiar'}
|
|
148
|
-
</button>
|
|
149
|
-
<button
|
|
150
|
-
type="button"
|
|
151
|
-
onClick={downloadBio}
|
|
152
|
-
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold rounded-full"
|
|
153
|
-
style={{ backgroundColor: 'rgba(255,255,255,0.08)', color: 'var(--pk-text)' }}
|
|
154
|
-
>
|
|
155
|
-
<FileText size={13} /> .txt
|
|
156
|
-
</button>
|
|
157
|
-
</div>
|
|
158
|
-
</section>
|
|
159
|
-
)}
|
|
160
|
-
|
|
161
|
-
{/* Photos + logo */}
|
|
162
|
-
{images.length > 0 && (
|
|
163
|
-
<section>
|
|
164
|
-
<h4 className="text-xs font-bold uppercase tracking-wider mb-2 opacity-60" style={{ color: 'var(--pk-muted)' }}>
|
|
165
|
-
Fotos & Logo
|
|
166
|
-
</h4>
|
|
167
|
-
<div className="grid grid-cols-3 gap-2">
|
|
168
|
-
{images.map((img) => (
|
|
169
|
-
<a
|
|
170
|
-
key={img.url}
|
|
171
|
-
href={img.url}
|
|
172
|
-
download={fileName(img.url, `${page.identity.slug}-${img.label}`)}
|
|
173
|
-
className="group relative aspect-square overflow-hidden rounded-xl"
|
|
174
|
-
style={{ backgroundColor: 'rgba(255,255,255,0.06)' }}
|
|
175
|
-
title={`Baixar ${img.label}`}
|
|
176
|
-
>
|
|
177
|
-
<img
|
|
178
|
-
src={img.url}
|
|
179
|
-
alt={img.label}
|
|
180
|
-
className={`w-full h-full ${img.kind === 'logo' ? 'object-contain p-3' : 'object-cover'}`}
|
|
181
|
-
loading="lazy"
|
|
182
|
-
/>
|
|
183
|
-
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
|
184
|
-
style={{ background: 'rgba(0,0,0,0.45)' }}
|
|
185
|
-
>
|
|
186
|
-
<Download size={20} style={{ color: '#fff' }} />
|
|
187
|
-
</div>
|
|
188
|
-
</a>
|
|
189
|
-
))}
|
|
190
|
-
</div>
|
|
191
|
-
</section>
|
|
192
|
-
)}
|
|
193
|
-
|
|
194
|
-
{/* Extra resources */}
|
|
195
|
-
{extras.length > 0 && (
|
|
196
|
-
<section>
|
|
197
|
-
<h4 className="text-xs font-bold uppercase tracking-wider mb-2 opacity-60" style={{ color: 'var(--pk-muted)' }}>
|
|
198
|
-
Recursos
|
|
199
|
-
</h4>
|
|
200
|
-
<div className="flex flex-col gap-2">
|
|
201
|
-
{extras.map((a) => (
|
|
202
|
-
<a
|
|
203
|
-
key={a.url}
|
|
204
|
-
href={a.url}
|
|
205
|
-
target="_blank"
|
|
206
|
-
rel="noopener noreferrer"
|
|
207
|
-
download={a.kind !== 'link' ? fileName(a.url, a.label) : undefined}
|
|
208
|
-
className="flex items-center gap-3 p-3 rounded-xl"
|
|
209
|
-
style={{ backgroundColor: 'rgba(255,255,255,0.05)', color: 'var(--pk-text)' }}
|
|
210
|
-
>
|
|
211
|
-
{a.kind === 'link' ? <ExternalLink size={16} /> : <FileText size={16} />}
|
|
212
|
-
<span className="text-sm font-medium">{a.label}</span>
|
|
213
|
-
<Download size={15} className="ml-auto opacity-50" />
|
|
214
|
-
</a>
|
|
215
|
-
))}
|
|
216
|
-
</div>
|
|
217
|
-
</section>
|
|
218
|
-
)}
|
|
219
|
-
</div>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
const isFab = variant === 'fab';
|
|
225
|
-
return (
|
|
226
|
-
<>
|
|
227
|
-
<button
|
|
228
|
-
type="button"
|
|
229
|
-
onClick={() => setOpen(true)}
|
|
230
|
-
aria-label="Baixar mídia kit"
|
|
231
|
-
title="Mídia Kit"
|
|
232
|
-
className={`flex items-center justify-center rounded-full transition-transform hover:scale-110 ${isFab ? 'w-12 h-12' : 'w-11 h-11'}`}
|
|
233
|
-
style={
|
|
234
|
-
isFab
|
|
235
|
-
? {
|
|
236
|
-
color: 'var(--pk-primary)',
|
|
237
|
-
border: '1.5px solid var(--pk-primary)',
|
|
238
|
-
backgroundColor: 'color-mix(in srgb, var(--pk-bg) 70%, transparent)',
|
|
239
|
-
backdropFilter: 'blur(6px)',
|
|
240
|
-
}
|
|
241
|
-
: { backgroundColor: 'rgba(255,255,255,0.07)', color: 'var(--pk-text)' }
|
|
242
|
-
}
|
|
243
|
-
>
|
|
244
|
-
<Download size={isFab ? 20 : 19} />
|
|
245
|
-
</button>
|
|
246
|
-
{open && typeof document !== 'undefined' && createPortal(modal, document.body)}
|
|
247
|
-
</>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties, ComponentType } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
SiInstagram,
|
|
4
|
-
SiYoutube,
|
|
5
|
-
SiSoundcloud,
|
|
6
|
-
SiTiktok,
|
|
7
|
-
SiSpotify,
|
|
8
|
-
SiWhatsapp,
|
|
9
|
-
SiApplemusic,
|
|
10
|
-
SiBandcamp,
|
|
11
|
-
SiFacebook,
|
|
12
|
-
SiX,
|
|
13
|
-
SiTelegram,
|
|
14
|
-
SiBeatport,
|
|
15
|
-
SiGoogledrive,
|
|
16
|
-
SiMixcloud,
|
|
17
|
-
} from 'react-icons/si';
|
|
18
|
-
import { Globe, Mail, ExternalLink } from 'lucide-react';
|
|
19
|
-
|
|
20
|
-
type IconProps = { size?: number | string; style?: CSSProperties; className?: string };
|
|
21
|
-
|
|
22
|
-
// Real brand glyphs for social/platform links (react-icons → simple-icons set).
|
|
23
|
-
const BRAND: Record<string, ComponentType<IconProps>> = {
|
|
24
|
-
instagram: SiInstagram,
|
|
25
|
-
youtube: SiYoutube,
|
|
26
|
-
soundcloud: SiSoundcloud,
|
|
27
|
-
tiktok: SiTiktok,
|
|
28
|
-
spotify: SiSpotify,
|
|
29
|
-
whatsapp: SiWhatsapp,
|
|
30
|
-
'apple-music': SiApplemusic,
|
|
31
|
-
bandcamp: SiBandcamp,
|
|
32
|
-
facebook: SiFacebook,
|
|
33
|
-
twitter: SiX,
|
|
34
|
-
telegram: SiTelegram,
|
|
35
|
-
beatport: SiBeatport,
|
|
36
|
-
mixcloud: SiMixcloud,
|
|
37
|
-
drive: SiGoogledrive,
|
|
38
|
-
email: Mail,
|
|
39
|
-
website: Globe,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/** Renders the real brand icon for a platform, falling back to a generic link glyph. */
|
|
43
|
-
export function PlatformIcon({
|
|
44
|
-
platform,
|
|
45
|
-
size = 20,
|
|
46
|
-
style,
|
|
47
|
-
className,
|
|
48
|
-
}: {
|
|
49
|
-
platform: string;
|
|
50
|
-
size?: number;
|
|
51
|
-
style?: CSSProperties;
|
|
52
|
-
className?: string;
|
|
53
|
-
}) {
|
|
54
|
-
const Icon = BRAND[platform] ?? ExternalLink;
|
|
55
|
-
return <Icon size={size} style={style} className={className} />;
|
|
56
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Scroll-reveal wrapper: fades + lifts its children into view the first time
|
|
5
|
-
* they intersect the viewport. Respects prefers-reduced-motion (renders shown).
|
|
6
|
-
* Works inside the page's `fixed inset-0` scroll container because the default
|
|
7
|
-
* IntersectionObserver root is the viewport.
|
|
8
|
-
*/
|
|
9
|
-
export function Reveal({
|
|
10
|
-
children,
|
|
11
|
-
delay = 0,
|
|
12
|
-
className,
|
|
13
|
-
}: {
|
|
14
|
-
children: ReactNode;
|
|
15
|
-
delay?: number;
|
|
16
|
-
className?: string;
|
|
17
|
-
}) {
|
|
18
|
-
const ref = useRef<HTMLDivElement>(null);
|
|
19
|
-
const [shown, setShown] = useState(false);
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
const el = ref.current;
|
|
23
|
-
if (!el) return;
|
|
24
|
-
const reduce =
|
|
25
|
-
typeof window !== 'undefined' &&
|
|
26
|
-
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
27
|
-
if (reduce || typeof IntersectionObserver === 'undefined') {
|
|
28
|
-
setShown(true);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const io = new IntersectionObserver(
|
|
32
|
-
([entry]) => {
|
|
33
|
-
if (entry.isIntersecting) {
|
|
34
|
-
setShown(true);
|
|
35
|
-
io.disconnect();
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
{ threshold: 0.1, rootMargin: '0px 0px -6% 0px' },
|
|
39
|
-
);
|
|
40
|
-
io.observe(el);
|
|
41
|
-
return () => io.disconnect();
|
|
42
|
-
}, []);
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div
|
|
46
|
-
ref={ref}
|
|
47
|
-
className={className}
|
|
48
|
-
style={{
|
|
49
|
-
opacity: shown ? 1 : 0,
|
|
50
|
-
transform: shown ? 'none' : 'translateY(22px)',
|
|
51
|
-
transition: `opacity 0.6s ease ${delay}ms, transform 0.6s cubic-bezier(0.2,0.7,0.2,1) ${delay}ms`,
|
|
52
|
-
willChange: 'opacity, transform',
|
|
53
|
-
}}
|
|
54
|
-
>
|
|
55
|
-
{children}
|
|
56
|
-
</div>
|
|
57
|
-
);
|
|
58
|
-
}
|
package/src/public/context.tsx
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, type ReactNode } from 'react';
|
|
2
|
-
import type { BioPageDataProvider } from './data';
|
|
3
|
-
|
|
4
|
-
export interface LinkInBioContextValue {
|
|
5
|
-
provider: BioPageDataProvider;
|
|
6
|
-
/** Optional "made with" footer link rendered by BioPageRenderer. */
|
|
7
|
-
poweredBy?: { label: string; url: string };
|
|
8
|
-
/** Fallback document.title suffix when a page omits seo.title. Default 'Bio'. */
|
|
9
|
-
titleSuffix: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const LinkInBioContext = createContext<LinkInBioContextValue | null>(null);
|
|
13
|
-
|
|
14
|
-
export function LinkInBioProvider({
|
|
15
|
-
value,
|
|
16
|
-
children,
|
|
17
|
-
}: {
|
|
18
|
-
value: LinkInBioContextValue;
|
|
19
|
-
children: ReactNode;
|
|
20
|
-
}) {
|
|
21
|
-
return <LinkInBioContext.Provider value={value}>{children}</LinkInBioContext.Provider>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function useLinkInBio(): LinkInBioContextValue {
|
|
25
|
-
const ctx = useContext(LinkInBioContext);
|
|
26
|
-
if (!ctx) {
|
|
27
|
-
throw new Error('useLinkInBio must be used within a LinkInBioProvider (mount the plugin Provider).');
|
|
28
|
-
}
|
|
29
|
-
return ctx;
|
|
30
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { createElement, type FC, type ReactNode } from 'react';
|
|
2
|
-
import { createSafeDataProvider, type PluginManifest, type PluginScope, type VerticalId } from '@fayz-ai/core';
|
|
3
|
-
import type { BioPage } from '../types';
|
|
4
|
-
import { createMockBioPageProvider, type BioPageDataProvider } from './data';
|
|
5
|
-
import { createSupabaseBioPageProvider } from './data.supabase';
|
|
6
|
-
import { LinkInBioProvider, type LinkInBioContextValue } from './context';
|
|
7
|
-
import BioPageRoute from './BioPage';
|
|
8
|
-
|
|
9
|
-
export interface PublicLinkInBioOptions {
|
|
10
|
-
/** Mount prefix for the public bio route. The route is `${basePath}/:slug`. Default '/p'. */
|
|
11
|
-
basePath?: string;
|
|
12
|
-
/**
|
|
13
|
-
* When a Supabase client is configured (setGlobalSupabaseClient) this selects
|
|
14
|
-
* the Supabase-backed provider. Omit to force the seeded mock provider.
|
|
15
|
-
*/
|
|
16
|
-
useSupabase?: boolean;
|
|
17
|
-
/** Table holding bio pages (Supabase mode). Default 'press_kits'. */
|
|
18
|
-
table?: string;
|
|
19
|
-
/** Inject a custom provider (overrides the mock/Supabase safe resolver). */
|
|
20
|
-
dataProvider?: BioPageDataProvider;
|
|
21
|
-
/** Seed catalog for the mock provider: slug → BioPage (host config / demo content). */
|
|
22
|
-
seed?: Record<string, BioPage>;
|
|
23
|
-
/** Optional "made with" footer link rendered on every page. */
|
|
24
|
-
poweredBy?: { label: string; url: string };
|
|
25
|
-
/** Fallback `<title>` suffix when a page omits seo.title. Default 'Bio'. */
|
|
26
|
-
titleSuffix?: string;
|
|
27
|
-
scope?: PluginScope;
|
|
28
|
-
verticalId?: VerticalId;
|
|
29
|
-
defaultEnabled?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface PublicLinkInBioPlugin {
|
|
33
|
-
manifest: PluginManifest;
|
|
34
|
-
Provider: FC<{ children: ReactNode }>;
|
|
35
|
-
dataProvider: BioPageDataProvider;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function createPublicLinkInBioPlugin(options: PublicLinkInBioOptions = {}): PublicLinkInBioPlugin {
|
|
39
|
-
const basePath = options.basePath ?? '/p';
|
|
40
|
-
const table = options.table ?? 'press_kits';
|
|
41
|
-
const seed = options.seed ?? {};
|
|
42
|
-
|
|
43
|
-
// Resolution: explicit dataProvider → Supabase (when a global client is
|
|
44
|
-
// configured AND useSupabase !== false) → seeded mock. Deferred + memoized so
|
|
45
|
-
// importing the plugin never forces a Supabase connection.
|
|
46
|
-
const provider: BioPageDataProvider =
|
|
47
|
-
options.dataProvider ??
|
|
48
|
-
(options.useSupabase === false
|
|
49
|
-
? createMockBioPageProvider({ seed })
|
|
50
|
-
: createSafeDataProvider<BioPageDataProvider>(
|
|
51
|
-
() => createSupabaseBioPageProvider({ table }),
|
|
52
|
-
() => createMockBioPageProvider({ seed }),
|
|
53
|
-
));
|
|
54
|
-
|
|
55
|
-
const contextValue: LinkInBioContextValue = {
|
|
56
|
-
provider,
|
|
57
|
-
poweredBy: options.poweredBy,
|
|
58
|
-
titleSuffix: options.titleSuffix ?? 'Bio',
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const Provider: FC<{ children: ReactNode }> = ({ children }) =>
|
|
62
|
-
createElement(LinkInBioProvider, { value: contextValue, children });
|
|
63
|
-
Provider.displayName = 'LinkInBioProvider';
|
|
64
|
-
|
|
65
|
-
const manifest: PluginManifest = {
|
|
66
|
-
id: 'linkinbio',
|
|
67
|
-
name: 'Link in Bio',
|
|
68
|
-
icon: 'Link',
|
|
69
|
-
version: '0.1.0',
|
|
70
|
-
scope: options.scope ?? 'universal',
|
|
71
|
-
verticalId: options.verticalId,
|
|
72
|
-
scaffolds: ['website', 'landing_page'],
|
|
73
|
-
defaultEnabled: options.defaultEnabled ?? true,
|
|
74
|
-
dependencies: [],
|
|
75
|
-
navigation: [],
|
|
76
|
-
routes: [{ path: `${basePath}/:slug`, component: BioPageRoute, guard: 'public', fullBleed: true }],
|
|
77
|
-
widgets: [],
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
return { manifest, Provider, dataProvider: provider };
|
|
81
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { getSupabaseClientOptional } from '@fayz-ai/core';
|
|
3
|
-
import type { BioPage } from '../types';
|
|
4
|
-
import type { BioPageDataProvider } from './data';
|
|
5
|
-
|
|
6
|
-
export interface SupabaseBioPageOptions {
|
|
7
|
-
/**
|
|
8
|
-
* Table holding bio pages. Defaults to `press_kits` — a jsonb `data` column
|
|
9
|
-
* with the full BioPage payload, plus `slug` and `is_published` columns and
|
|
10
|
-
* an RLS policy allowing anon SELECT when `is_published` is true.
|
|
11
|
-
*/
|
|
12
|
-
table?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function createSupabaseBioPageProvider(options: SupabaseBioPageOptions = {}): BioPageDataProvider {
|
|
16
|
-
const table = options.table ?? 'press_kits';
|
|
17
|
-
return {
|
|
18
|
-
async getBySlug(slug: string): Promise<BioPage | null> {
|
|
19
|
-
const client = getSupabaseClientOptional() as any;
|
|
20
|
-
if (!client) return null;
|
|
21
|
-
const { data, error } = await client
|
|
22
|
-
.from(table)
|
|
23
|
-
.select('data, is_published')
|
|
24
|
-
.eq('slug', slug)
|
|
25
|
-
.maybeSingle();
|
|
26
|
-
if (error || !data) return null;
|
|
27
|
-
if (!data.is_published) return null;
|
|
28
|
-
return data.data as BioPage;
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
package/src/public/data.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { BioPage } from '../types';
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// BioPageDataProvider — the read seam for the public bio page.
|
|
5
|
-
//
|
|
6
|
-
// Two implementations ship: a mock provider seeded from an in-memory map (host
|
|
7
|
-
// config / demo content) and a Supabase provider (data.supabase.ts). The
|
|
8
|
-
// factory picks between them via createSafeDataProvider — Supabase when a global
|
|
9
|
-
// client is configured, mock otherwise.
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
export interface BioPageDataProvider {
|
|
13
|
-
/** Resolve a published bio page by its public slug, or null when missing/unpublished. */
|
|
14
|
-
getBySlug(slug: string): Promise<BioPage | null>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface MockBioPageOptions {
|
|
18
|
-
/** slug → BioPage map used as the mock catalog. */
|
|
19
|
-
seed?: Record<string, BioPage>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function createMockBioPageProvider(options: MockBioPageOptions = {}): BioPageDataProvider {
|
|
23
|
-
const seed = options.seed ?? {};
|
|
24
|
-
return {
|
|
25
|
-
async getBySlug(slug: string): Promise<BioPage | null> {
|
|
26
|
-
return seed[slug] ?? null;
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
package/src/public/index.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// @fayz-ai/plugin-linkinbio/public — customer-facing link-in-bio surface.
|
|
3
|
-
//
|
|
4
|
-
// A komi/linktree-style public bio page for any creator: identity + branding +
|
|
5
|
-
// an ordered list of content blocks. Separate entry from the admin plugin so a
|
|
6
|
-
// website host imports only the renderer + data seam (no editor graph).
|
|
7
|
-
//
|
|
8
|
-
// Data: BioPageDataProvider — a seeded mock by default, Supabase (`press_kits`
|
|
9
|
-
// jsonb) when a global client is configured. Extend the block set with
|
|
10
|
-
// registerBlock(type, component).
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
export { createPublicLinkInBioPlugin } from './createPublicLinkInBioPlugin';
|
|
14
|
-
export type { PublicLinkInBioOptions, PublicLinkInBioPlugin } from './createPublicLinkInBioPlugin';
|
|
15
|
-
|
|
16
|
-
export { default as BioPageRenderer } from './BioPageRenderer';
|
|
17
|
-
export type { BioPageRendererProps } from './BioPageRenderer';
|
|
18
|
-
export { default as BioPage } from './BioPage';
|
|
19
|
-
export { LinkInBioProvider, useLinkInBio } from './context';
|
|
20
|
-
export type { LinkInBioContextValue } from './context';
|
|
21
|
-
|
|
22
|
-
// Block registry — the extensibility seam
|
|
23
|
-
export { registerBlock, getBlock, hasBlock } from './registry';
|
|
24
|
-
export type { BlockRenderProps, BlockComponent } from './registry';
|
|
25
|
-
|
|
26
|
-
// Data seam
|
|
27
|
-
export { createMockBioPageProvider } from './data';
|
|
28
|
-
export type { BioPageDataProvider, MockBioPageOptions } from './data';
|
|
29
|
-
export { createSupabaseBioPageProvider } from './data.supabase';
|
|
30
|
-
export type { SupabaseBioPageOptions } from './data.supabase';
|
|
31
|
-
|
|
32
|
-
// Rendering helpers (useful inside custom blocks)
|
|
33
|
-
export {
|
|
34
|
-
renderMarkdown,
|
|
35
|
-
platformColors,
|
|
36
|
-
platformLabels,
|
|
37
|
-
platformIconMap,
|
|
38
|
-
borderRadiusMap,
|
|
39
|
-
getBrandingVars,
|
|
40
|
-
} from './utils';
|
|
41
|
-
|
|
42
|
-
// Domain types
|
|
43
|
-
export type {
|
|
44
|
-
BioPage as BioPageData,
|
|
45
|
-
BioBlock,
|
|
46
|
-
BioBranding,
|
|
47
|
-
BioIdentity,
|
|
48
|
-
BioSEO,
|
|
49
|
-
BioSocialLink,
|
|
50
|
-
BioMediaItem,
|
|
51
|
-
BioFloatingCTA,
|
|
52
|
-
HeroSection,
|
|
53
|
-
BioSection,
|
|
54
|
-
GallerySection,
|
|
55
|
-
VideoGridSection,
|
|
56
|
-
SocialLinksSection,
|
|
57
|
-
EmbedSection,
|
|
58
|
-
ProjectSection,
|
|
59
|
-
StatsSection,
|
|
60
|
-
VenueListSection,
|
|
61
|
-
CTASection,
|
|
62
|
-
TextSection,
|
|
63
|
-
ProfileHeaderSection,
|
|
64
|
-
LinksSection,
|
|
65
|
-
LinkCardItem,
|
|
66
|
-
MusicReleasesSection,
|
|
67
|
-
MusicReleaseItem,
|
|
68
|
-
TourDatesSection,
|
|
69
|
-
TourDateItem,
|
|
70
|
-
FeaturedSection,
|
|
71
|
-
} from '../types';
|
|
72
|
-
|
|
73
|
-
// Shared UI (useful when composing custom blocks)
|
|
74
|
-
export { Reveal } from './components/Reveal';
|
|
75
|
-
export { Carousel } from './components/Carousel';
|
package/src/public/registry.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { ComponentType } from 'react';
|
|
2
|
-
import type { BioBlock, BioPage } from '../types';
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Block registry — the extensibility seam.
|
|
6
|
-
//
|
|
7
|
-
// The 11 built-in blocks are rendered by BioPageRenderer's switch. Hosts that
|
|
8
|
-
// need a niche block type (e.g. a DJ tour map, a course catalog) register it
|
|
9
|
-
// here; BioPageRenderer consults the registry BEFORE its built-in switch, so a
|
|
10
|
-
// registered type wins and unknown types render nothing. This keeps the plugin
|
|
11
|
-
// komi-generic while letting each app extend it without forking.
|
|
12
|
-
//
|
|
13
|
-
// import { registerBlock } from '@fayz-ai/plugin-linkinbio/public'
|
|
14
|
-
// registerBlock('tour-map', ({ block }) => <TourMap stops={block.stops} />)
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
/** Uniform props every registered block component receives. */
|
|
18
|
-
export interface BlockRenderProps<B = BioBlock> {
|
|
19
|
-
block: B;
|
|
20
|
-
page: BioPage;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type BlockComponent<B = BioBlock> = ComponentType<BlockRenderProps<B>>;
|
|
24
|
-
|
|
25
|
-
const registry = new Map<string, BlockComponent<never>>();
|
|
26
|
-
|
|
27
|
-
/** Register a custom block renderer for a `type` string not covered by the built-ins. */
|
|
28
|
-
export function registerBlock<B extends { type: string }>(type: B['type'], component: BlockComponent<B>): void {
|
|
29
|
-
registry.set(type, component as unknown as BlockComponent<never>);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Look up a host-registered block renderer, or undefined for built-in/unknown types. */
|
|
33
|
-
export function getBlock(type: string): BlockComponent<never> | undefined {
|
|
34
|
-
return registry.get(type);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Whether a host has registered a renderer for this type (built-ins are not in the registry). */
|
|
38
|
-
export function hasBlock(type: string): boolean {
|
|
39
|
-
return registry.has(type);
|
|
40
|
-
}
|