@fayz-ai/plugin-linkinbio 0.9.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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/index.cjs +33 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/public/BioPage.d.ts +8 -0
- package/dist/public/BioPage.d.ts.map +1 -0
- package/dist/public/BioPageRenderer.d.ts +11 -0
- package/dist/public/BioPageRenderer.d.ts.map +1 -0
- package/dist/public/__tests__/manifest.test.d.ts +2 -0
- package/dist/public/__tests__/manifest.test.d.ts.map +1 -0
- package/dist/public/blocks/BioSection.d.ts +7 -0
- package/dist/public/blocks/BioSection.d.ts.map +1 -0
- package/dist/public/blocks/CTASection.d.ts +7 -0
- package/dist/public/blocks/CTASection.d.ts.map +1 -0
- package/dist/public/blocks/EmbedSection.d.ts +7 -0
- package/dist/public/blocks/EmbedSection.d.ts.map +1 -0
- package/dist/public/blocks/FeaturedBlock.d.ts +7 -0
- package/dist/public/blocks/FeaturedBlock.d.ts.map +1 -0
- package/dist/public/blocks/GallerySection.d.ts +7 -0
- package/dist/public/blocks/GallerySection.d.ts.map +1 -0
- package/dist/public/blocks/HeroSection.d.ts +9 -0
- package/dist/public/blocks/HeroSection.d.ts.map +1 -0
- package/dist/public/blocks/LinksBlock.d.ts +7 -0
- package/dist/public/blocks/LinksBlock.d.ts.map +1 -0
- package/dist/public/blocks/MusicReleasesBlock.d.ts +7 -0
- package/dist/public/blocks/MusicReleasesBlock.d.ts.map +1 -0
- package/dist/public/blocks/ProfileHeader.d.ts +8 -0
- package/dist/public/blocks/ProfileHeader.d.ts.map +1 -0
- package/dist/public/blocks/ProjectSection.d.ts +7 -0
- package/dist/public/blocks/ProjectSection.d.ts.map +1 -0
- package/dist/public/blocks/SocialLinksSection.d.ts +7 -0
- package/dist/public/blocks/SocialLinksSection.d.ts.map +1 -0
- package/dist/public/blocks/StatsSection.d.ts +7 -0
- package/dist/public/blocks/StatsSection.d.ts.map +1 -0
- package/dist/public/blocks/TextSection.d.ts +7 -0
- package/dist/public/blocks/TextSection.d.ts.map +1 -0
- package/dist/public/blocks/TourDatesBlock.d.ts +7 -0
- package/dist/public/blocks/TourDatesBlock.d.ts.map +1 -0
- package/dist/public/blocks/VenueListSection.d.ts +7 -0
- package/dist/public/blocks/VenueListSection.d.ts.map +1 -0
- package/dist/public/blocks/VideoGridSection.d.ts +7 -0
- package/dist/public/blocks/VideoGridSection.d.ts.map +1 -0
- package/dist/public/blocks/index.d.ts +17 -0
- package/dist/public/blocks/index.d.ts.map +1 -0
- package/dist/public/components/Carousel.d.ts +11 -0
- package/dist/public/components/Carousel.d.ts.map +1 -0
- package/dist/public/components/PlatformIcon.d.ts +9 -0
- package/dist/public/components/PlatformIcon.d.ts.map +1 -0
- package/dist/public/components/Reveal.d.ts +13 -0
- package/dist/public/components/Reveal.d.ts.map +1 -0
- package/dist/public/context.d.ts +18 -0
- package/dist/public/context.d.ts.map +1 -0
- package/dist/public/createPublicLinkInBioPlugin.d.ts +38 -0
- package/dist/public/createPublicLinkInBioPlugin.d.ts.map +1 -0
- package/dist/public/data.d.ts +11 -0
- package/dist/public/data.d.ts.map +1 -0
- package/dist/public/data.supabase.d.ts +11 -0
- package/dist/public/data.supabase.d.ts.map +1 -0
- package/dist/public/index.cjs +1750 -0
- package/dist/public/index.cjs.map +1 -0
- package/dist/public/index.d.ts +18 -0
- package/dist/public/index.d.ts.map +1 -0
- package/dist/public/index.js +1731 -0
- package/dist/public/index.js.map +1 -0
- package/dist/public/registry.d.ts +17 -0
- package/dist/public/registry.d.ts.map +1 -0
- package/dist/public/utils.d.ts +9 -0
- package/dist/public/utils.d.ts.map +1 -0
- package/dist/types.d.ts +251 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +68 -0
- package/src/index.ts +56 -0
- package/src/public/BioPage.tsx +123 -0
- package/src/public/BioPageRenderer.tsx +249 -0
- package/src/public/__tests__/manifest.test.ts +39 -0
- package/src/public/blocks/BioSection.tsx +66 -0
- package/src/public/blocks/CTASection.tsx +56 -0
- package/src/public/blocks/EmbedSection.tsx +53 -0
- package/src/public/blocks/FeaturedBlock.tsx +70 -0
- package/src/public/blocks/GallerySection.tsx +148 -0
- package/src/public/blocks/HeroSection.tsx +107 -0
- package/src/public/blocks/LinksBlock.tsx +80 -0
- package/src/public/blocks/MusicReleasesBlock.tsx +83 -0
- package/src/public/blocks/ProfileHeader.tsx +176 -0
- package/src/public/blocks/ProjectSection.tsx +158 -0
- package/src/public/blocks/SocialLinksSection.tsx +125 -0
- package/src/public/blocks/StatsSection.tsx +46 -0
- package/src/public/blocks/TextSection.tsx +29 -0
- package/src/public/blocks/TourDatesBlock.tsx +83 -0
- package/src/public/blocks/VenueListSection.tsx +91 -0
- package/src/public/blocks/VideoGridSection.tsx +105 -0
- package/src/public/blocks/index.ts +16 -0
- package/src/public/components/Carousel.tsx +90 -0
- package/src/public/components/PlatformIcon.tsx +56 -0
- package/src/public/components/Reveal.tsx +58 -0
- package/src/public/context.tsx +30 -0
- package/src/public/createPublicLinkInBioPlugin.ts +81 -0
- package/src/public/data.supabase.ts +31 -0
- package/src/public/data.ts +29 -0
- package/src/public/index.ts +75 -0
- package/src/public/registry.ts +40 -0
- package/src/public/utils.ts +102 -0
- package/src/types.ts +328 -0
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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';
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Instagram, Music, Youtube, Globe, Mail, Phone, Facebook, Twitter } from 'lucide-react';
|
|
2
|
+
import type { BioBranding } from '../types';
|
|
3
|
+
|
|
4
|
+
// Lightweight markdown → HTML renderer (bold, italic, links, paragraphs)
|
|
5
|
+
export function renderMarkdown(md: string): string {
|
|
6
|
+
return md
|
|
7
|
+
.split(/\n\n+/)
|
|
8
|
+
.map((block) => {
|
|
9
|
+
const html = block
|
|
10
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
|
11
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
12
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
13
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
|
14
|
+
.replace(/\n/g, '<br />');
|
|
15
|
+
return `<p>${html}</p>`;
|
|
16
|
+
})
|
|
17
|
+
.join('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const platformColors: Record<string, string> = {
|
|
21
|
+
instagram: '#E4405F',
|
|
22
|
+
spotify: '#1DB954',
|
|
23
|
+
soundcloud: '#FF5500',
|
|
24
|
+
youtube: '#FF0000',
|
|
25
|
+
tiktok: '#000000',
|
|
26
|
+
whatsapp: '#25D366',
|
|
27
|
+
email: '#EA4335',
|
|
28
|
+
twitter: '#1DA1F2',
|
|
29
|
+
facebook: '#1877F2',
|
|
30
|
+
bandcamp: '#629AA9',
|
|
31
|
+
mixcloud: '#5000FF',
|
|
32
|
+
beatport: '#94D500',
|
|
33
|
+
'apple-music': '#FC3C44',
|
|
34
|
+
deezer: '#FEAA2D',
|
|
35
|
+
telegram: '#0088CC',
|
|
36
|
+
website: '#888888',
|
|
37
|
+
custom: '#888888',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const platformLabels: Record<string, string> = {
|
|
41
|
+
instagram: 'Instagram',
|
|
42
|
+
spotify: 'Spotify',
|
|
43
|
+
soundcloud: 'SoundCloud',
|
|
44
|
+
youtube: 'YouTube',
|
|
45
|
+
tiktok: 'TikTok',
|
|
46
|
+
whatsapp: 'WhatsApp',
|
|
47
|
+
email: 'Email',
|
|
48
|
+
twitter: 'Twitter / X',
|
|
49
|
+
facebook: 'Facebook',
|
|
50
|
+
bandcamp: 'Bandcamp',
|
|
51
|
+
mixcloud: 'Mixcloud',
|
|
52
|
+
beatport: 'Beatport',
|
|
53
|
+
'apple-music': 'Apple Music',
|
|
54
|
+
deezer: 'Deezer',
|
|
55
|
+
telegram: 'Telegram',
|
|
56
|
+
website: 'Website',
|
|
57
|
+
custom: 'Link',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const platformIconMap: Record<string, typeof Instagram> = {
|
|
61
|
+
instagram: Instagram,
|
|
62
|
+
spotify: Music,
|
|
63
|
+
soundcloud: Music,
|
|
64
|
+
youtube: Youtube,
|
|
65
|
+
tiktok: Music,
|
|
66
|
+
whatsapp: Phone,
|
|
67
|
+
email: Mail,
|
|
68
|
+
twitter: Twitter,
|
|
69
|
+
facebook: Facebook,
|
|
70
|
+
bandcamp: Music,
|
|
71
|
+
mixcloud: Music,
|
|
72
|
+
beatport: Music,
|
|
73
|
+
'apple-music': Music,
|
|
74
|
+
deezer: Music,
|
|
75
|
+
telegram: Globe,
|
|
76
|
+
website: Globe,
|
|
77
|
+
custom: Globe,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const borderRadiusMap: Record<string, string> = {
|
|
81
|
+
none: '0px',
|
|
82
|
+
sm: '4px',
|
|
83
|
+
md: '8px',
|
|
84
|
+
lg: '16px',
|
|
85
|
+
full: '9999px',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export function getBrandingVars(branding: BioBranding): Record<string, string> {
|
|
89
|
+
return {
|
|
90
|
+
'--pk-primary': branding.primaryColor,
|
|
91
|
+
'--pk-secondary': branding.secondaryColor,
|
|
92
|
+
'--pk-accent': branding.accentColor,
|
|
93
|
+
'--pk-bg': branding.backgroundColor,
|
|
94
|
+
'--pk-text': branding.textColor,
|
|
95
|
+
'--pk-muted': branding.mutedTextColor,
|
|
96
|
+
'--pk-radius': borderRadiusMap[branding.borderRadius ?? 'md'],
|
|
97
|
+
'--pk-font-heading': branding.fontHeading ?? "'Space Grotesk', sans-serif",
|
|
98
|
+
'--pk-font-body': branding.fontBody ?? "'DM Sans', sans-serif",
|
|
99
|
+
'--pk-alt-bg': branding.sectionAltBg ?? branding.backgroundColor,
|
|
100
|
+
'--pk-alt-bg2': branding.sectionAltBg2 ?? branding.backgroundColor,
|
|
101
|
+
};
|
|
102
|
+
}
|