@fayz-ai/plugin-linkinbio 0.10.0 → 0.10.2

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.
Files changed (58) hide show
  1. package/dist/public/{data.d.ts → data/index.d.ts} +2 -2
  2. package/dist/public/data/index.d.ts.map +1 -0
  3. package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
  4. package/dist/public/data/supabase.d.ts.map +1 -0
  5. package/dist/public/index.d.ts +8 -8
  6. package/dist/public/index.d.ts.map +1 -1
  7. package/dist/public/index.js +2 -2
  8. package/dist/public/index.js.map +1 -1
  9. package/dist/public/{registry.d.ts → lib/registry.d.ts} +1 -1
  10. package/dist/public/lib/registry.d.ts.map +1 -0
  11. package/dist/public/{utils.d.ts → lib/utils.d.ts} +1 -1
  12. package/dist/public/lib/utils.d.ts.map +1 -0
  13. package/dist/public/views/BioPage.d.ts.map +1 -0
  14. package/dist/public/{BioPageRenderer.d.ts → views/BioPageRenderer.d.ts} +1 -1
  15. package/dist/public/views/BioPageRenderer.d.ts.map +1 -0
  16. package/package.json +9 -12
  17. package/dist/public/BioPage.d.ts.map +0 -1
  18. package/dist/public/BioPageRenderer.d.ts.map +0 -1
  19. package/dist/public/__tests__/manifest.test.d.ts +0 -2
  20. package/dist/public/__tests__/manifest.test.d.ts.map +0 -1
  21. package/dist/public/data.d.ts.map +0 -1
  22. package/dist/public/data.supabase.d.ts.map +0 -1
  23. package/dist/public/registry.d.ts.map +0 -1
  24. package/dist/public/utils.d.ts.map +0 -1
  25. package/src/index.ts +0 -56
  26. package/src/public/BioPage.tsx +0 -123
  27. package/src/public/BioPageRenderer.tsx +0 -251
  28. package/src/public/__tests__/manifest.test.ts +0 -39
  29. package/src/public/blocks/BioSection.tsx +0 -66
  30. package/src/public/blocks/CTASection.tsx +0 -56
  31. package/src/public/blocks/EmbedSection.tsx +0 -53
  32. package/src/public/blocks/FeaturedBlock.tsx +0 -70
  33. package/src/public/blocks/GallerySection.tsx +0 -148
  34. package/src/public/blocks/HeroSection.tsx +0 -107
  35. package/src/public/blocks/LinksBlock.tsx +0 -80
  36. package/src/public/blocks/MusicReleasesBlock.tsx +0 -83
  37. package/src/public/blocks/ProfileHeader.tsx +0 -174
  38. package/src/public/blocks/ProjectSection.tsx +0 -158
  39. package/src/public/blocks/SocialLinksSection.tsx +0 -125
  40. package/src/public/blocks/StatsSection.tsx +0 -46
  41. package/src/public/blocks/TextSection.tsx +0 -29
  42. package/src/public/blocks/TourDatesBlock.tsx +0 -83
  43. package/src/public/blocks/VenueListSection.tsx +0 -91
  44. package/src/public/blocks/VideoGridSection.tsx +0 -105
  45. package/src/public/blocks/index.ts +0 -16
  46. package/src/public/components/Carousel.tsx +0 -90
  47. package/src/public/components/MediaKit.tsx +0 -249
  48. package/src/public/components/PlatformIcon.tsx +0 -56
  49. package/src/public/components/Reveal.tsx +0 -58
  50. package/src/public/context.tsx +0 -30
  51. package/src/public/createPublicLinkInBioPlugin.ts +0 -81
  52. package/src/public/data.supabase.ts +0 -31
  53. package/src/public/data.ts +0 -29
  54. package/src/public/index.ts +0 -75
  55. package/src/public/registry.ts +0 -40
  56. package/src/public/utils.ts +0 -102
  57. package/src/types.ts +0 -337
  58. /package/dist/public/{BioPage.d.ts → views/BioPage.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import type { ComponentType } from 'react';
2
- import type { BioBlock, BioPage } from '../types';
2
+ import type { BioBlock, BioPage } from '../../types';
3
3
  /** Uniform props every registered block component receives. */
4
4
  export interface BlockRenderProps<B = BioBlock> {
5
5
  block: B;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/public/lib/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAerD,+DAA+D;AAC/D,MAAM,WAAW,gBAAgB,CAAC,CAAC,GAAG,QAAQ;IAC5C,KAAK,EAAE,CAAC,CAAC;IACT,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,GAAG,QAAQ,IAAI,aAAa,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;AAI9E,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,IAAI,CAE7G;AAED,yFAAyF;AACzF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG,SAAS,CAExE;AAED,kGAAkG;AAClG,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE9C"}
@@ -1,5 +1,5 @@
1
1
  import { Instagram } from 'lucide-react';
2
- import type { BioBranding } from '../types';
2
+ import type { BioBranding } from '../../types';
3
3
  export declare function renderMarkdown(md: string): string;
4
4
  export declare const platformColors: Record<string, string>;
5
5
  export declare const platformLabels: Record<string, string>;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/public/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyD,MAAM,cAAc,CAAC;AAChG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAajD;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkBjD,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkBjD,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,SAAS,CAkB5D,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMlD,CAAC;AAEF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAc7E"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BioPage.d.ts","sourceRoot":"","sources":["../../../src/public/views/BioPage.tsx"],"names":[],"mappings":"AAOA;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,OAAO,gCA6G9B"}
@@ -1,4 +1,4 @@
1
- import type { BioPage } from '../types';
1
+ import type { BioPage } from '../../types';
2
2
  export interface BioPageRendererProps {
3
3
  page: BioPage;
4
4
  /** Optional "made with" footer link. Omit for just the copyright line. */
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BioPageRenderer.d.ts","sourceRoot":"","sources":["../../../src/public/views/BioPageRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,aAAa,CAAC;AAqBrD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,0EAA0E;IAC1E,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C;AAKD,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,oBAAoB,+BA0NhF"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "preview"
5
5
  },
6
- "version": "0.10.0",
6
+ "version": "0.10.2",
7
7
  "description": "Fayz SDK — plugin-linkinbio: komi/linktree-style link-in-bio pages for any creator (public bio page + modular block registry)",
8
8
  "type": "module",
9
9
  "sideEffects": false,
@@ -12,40 +12,37 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "exports": {
14
14
  ".": {
15
- "source": "./src/index.ts",
16
15
  "types": "./dist/index.d.ts",
17
16
  "import": "./dist/index.js"
18
17
  },
19
18
  "./public": {
20
- "source": "./src/public/index.ts",
21
19
  "types": "./dist/public/index.d.ts",
22
20
  "import": "./dist/public/index.js"
23
21
  }
24
22
  },
25
23
  "files": [
26
- "dist",
27
- "src"
24
+ "dist"
28
25
  ],
29
26
  "peerDependencies": {
27
+ "lucide-react": ">=0.400.0 <1.0.0",
30
28
  "react": "^18.0.0 || ^19.0.0",
31
29
  "react-dom": "^18.0.0 || ^19.0.0",
32
- "react-router-dom": "^6.0.0 || ^7.0.0",
33
- "lucide-react": ">=0.400.0 <1.0.0"
30
+ "react-router-dom": "^6.0.0 || ^7.0.0"
34
31
  },
35
32
  "dependencies": {
36
33
  "react-icons": "^5.3.0",
37
- "@fayz-ai/core": "^0.9.0"
34
+ "@fayz-ai/core": "^0.11.0"
38
35
  },
39
36
  "devDependencies": {
40
37
  "@types/react": "^18.3.0",
38
+ "@types/react-dom": "^18.3.0",
39
+ "lucide-react": "^0.400.0",
41
40
  "react": "^18.3.0",
42
41
  "react-dom": "^18.3.0",
43
42
  "react-router-dom": "^6.30.0",
44
43
  "tsup": "^8.2.0",
45
44
  "typescript": "^5.5.0",
46
- "vitest": "^4.1.5",
47
- "@types/react-dom": "^18.3.0",
48
- "lucide-react": "^0.400.0"
45
+ "vitest": "^4.1.5"
49
46
  },
50
47
  "license": "MIT",
51
48
  "keywords": [
@@ -62,7 +59,7 @@
62
59
  "scripts": {
63
60
  "build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap --noEmit false",
64
61
  "dev": "tsup --watch",
65
- "typecheck": "tsc --noEmit",
62
+ "typecheck": "tsc --noEmit -p tsconfig.test.json",
66
63
  "clean": "rm -rf dist",
67
64
  "test": "vitest run"
68
65
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"BioPage.d.ts","sourceRoot":"","sources":["../../src/public/BioPage.tsx"],"names":[],"mappings":"AAOA;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,OAAO,gCA6G9B"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"BioPageRenderer.d.ts","sourceRoot":"","sources":["../../src/public/BioPageRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,UAAU,CAAC;AAqBlD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,0EAA0E;IAC1E,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C;AAKD,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,oBAAoB,+BA0NhF"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=manifest.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"manifest.test.d.ts","sourceRoot":"","sources":["../../../src/public/__tests__/manifest.test.ts"],"names":[],"mappings":""}
@@ -1 +0,0 @@
1
- {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/public/data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAWxC,MAAM,WAAW,mBAAmB;IAClC,yFAAyF;IACzF,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,kBAAuB,GAAG,mBAAmB,CAO/F"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"data.supabase.d.ts","sourceRoot":"","sources":["../../src/public/data.supabase.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAElD,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,sBAA2B,GAAG,mBAAmB,CAgBvG"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/public/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAelD,+DAA+D;AAC/D,MAAM,WAAW,gBAAgB,CAAC,CAAC,GAAG,QAAQ;IAC5C,KAAK,EAAE,CAAC,CAAC;IACT,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,GAAG,QAAQ,IAAI,aAAa,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;AAI9E,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,IAAI,CAE7G;AAED,yFAAyF;AACzF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG,SAAS,CAExE;AAED,kGAAkG;AAClG,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE9C"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/public/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyD,MAAM,cAAc,CAAC;AAChG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5C,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAajD;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkBjD,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkBjD,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,SAAS,CAkB5D,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMlD,CAAC;AAEF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAc7E"}
package/src/index.ts DELETED
@@ -1,56 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // @fayz-ai/plugin-linkinbio — admin entry (v1 stub).
3
- //
4
- // The public bio page ships today via the `/public` entry
5
- // (createPublicLinkInBioPlugin). The in-app editor / settings surface is a v2
6
- // follow-up: this factory returns a minimal manifest with a placeholder view so
7
- // the plugin can be listed in an admin shell without shipping the editor yet.
8
- //
9
- // For the customer-facing page, import from '@fayz-ai/plugin-linkinbio/public'.
10
- // ---------------------------------------------------------------------------
11
- import { createElement } from 'react';
12
- import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core';
13
-
14
- export interface LinkInBioPluginOptions {
15
- basePath?: string;
16
- scope?: PluginScope;
17
- verticalId?: VerticalId;
18
- defaultEnabled?: boolean;
19
- }
20
-
21
- function EditorPlaceholder() {
22
- return createElement(
23
- 'div',
24
- { className: 'p-8 text-sm text-muted-foreground' },
25
- 'O editor de Link in Bio chega no v2. A página pública já está disponível em /p/:slug.',
26
- );
27
- }
28
-
29
- export function createLinkInBioPlugin(options: LinkInBioPluginOptions = {}): PluginManifest {
30
- const basePath = options.basePath ?? '/link-in-bio';
31
- return {
32
- id: 'linkinbio',
33
- name: 'Link in Bio',
34
- icon: 'Link',
35
- version: '0.1.0',
36
- scope: options.scope ?? 'universal',
37
- verticalId: options.verticalId,
38
- scaffolds: ['saas', 'website'],
39
- defaultEnabled: options.defaultEnabled ?? false,
40
- dependencies: [],
41
- navigation: [{ section: 'main', position: 10, label: 'Link in Bio', route: basePath, icon: 'Link' }],
42
- routes: [{ path: basePath, component: EditorPlaceholder, guard: 'authenticated' }],
43
- widgets: [],
44
- };
45
- }
46
-
47
- export type {
48
- BioPage,
49
- BioBlock,
50
- BioBranding,
51
- BioIdentity,
52
- BioSEO,
53
- BioSocialLink,
54
- BioMediaItem,
55
- BioFloatingCTA,
56
- } from './types';
@@ -1,123 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- import { useParams } from 'react-router-dom';
3
- import type { BioPage as BioPageType } from '../types';
4
- import { getBrandingVars } from './utils';
5
- import BioPageRenderer from './BioPageRenderer';
6
- import { useLinkInBio } from './context';
7
-
8
- /**
9
- * Public bio-page route component. Reads `:slug` from the URL, resolves the page
10
- * from the data provider, applies SEO + dynamic Google Fonts + branding CSS vars,
11
- * and renders the block list. Mounted by the plugin manifest as a guard:'public'
12
- * route (default path `/p/:slug`).
13
- */
14
- export default function BioPage() {
15
- const { slug } = useParams<{ slug: string }>();
16
- const { provider, poweredBy, titleSuffix } = useLinkInBio();
17
- const [page, setPage] = useState<BioPageType | null>(null);
18
- const [resolved, setResolved] = useState(false);
19
-
20
- useEffect(() => {
21
- if (!slug) {
22
- setResolved(true);
23
- return;
24
- }
25
- let cancelled = false;
26
- setResolved(false);
27
- provider
28
- .getBySlug(slug)
29
- .then((pk) => {
30
- if (cancelled) return;
31
- setPage(pk);
32
- setResolved(true);
33
- })
34
- .catch(() => {
35
- if (cancelled) return;
36
- setResolved(true);
37
- });
38
- return () => {
39
- cancelled = true;
40
- };
41
- }, [slug, provider]);
42
-
43
- // Set document title & load custom fonts
44
- useEffect(() => {
45
- if (!page) return;
46
-
47
- // SEO
48
- document.title = page.seo.title ?? `${page.identity.name} | ${titleSuffix}`;
49
- const metaDesc = document.querySelector('meta[name="description"]');
50
- if (metaDesc && page.seo.description) {
51
- metaDesc.setAttribute('content', page.seo.description);
52
- }
53
-
54
- // Dynamic Google Fonts loading
55
- const fontsToLoad: string[] = [];
56
- const { fontHeading, fontBody } = page.branding;
57
- if (fontHeading && !fontHeading.includes('Space Grotesk')) {
58
- const family = fontHeading.replace(/['"]/g, '').split(',')[0].trim();
59
- fontsToLoad.push(family);
60
- }
61
- if (fontBody && !fontBody.includes('DM Sans')) {
62
- const family = fontBody.replace(/['"]/g, '').split(',')[0].trim();
63
- fontsToLoad.push(family);
64
- }
65
-
66
- const linkElements: HTMLLinkElement[] = [];
67
- if (fontsToLoad.length > 0) {
68
- const families = fontsToLoad.map((f) => `family=${f.replace(/\s/g, '+')}:wght@400;600;700`).join('&');
69
- const link = document.createElement('link');
70
- link.rel = 'stylesheet';
71
- link.href = `https://fonts.googleapis.com/css2?${families}&display=swap`;
72
- document.head.appendChild(link);
73
- linkElements.push(link);
74
- }
75
-
76
- return () => {
77
- linkElements.forEach((el) => el.remove());
78
- };
79
- }, [page, titleSuffix]);
80
-
81
- if (!resolved) {
82
- return (
83
- <div className="dark fixed inset-0 flex items-center justify-center bg-background text-foreground">
84
- <div className="w-2 h-2 rounded-full bg-foreground/40 animate-pulse" aria-label="carregando" />
85
- </div>
86
- );
87
- }
88
-
89
- if (!page) {
90
- return (
91
- <div className="dark fixed inset-0 flex items-center justify-center bg-background text-foreground">
92
- <div className="text-center px-6">
93
- <h1 className="text-large font-bold mb-4" style={{ fontFamily: 'var(--font-display)' }}>
94
- 404
95
- </h1>
96
- <p className="text-foreground-secondary mb-6">Página não encontrada.</p>
97
- <a
98
- href="/"
99
- className="text-sm underline underline-offset-4 text-foreground-secondary hover:text-foreground transition-colors"
100
- >
101
- voltar ao início
102
- </a>
103
- </div>
104
- </div>
105
- );
106
- }
107
-
108
- const brandingVars = getBrandingVars(page.branding);
109
-
110
- return (
111
- <div
112
- className="fixed inset-0 overflow-y-auto overflow-x-hidden"
113
- style={{
114
- ...brandingVars,
115
- backgroundColor: 'var(--pk-bg)',
116
- color: 'var(--pk-text)',
117
- fontFamily: 'var(--pk-font-body)',
118
- } as React.CSSProperties}
119
- >
120
- <BioPageRenderer page={page} poweredBy={poweredBy} />
121
- </div>
122
- );
123
- }
@@ -1,251 +0,0 @@
1
- import { useState, useEffect, useRef } from 'react';
2
- import { ChevronUp, MessageCircle } from 'lucide-react';
3
- import type { BioPage, BioBlock } from '../types';
4
- import { getBlock } from './registry';
5
- import { Reveal } from './components/Reveal';
6
- import { MediaKit } from './components/MediaKit';
7
- import HeroSection from './blocks/HeroSection';
8
- import BioSection from './blocks/BioSection';
9
- import GallerySection from './blocks/GallerySection';
10
- import VideoGridSection from './blocks/VideoGridSection';
11
- import SocialLinksSection from './blocks/SocialLinksSection';
12
- import EmbedSection from './blocks/EmbedSection';
13
- import ProjectSection from './blocks/ProjectSection';
14
- import StatsSection from './blocks/StatsSection';
15
- import VenueListSection from './blocks/VenueListSection';
16
- import CTASection from './blocks/CTASection';
17
- import TextSection from './blocks/TextSection';
18
- import ProfileHeader from './blocks/ProfileHeader';
19
- import LinksBlock from './blocks/LinksBlock';
20
- import MusicReleasesBlock from './blocks/MusicReleasesBlock';
21
- import TourDatesBlock from './blocks/TourDatesBlock';
22
- import FeaturedBlock from './blocks/FeaturedBlock';
23
-
24
- export interface BioPageRendererProps {
25
- page: BioPage;
26
- /** Optional "made with" footer link. Omit for just the copyright line. */
27
- poweredBy?: { label: string; url: string };
28
- }
29
-
30
- // Blocks that get alternating backgrounds in scroll mode (hero/header/cta excluded).
31
- const ALT_BG_TYPES = new Set(['bio', 'gallery', 'video-grid', 'project', 'embed', 'stats', 'venue-list', 'text', 'social-links']);
32
-
33
- export default function BioPageRenderer({ page, poweredBy }: BioPageRendererProps) {
34
- const { identity, branding, sections, floatingCta } = page;
35
- const isHub = page.layout === 'hub';
36
- const scrollRef = useRef<HTMLDivElement | null>(null);
37
- const [scrollProgress, setScrollProgress] = useState(0);
38
- const [showFloatingCta, setShowFloatingCta] = useState(false);
39
-
40
- // Scroll progress & floating CTA visibility
41
- useEffect(() => {
42
- const container = scrollRef.current?.closest('.fixed.inset-0') as HTMLElement | null;
43
- if (!container) return;
44
-
45
- const onScroll = () => {
46
- const { scrollTop, scrollHeight, clientHeight } = container;
47
- const progress = scrollHeight > clientHeight ? scrollTop / (scrollHeight - clientHeight) : 0;
48
- setScrollProgress(progress);
49
- setShowFloatingCta(scrollTop > clientHeight * 0.5);
50
- };
51
-
52
- container.addEventListener('scroll', onScroll, { passive: true });
53
- return () => container.removeEventListener('scroll', onScroll);
54
- }, []);
55
-
56
- // Assign alternating bg to non-hero/cta sections (scroll mode only)
57
- let altIndex = 0;
58
- const sectionBgs: Map<string, string | undefined> = new Map();
59
- if (!isHub) {
60
- for (const s of sections) {
61
- if (s.visible === false) continue;
62
- if (ALT_BG_TYPES.has(s.type)) {
63
- const bgs = [undefined, 'var(--pk-alt-bg)', 'var(--pk-alt-bg2)'];
64
- sectionBgs.set(s.id, bgs[altIndex % bgs.length]);
65
- altIndex++;
66
- }
67
- }
68
- }
69
-
70
- function renderInner(section: BioBlock) {
71
- // Host-registered niche blocks win over the built-in switch.
72
- const Custom = getBlock(section.type);
73
- if (Custom) return <Custom block={section as never} page={page} />;
74
- switch (section.type) {
75
- case 'hero':
76
- return <HeroSection section={section} identity={identity} branding={branding} />;
77
- case 'profile-header':
78
- return <ProfileHeader section={section} page={page} />;
79
- case 'bio':
80
- return <BioSection section={section} />;
81
- case 'gallery':
82
- return <GallerySection section={section} />;
83
- case 'video-grid':
84
- return <VideoGridSection section={section} />;
85
- case 'social-links':
86
- return <SocialLinksSection section={section} />;
87
- case 'embed':
88
- return <EmbedSection section={section} />;
89
- case 'project':
90
- return <ProjectSection section={section} />;
91
- case 'stats':
92
- return <StatsSection section={section} />;
93
- case 'venue-list':
94
- return <VenueListSection section={section} />;
95
- case 'cta':
96
- return <CTASection section={section} />;
97
- case 'text':
98
- return <TextSection section={section} />;
99
- case 'links':
100
- return <LinksBlock section={section} />;
101
- case 'music-releases':
102
- return <MusicReleasesBlock section={section} />;
103
- case 'tour-dates':
104
- return <TourDatesBlock section={section} />;
105
- case 'featured':
106
- return <FeaturedBlock section={section} />;
107
- default:
108
- return null;
109
- }
110
- }
111
-
112
- function renderSection(section: BioBlock, index: number) {
113
- if (section.visible === false) return null;
114
- const inner = renderInner(section);
115
- if (inner === null) return null;
116
-
117
- // ── Hub layout: everything sits inside the mobile-width column (padding is
118
- // provided by the wrapper below). The header renders without a reveal
119
- // (it's above the fold); other blocks fade in on scroll.
120
- if (isHub) {
121
- // Header breaks out of the column padding to sit full-bleed on mobile;
122
- // on desktop it becomes a contained rounded card within the column.
123
- if (section.type === 'profile-header')
124
- return (
125
- <div key={section.id} className="-mx-3 -mt-3 sm:mx-0 sm:mt-0">
126
- {inner}
127
- </div>
128
- );
129
- return <Reveal key={section.id}>{inner}</Reveal>;
130
- }
131
-
132
- // ── Scroll (press-kit) layout: full-width sections, alternating bg ──
133
- if (section.type === 'hero') return <div key={section.id}>{inner}</div>;
134
- const bg = sectionBgs.get(section.id);
135
- return (
136
- <div key={section.id} style={bg ? { backgroundColor: bg } : undefined}>
137
- <Reveal delay={index === 0 ? 0 : 40}>{inner}</Reveal>
138
- </div>
139
- );
140
- }
141
-
142
- return (
143
- <div ref={scrollRef} className="min-h-full">
144
- {/* Scroll progress bar */}
145
- <div className="fixed top-0 left-0 right-0 z-50 h-[3px]" style={{ backgroundColor: 'transparent' }}>
146
- <div
147
- className="h-full transition-[width] duration-100 ease-out"
148
- style={{
149
- width: `${scrollProgress * 100}%`,
150
- background: `linear-gradient(90deg, var(--pk-primary), var(--pk-accent))`,
151
- }}
152
- />
153
- </div>
154
-
155
- {/* Sections + footer. Hub = a centered mobile-width column (komi/linktree
156
- style) that stays phone-width even on desktop, with breathing room so
157
- the hero reads as a rounded floating card. */}
158
- <div className={isHub ? 'max-w-[480px] mx-auto px-3 sm:px-4 pt-3 pb-6' : ''}>
159
- {sections.map(renderSection)}
160
-
161
- {/* Footer */}
162
- <footer className="pt-10 pb-24 px-6 text-center" style={{ backgroundColor: 'var(--pk-bg)' }}>
163
- <p className="text-xs opacity-40" style={{ color: 'var(--pk-muted)' }}>
164
- &copy; {new Date().getFullYear()} {identity.name}
165
- </p>
166
- {poweredBy && (
167
- <a
168
- href={poweredBy.url}
169
- className="text-xs opacity-25 mt-1 hover:opacity-40 transition-opacity"
170
- style={{ color: 'var(--pk-muted)' }}
171
- >
172
- {poweredBy.label}
173
- </a>
174
- )}
175
- </footer>
176
- </div>
177
-
178
- {/* Floating actions — hub: circular stack (scroll-to-top + primary action,
179
- presskitpro style); scroll: the classic wide pill. */}
180
- {isHub ? (
181
- <div className="fixed z-50 right-4 bottom-6 flex flex-col items-center gap-3">
182
- <button
183
- type="button"
184
- aria-label="Voltar ao topo"
185
- onClick={() =>
186
- (scrollRef.current?.closest('.fixed.inset-0') as HTMLElement | null)?.scrollTo({
187
- top: 0,
188
- behavior: 'smooth',
189
- })
190
- }
191
- className="flex items-center justify-center w-11 h-11 rounded-full transition-all duration-300"
192
- style={{
193
- opacity: showFloatingCta ? 1 : 0,
194
- transform: showFloatingCta ? 'none' : 'translateY(12px)',
195
- pointerEvents: showFloatingCta ? 'auto' : 'none',
196
- color: 'var(--pk-primary)',
197
- border: '1.5px solid var(--pk-primary)',
198
- backgroundColor: 'color-mix(in srgb, var(--pk-bg) 70%, transparent)',
199
- backdropFilter: 'blur(6px)',
200
- }}
201
- >
202
- <ChevronUp size={20} />
203
- </button>
204
- <MediaKit page={page} variant="fab" />
205
- {floatingCta && (
206
- <a
207
- href={floatingCta.url}
208
- target="_blank"
209
- rel="noopener noreferrer"
210
- aria-label={floatingCta.label}
211
- className="flex items-center justify-center w-14 h-14 rounded-full transition-transform hover:scale-105"
212
- style={{
213
- backgroundColor: 'var(--pk-primary)',
214
- color: 'var(--pk-bg)',
215
- boxShadow: '0 8px 28px rgba(0,0,0,0.45)',
216
- }}
217
- >
218
- <MessageCircle size={24} />
219
- </a>
220
- )}
221
- </div>
222
- ) : (
223
- floatingCta && (
224
- <a
225
- href={floatingCta.url}
226
- target="_blank"
227
- rel="noopener noreferrer"
228
- className="fixed z-50 transition-all duration-500 ease-out"
229
- style={{
230
- bottom: showFloatingCta ? '2rem' : '-4rem',
231
- left: '50%',
232
- transform: 'translateX(-50%)',
233
- opacity: showFloatingCta ? 1 : 0,
234
- backgroundColor: 'var(--pk-primary)',
235
- color: 'var(--pk-bg)',
236
- fontFamily: 'var(--pk-font-heading)',
237
- borderRadius: '9999px',
238
- padding: '1rem 2.5rem',
239
- fontSize: '1rem',
240
- fontWeight: 700,
241
- letterSpacing: '0.1em',
242
- boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
243
- }}
244
- >
245
- {floatingCta.label}
246
- </a>
247
- )
248
- )}
249
- </div>
250
- );
251
- }
@@ -1,39 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { assertPluginManifestContract } from '@fayz-ai/core/testing';
3
- import { createPublicLinkInBioPlugin } from '../createPublicLinkInBioPlugin';
4
- import type { BioPage } from '../../types';
5
-
6
- const demo: BioPage = {
7
- identity: { name: 'Demo', slug: 'demo', genres: [] },
8
- branding: {
9
- primaryColor: '#F97316',
10
- secondaryColor: '#FFB347',
11
- accentColor: '#FFCF40',
12
- backgroundColor: '#0A0A0A',
13
- textColor: '#FFFFFF',
14
- mutedTextColor: '#9CA3AF',
15
- },
16
- seo: {},
17
- socialLinks: [],
18
- sections: [{ id: 'hero', type: 'hero', backgroundImageUrl: '', showGenres: true }],
19
- };
20
-
21
- describe('createPublicLinkInBioPlugin', () => {
22
- it('returns a manifest that passes the plugin contract', () => {
23
- const { manifest } = createPublicLinkInBioPlugin({ seed: { demo } });
24
- expect(() => assertPluginManifestContract(manifest)).not.toThrow();
25
- });
26
-
27
- it('exposes a public route at basePath/:slug', () => {
28
- const { manifest } = createPublicLinkInBioPlugin({ basePath: '/p' });
29
- const route = manifest.routes.find((r) => r.guard === 'public');
30
- expect(route?.path).toBe('/p/:slug');
31
- expect(route?.component).toBeTruthy();
32
- });
33
-
34
- it('mock provider resolves a seeded page and returns null for unknown slugs', async () => {
35
- const { dataProvider } = createPublicLinkInBioPlugin({ useSupabase: false, seed: { demo } });
36
- expect(await dataProvider.getBySlug('demo')).toEqual(demo);
37
- expect(await dataProvider.getBySlug('nope')).toBeNull();
38
- });
39
- });