@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.
- 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 +9 -12
- 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/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,66 +0,0 @@
|
|
|
1
|
-
import type { BioSection as BioSectionType } from '../../types';
|
|
2
|
-
import { renderMarkdown } from '../utils';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
section: BioSectionType;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export default function BioSection({ section }: Props) {
|
|
9
|
-
const hasImage = section.imageUrl && section.imagePosition !== 'none';
|
|
10
|
-
const isTop = section.imagePosition === 'top';
|
|
11
|
-
const isSide = section.imagePosition === 'left' || section.imagePosition === 'right';
|
|
12
|
-
const isRight = section.imagePosition === 'right';
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<section className="py-16 md:py-24 px-6" id={section.id}>
|
|
16
|
-
<div className="max-w-4xl mx-auto">
|
|
17
|
-
{section.title && (
|
|
18
|
-
<h2
|
|
19
|
-
className="text-3xl md:text-4xl font-bold mb-8"
|
|
20
|
-
style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
|
|
21
|
-
>
|
|
22
|
-
{section.title}
|
|
23
|
-
</h2>
|
|
24
|
-
)}
|
|
25
|
-
|
|
26
|
-
<div className={isSide && hasImage ? `grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 items-start` : ''}>
|
|
27
|
-
{hasImage && isTop && (
|
|
28
|
-
<img
|
|
29
|
-
src={section.imageUrl}
|
|
30
|
-
alt=""
|
|
31
|
-
className="w-full max-h-96 object-cover mb-8"
|
|
32
|
-
style={{ borderRadius: 'var(--pk-radius)' }}
|
|
33
|
-
loading="lazy"
|
|
34
|
-
/>
|
|
35
|
-
)}
|
|
36
|
-
|
|
37
|
-
{hasImage && isSide && !isRight && (
|
|
38
|
-
<img
|
|
39
|
-
src={section.imageUrl}
|
|
40
|
-
alt=""
|
|
41
|
-
className="w-full aspect-[3/4] object-cover"
|
|
42
|
-
style={{ borderRadius: 'var(--pk-radius)' }}
|
|
43
|
-
loading="lazy"
|
|
44
|
-
/>
|
|
45
|
-
)}
|
|
46
|
-
|
|
47
|
-
<div
|
|
48
|
-
className="prose prose-invert max-w-none text-base md:text-lg leading-relaxed [&_p]:mb-4 [&_strong]:font-semibold [&_a]:underline [&_a]:decoration-[var(--pk-primary)]"
|
|
49
|
-
style={{ color: 'var(--pk-muted)' }}
|
|
50
|
-
dangerouslySetInnerHTML={{ __html: renderMarkdown(section.content) }}
|
|
51
|
-
/>
|
|
52
|
-
|
|
53
|
-
{hasImage && isSide && isRight && (
|
|
54
|
-
<img
|
|
55
|
-
src={section.imageUrl}
|
|
56
|
-
alt=""
|
|
57
|
-
className="w-full aspect-[3/4] object-cover"
|
|
58
|
-
style={{ borderRadius: 'var(--pk-radius)' }}
|
|
59
|
-
loading="lazy"
|
|
60
|
-
/>
|
|
61
|
-
)}
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
</section>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { CTASection as CTASectionType } from '../../types';
|
|
2
|
-
import { renderMarkdown } from '../utils';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
section: CTASectionType;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export default function CTASection({ section }: Props) {
|
|
9
|
-
return (
|
|
10
|
-
<section className="py-24 md:py-36 px-6 text-center" id={section.id}>
|
|
11
|
-
<div className="max-w-2xl mx-auto">
|
|
12
|
-
{section.title && (
|
|
13
|
-
<h2
|
|
14
|
-
className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6"
|
|
15
|
-
style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
|
|
16
|
-
>
|
|
17
|
-
{section.title}
|
|
18
|
-
</h2>
|
|
19
|
-
)}
|
|
20
|
-
|
|
21
|
-
{section.content && (
|
|
22
|
-
<div
|
|
23
|
-
className="text-base md:text-lg mb-10 [&_p]:mb-2"
|
|
24
|
-
style={{ color: 'var(--pk-muted)' }}
|
|
25
|
-
dangerouslySetInnerHTML={{ __html: renderMarkdown(section.content) }}
|
|
26
|
-
/>
|
|
27
|
-
)}
|
|
28
|
-
|
|
29
|
-
<div className="flex flex-col sm:flex-row items-center justify-center gap-5">
|
|
30
|
-
{section.ctaButtons.map((btn) => {
|
|
31
|
-
const isPrimary = btn.variant === 'primary';
|
|
32
|
-
const isOutline = btn.variant === 'outline';
|
|
33
|
-
return (
|
|
34
|
-
<a
|
|
35
|
-
key={btn.url}
|
|
36
|
-
href={btn.url}
|
|
37
|
-
target="_blank"
|
|
38
|
-
rel="noopener noreferrer"
|
|
39
|
-
className="inline-flex items-center justify-center px-10 py-4 text-base font-bold tracking-[0.1em] uppercase transition-all duration-300 hover:scale-105 min-w-[200px]"
|
|
40
|
-
style={{
|
|
41
|
-
backgroundColor: isPrimary ? 'var(--pk-primary)' : isOutline ? 'transparent' : 'var(--pk-secondary)',
|
|
42
|
-
color: isPrimary ? 'var(--pk-bg)' : 'var(--pk-text)',
|
|
43
|
-
border: isOutline ? '2px solid rgba(255,255,255,0.2)' : 'none',
|
|
44
|
-
borderRadius: '9999px',
|
|
45
|
-
boxShadow: isPrimary ? '0 8px 32px rgba(0,0,0,0.3)' : 'none',
|
|
46
|
-
}}
|
|
47
|
-
>
|
|
48
|
-
{btn.label}
|
|
49
|
-
</a>
|
|
50
|
-
);
|
|
51
|
-
})}
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</section>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { EmbedSection as EmbedSectionType } from '../../types';
|
|
2
|
-
|
|
3
|
-
interface Props {
|
|
4
|
-
section: EmbedSectionType;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export default function EmbedSection({ section }: Props) {
|
|
8
|
-
const isVideo = section.aspectRatio === '16:9' || section.embedType === 'youtube';
|
|
9
|
-
const height = section.height ?? (isVideo ? undefined : 352);
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<section className="py-16 md:py-24 px-6" id={section.id}>
|
|
13
|
-
<div className="max-w-4xl mx-auto">
|
|
14
|
-
{section.title && (
|
|
15
|
-
<h2
|
|
16
|
-
className="text-3xl md:text-4xl font-bold mb-8"
|
|
17
|
-
style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
|
|
18
|
-
>
|
|
19
|
-
{section.title}
|
|
20
|
-
</h2>
|
|
21
|
-
)}
|
|
22
|
-
|
|
23
|
-
{section.embedHtml && section.embedType === 'custom' ? (
|
|
24
|
-
<div
|
|
25
|
-
className="overflow-hidden"
|
|
26
|
-
style={{ borderRadius: 'var(--pk-radius)' }}
|
|
27
|
-
dangerouslySetInnerHTML={{ __html: section.embedHtml }}
|
|
28
|
-
/>
|
|
29
|
-
) : isVideo ? (
|
|
30
|
-
<div className="aspect-video overflow-hidden" style={{ borderRadius: 'var(--pk-radius)' }}>
|
|
31
|
-
<iframe
|
|
32
|
-
src={section.embedUrl}
|
|
33
|
-
className="h-full w-full"
|
|
34
|
-
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
|
35
|
-
loading="lazy"
|
|
36
|
-
title={section.title ?? 'Embed'}
|
|
37
|
-
/>
|
|
38
|
-
</div>
|
|
39
|
-
) : (
|
|
40
|
-
<div className="overflow-hidden" style={{ borderRadius: 'var(--pk-radius)', height }}>
|
|
41
|
-
<iframe
|
|
42
|
-
src={section.embedUrl}
|
|
43
|
-
className="w-full h-full"
|
|
44
|
-
allow="autoplay; clipboard-write; encrypted-media; fullscreen"
|
|
45
|
-
loading="lazy"
|
|
46
|
-
title={section.title ?? 'Embed'}
|
|
47
|
-
/>
|
|
48
|
-
</div>
|
|
49
|
-
)}
|
|
50
|
-
</div>
|
|
51
|
-
</section>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { ArrowUpRight } from 'lucide-react';
|
|
2
|
-
import type { FeaturedSection } from '../../types';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
section: FeaturedSection;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export default function FeaturedBlock({ section }: Props) {
|
|
9
|
-
return (
|
|
10
|
-
<section className="py-6" id={section.id}>
|
|
11
|
-
{section.title && (
|
|
12
|
-
<h2
|
|
13
|
-
className="text-xl md:text-2xl font-bold mb-4 px-1"
|
|
14
|
-
style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
|
|
15
|
-
>
|
|
16
|
-
{section.title}
|
|
17
|
-
</h2>
|
|
18
|
-
)}
|
|
19
|
-
<a
|
|
20
|
-
href={section.url}
|
|
21
|
-
target="_blank"
|
|
22
|
-
rel="noopener noreferrer"
|
|
23
|
-
className="group flex items-center gap-4 p-4 overflow-hidden transition-all duration-300 hover:scale-[1.01]"
|
|
24
|
-
style={{
|
|
25
|
-
borderRadius: 'var(--pk-radius)',
|
|
26
|
-
border: '1px solid rgba(255,255,255,0.09)',
|
|
27
|
-
background:
|
|
28
|
-
'linear-gradient(135deg, color-mix(in srgb, var(--pk-primary) 14%, transparent), rgba(255,255,255,0.03) 55%)',
|
|
29
|
-
}}
|
|
30
|
-
>
|
|
31
|
-
{section.imageUrl && (
|
|
32
|
-
<div
|
|
33
|
-
className="shrink-0 w-20 h-20 overflow-hidden"
|
|
34
|
-
style={{ borderRadius: 'calc(var(--pk-radius) - 4px)' }}
|
|
35
|
-
>
|
|
36
|
-
<img
|
|
37
|
-
src={section.imageUrl}
|
|
38
|
-
alt={section.headline}
|
|
39
|
-
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
|
40
|
-
loading="lazy"
|
|
41
|
-
/>
|
|
42
|
-
</div>
|
|
43
|
-
)}
|
|
44
|
-
<div className="min-w-0 flex-1">
|
|
45
|
-
<p className="text-base font-bold leading-tight" style={{ color: 'var(--pk-text)', fontFamily: 'var(--pk-font-heading)' }}>
|
|
46
|
-
{section.headline}
|
|
47
|
-
</p>
|
|
48
|
-
{section.subtitle && (
|
|
49
|
-
<p className="text-xs mt-1 line-clamp-2" style={{ color: 'var(--pk-muted)' }}>
|
|
50
|
-
{section.subtitle}
|
|
51
|
-
</p>
|
|
52
|
-
)}
|
|
53
|
-
{section.ctaLabel && (
|
|
54
|
-
<span
|
|
55
|
-
className="inline-flex items-center gap-1 mt-2 px-3 py-1 text-xs font-semibold rounded-full"
|
|
56
|
-
style={{ backgroundColor: 'var(--pk-primary)', color: 'var(--pk-bg)' }}
|
|
57
|
-
>
|
|
58
|
-
{section.ctaLabel}
|
|
59
|
-
</span>
|
|
60
|
-
)}
|
|
61
|
-
</div>
|
|
62
|
-
<ArrowUpRight
|
|
63
|
-
size={20}
|
|
64
|
-
className="shrink-0 opacity-50 group-hover:opacity-90 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-all"
|
|
65
|
-
style={{ color: 'var(--pk-text)' }}
|
|
66
|
-
/>
|
|
67
|
-
</a>
|
|
68
|
-
</section>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import type { GallerySection as GallerySectionType, BioMediaItem } from '../../types';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
section: GallerySectionType;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function isInstagramEmbed(item: BioMediaItem) {
|
|
9
|
-
return item.url.includes('instagram.com');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function getInstagramEmbedUrl(url: string) {
|
|
13
|
-
// Convert instagram reel/post URL to embed URL
|
|
14
|
-
const match = url.match(/instagram\.com\/(reel|p)\/([^/?]+)/);
|
|
15
|
-
if (match) return `https://www.instagram.com/${match[1]}/${match[2]}/embed`;
|
|
16
|
-
return url;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function MediaItem({ item, index, cols, isFeatured, onImageClick }: {
|
|
20
|
-
item: BioMediaItem;
|
|
21
|
-
index: number;
|
|
22
|
-
cols: number;
|
|
23
|
-
isFeatured: boolean;
|
|
24
|
-
onImageClick: (i: number) => void;
|
|
25
|
-
}) {
|
|
26
|
-
if (isInstagramEmbed(item)) {
|
|
27
|
-
return (
|
|
28
|
-
<div
|
|
29
|
-
className={`overflow-hidden ${isFeatured && index === 0 ? 'col-span-2' : ''}`}
|
|
30
|
-
style={{ borderRadius: 'var(--pk-radius)' }}
|
|
31
|
-
>
|
|
32
|
-
<iframe
|
|
33
|
-
src={getInstagramEmbedUrl(item.url)}
|
|
34
|
-
className="w-full border-0"
|
|
35
|
-
style={{ minHeight: cols <= 2 ? '500px' : '420px' }}
|
|
36
|
-
loading="lazy"
|
|
37
|
-
// @ts-expect-error React wants lowercase for custom DOM attributes
|
|
38
|
-
allowtransparency="true"
|
|
39
|
-
allow="encrypted-media"
|
|
40
|
-
title={item.alt ?? 'Instagram'}
|
|
41
|
-
/>
|
|
42
|
-
</div>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<img
|
|
48
|
-
src={item.url}
|
|
49
|
-
alt={item.alt ?? ''}
|
|
50
|
-
className={`w-full object-cover cursor-pointer hover:opacity-90 transition-opacity ${
|
|
51
|
-
isFeatured && index === 0 ? 'col-span-2 aspect-[16/9]' : 'aspect-square'
|
|
52
|
-
}`}
|
|
53
|
-
style={{ borderRadius: 'var(--pk-radius)' }}
|
|
54
|
-
loading="lazy"
|
|
55
|
-
onClick={() => onImageClick(index)}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export default function GallerySection({ section }: Props) {
|
|
61
|
-
const [lightboxIdx, setLightboxIdx] = useState<number | null>(null);
|
|
62
|
-
const cols = section.columns ?? 3;
|
|
63
|
-
const isFeatured = section.layout === 'featured-grid';
|
|
64
|
-
const isCarousel = section.layout === 'carousel';
|
|
65
|
-
|
|
66
|
-
// Only images for lightbox navigation
|
|
67
|
-
const imageItems = section.media.filter((m) => !isInstagramEmbed(m));
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<section className="py-16 md:py-24 px-6" id={section.id}>
|
|
71
|
-
<div className="max-w-5xl mx-auto">
|
|
72
|
-
{section.title && (
|
|
73
|
-
<h2
|
|
74
|
-
className="text-3xl md:text-4xl font-bold mb-8"
|
|
75
|
-
style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
|
|
76
|
-
>
|
|
77
|
-
{section.title}
|
|
78
|
-
</h2>
|
|
79
|
-
)}
|
|
80
|
-
|
|
81
|
-
{isCarousel ? (
|
|
82
|
-
<div className="flex gap-4 overflow-x-auto snap-x snap-mandatory pb-4 -mx-6 px-6 scrollbar-hide">
|
|
83
|
-
{section.media.map((item, i) => (
|
|
84
|
-
<div key={i} className="flex-none w-72 md:w-96 snap-center">
|
|
85
|
-
<MediaItem item={item} index={i} cols={cols} isFeatured={false} onImageClick={setLightboxIdx} />
|
|
86
|
-
</div>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
) : (
|
|
90
|
-
<div
|
|
91
|
-
className={`grid gap-3 md:gap-4 ${
|
|
92
|
-
isFeatured
|
|
93
|
-
? 'grid-cols-2'
|
|
94
|
-
: cols === 2
|
|
95
|
-
? 'grid-cols-1 sm:grid-cols-2'
|
|
96
|
-
: cols === 4
|
|
97
|
-
? 'grid-cols-2 md:grid-cols-4'
|
|
98
|
-
: 'grid-cols-2 md:grid-cols-3'
|
|
99
|
-
}`}
|
|
100
|
-
>
|
|
101
|
-
{section.media.map((item, i) => (
|
|
102
|
-
<MediaItem key={i} item={item} index={i} cols={cols} isFeatured={isFeatured} onImageClick={setLightboxIdx} />
|
|
103
|
-
))}
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
|
|
107
|
-
{/* Lightbox — images only */}
|
|
108
|
-
{lightboxIdx !== null && imageItems[lightboxIdx] && (
|
|
109
|
-
<div
|
|
110
|
-
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 p-4"
|
|
111
|
-
onClick={() => setLightboxIdx(null)}
|
|
112
|
-
>
|
|
113
|
-
<button
|
|
114
|
-
className="absolute top-4 right-4 text-white text-3xl font-light hover:opacity-70 z-10"
|
|
115
|
-
onClick={() => setLightboxIdx(null)}
|
|
116
|
-
>
|
|
117
|
-
×
|
|
118
|
-
</button>
|
|
119
|
-
<button
|
|
120
|
-
className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl font-light hover:opacity-70 z-10"
|
|
121
|
-
onClick={(e) => {
|
|
122
|
-
e.stopPropagation();
|
|
123
|
-
setLightboxIdx((lightboxIdx - 1 + imageItems.length) % imageItems.length);
|
|
124
|
-
}}
|
|
125
|
-
>
|
|
126
|
-
‹
|
|
127
|
-
</button>
|
|
128
|
-
<button
|
|
129
|
-
className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl font-light hover:opacity-70 z-10"
|
|
130
|
-
onClick={(e) => {
|
|
131
|
-
e.stopPropagation();
|
|
132
|
-
setLightboxIdx((lightboxIdx + 1) % imageItems.length);
|
|
133
|
-
}}
|
|
134
|
-
>
|
|
135
|
-
›
|
|
136
|
-
</button>
|
|
137
|
-
<img
|
|
138
|
-
src={imageItems[lightboxIdx].url}
|
|
139
|
-
alt={imageItems[lightboxIdx].alt ?? ''}
|
|
140
|
-
className="max-h-[85vh] max-w-[90vw] object-contain"
|
|
141
|
-
onClick={(e) => e.stopPropagation()}
|
|
142
|
-
/>
|
|
143
|
-
</div>
|
|
144
|
-
)}
|
|
145
|
-
</div>
|
|
146
|
-
</section>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import type { HeroSection as HeroSectionType, BioIdentity, BioBranding } from '../../types';
|
|
2
|
-
|
|
3
|
-
interface Props {
|
|
4
|
-
section: HeroSectionType;
|
|
5
|
-
identity: BioIdentity;
|
|
6
|
-
branding: BioBranding;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default function HeroSection({ section, identity, branding }: Props) {
|
|
10
|
-
const hasLogo = section.showLogo && branding.logoUrl;
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<section
|
|
14
|
-
className="relative flex flex-col items-center justify-center min-h-screen overflow-hidden"
|
|
15
|
-
id={section.id}
|
|
16
|
-
>
|
|
17
|
-
{/* Background image (skipped when no image — overlay carries the visual) */}
|
|
18
|
-
<div className="absolute inset-0">
|
|
19
|
-
{section.backgroundImageUrl && (
|
|
20
|
-
<img
|
|
21
|
-
src={section.backgroundImageUrl}
|
|
22
|
-
alt={identity.name}
|
|
23
|
-
className="h-full w-full object-cover"
|
|
24
|
-
/>
|
|
25
|
-
)}
|
|
26
|
-
{section.overlayGradient && (
|
|
27
|
-
<div className="absolute inset-0" style={{ background: section.overlayGradient }} />
|
|
28
|
-
)}
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
{/* Content — always centered */}
|
|
32
|
-
<div className="relative z-10 flex flex-col items-center text-center px-6 pt-20 pb-24">
|
|
33
|
-
{hasLogo ? (
|
|
34
|
-
<img
|
|
35
|
-
src={branding.logoUrl}
|
|
36
|
-
alt={`${identity.name} logo`}
|
|
37
|
-
className="mb-6 h-48 w-auto md:h-64 lg:h-80 object-contain drop-shadow-2xl"
|
|
38
|
-
/>
|
|
39
|
-
) : (
|
|
40
|
-
<h1
|
|
41
|
-
className="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tight mb-4 drop-shadow-lg"
|
|
42
|
-
style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
|
|
43
|
-
>
|
|
44
|
-
{identity.name}
|
|
45
|
-
</h1>
|
|
46
|
-
)}
|
|
47
|
-
|
|
48
|
-
{identity.tagline && (
|
|
49
|
-
<p
|
|
50
|
-
className="text-lg md:text-xl mb-6 max-w-lg opacity-70"
|
|
51
|
-
style={{ color: 'var(--pk-text)' }}
|
|
52
|
-
>
|
|
53
|
-
{identity.tagline}
|
|
54
|
-
</p>
|
|
55
|
-
)}
|
|
56
|
-
|
|
57
|
-
{section.showGenres && identity.genres.length > 0 && (
|
|
58
|
-
<div className="flex flex-wrap justify-center gap-2 mb-4">
|
|
59
|
-
{identity.genres.map((genre) => (
|
|
60
|
-
<span
|
|
61
|
-
key={genre}
|
|
62
|
-
className="px-4 py-1.5 text-sm font-semibold backdrop-blur-sm"
|
|
63
|
-
style={{
|
|
64
|
-
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
65
|
-
color: 'var(--pk-primary)',
|
|
66
|
-
border: '1px solid rgba(255,255,255,0.12)',
|
|
67
|
-
borderRadius: '9999px',
|
|
68
|
-
}}
|
|
69
|
-
>
|
|
70
|
-
{genre}
|
|
71
|
-
</span>
|
|
72
|
-
))}
|
|
73
|
-
</div>
|
|
74
|
-
)}
|
|
75
|
-
|
|
76
|
-
{section.showLocation && identity.location && (
|
|
77
|
-
<p className="text-sm mt-2 opacity-50" style={{ color: 'var(--pk-text)' }}>
|
|
78
|
-
{identity.location}
|
|
79
|
-
</p>
|
|
80
|
-
)}
|
|
81
|
-
|
|
82
|
-
{section.ctaText && section.ctaUrl && (
|
|
83
|
-
<a
|
|
84
|
-
href={section.ctaUrl}
|
|
85
|
-
target="_blank"
|
|
86
|
-
rel="noopener noreferrer"
|
|
87
|
-
className="mt-8 inline-block px-10 py-4 font-bold text-lg tracking-wider transition-transform hover:scale-105"
|
|
88
|
-
style={{
|
|
89
|
-
backgroundColor: 'var(--pk-primary)',
|
|
90
|
-
color: 'var(--pk-bg)',
|
|
91
|
-
borderRadius: '9999px',
|
|
92
|
-
}}
|
|
93
|
-
>
|
|
94
|
-
{section.ctaText}
|
|
95
|
-
</a>
|
|
96
|
-
)}
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
{/* Scroll hint */}
|
|
100
|
-
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-10 animate-bounce opacity-40">
|
|
101
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: 'var(--pk-text)' }}>
|
|
102
|
-
<path d="M12 5v14M5 12l7 7 7-7" />
|
|
103
|
-
</svg>
|
|
104
|
-
</div>
|
|
105
|
-
</section>
|
|
106
|
-
);
|
|
107
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { ChevronRight } from 'lucide-react';
|
|
2
|
-
import type { LinksSection, LinkCardItem } from '../../types';
|
|
3
|
-
import { PlatformIcon } from '../components/PlatformIcon';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
section: LinksSection;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function LinkCard({ item }: { item: LinkCardItem }) {
|
|
10
|
-
return (
|
|
11
|
-
<a
|
|
12
|
-
href={item.url}
|
|
13
|
-
target="_blank"
|
|
14
|
-
rel="noopener noreferrer"
|
|
15
|
-
className="group flex items-center gap-4 p-3 transition-all duration-200 hover:scale-[1.015] active:scale-[0.99]"
|
|
16
|
-
style={{
|
|
17
|
-
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
18
|
-
border: '1px solid rgba(255,255,255,0.07)',
|
|
19
|
-
borderRadius: 'var(--pk-radius)',
|
|
20
|
-
}}
|
|
21
|
-
>
|
|
22
|
-
<div
|
|
23
|
-
className="shrink-0 flex items-center justify-center w-12 h-12 overflow-hidden"
|
|
24
|
-
style={{ borderRadius: 'calc(var(--pk-radius) - 4px)', backgroundColor: 'rgba(255,255,255,0.06)' }}
|
|
25
|
-
>
|
|
26
|
-
{item.thumbnailUrl ? (
|
|
27
|
-
<img src={item.thumbnailUrl} alt="" className="w-full h-full object-cover" loading="lazy" />
|
|
28
|
-
) : (
|
|
29
|
-
<PlatformIcon platform={item.icon ?? 'custom'} size={20} style={{ color: 'var(--pk-primary)' }} />
|
|
30
|
-
)}
|
|
31
|
-
</div>
|
|
32
|
-
<div className="min-w-0 flex-1">
|
|
33
|
-
<div className="flex items-center gap-2">
|
|
34
|
-
<span className="text-sm font-semibold truncate" style={{ color: 'var(--pk-text)' }}>
|
|
35
|
-
{item.title}
|
|
36
|
-
</span>
|
|
37
|
-
{item.badge && (
|
|
38
|
-
<span
|
|
39
|
-
className="text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded-full shrink-0"
|
|
40
|
-
style={{ backgroundColor: 'var(--pk-primary)', color: 'var(--pk-bg)' }}
|
|
41
|
-
>
|
|
42
|
-
{item.badge}
|
|
43
|
-
</span>
|
|
44
|
-
)}
|
|
45
|
-
</div>
|
|
46
|
-
{item.subtitle && (
|
|
47
|
-
<p className="text-xs truncate mt-0.5" style={{ color: 'var(--pk-muted)' }}>
|
|
48
|
-
{item.subtitle}
|
|
49
|
-
</p>
|
|
50
|
-
)}
|
|
51
|
-
</div>
|
|
52
|
-
<ChevronRight
|
|
53
|
-
size={18}
|
|
54
|
-
className="shrink-0 opacity-40 group-hover:opacity-80 group-hover:translate-x-0.5 transition-all"
|
|
55
|
-
style={{ color: 'var(--pk-text)' }}
|
|
56
|
-
/>
|
|
57
|
-
</a>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export default function LinksBlock({ section }: Props) {
|
|
62
|
-
const isGrid = section.layout === 'grid';
|
|
63
|
-
return (
|
|
64
|
-
<section className="py-6" id={section.id}>
|
|
65
|
-
{section.title && (
|
|
66
|
-
<h2
|
|
67
|
-
className="text-xl md:text-2xl font-bold mb-4 px-1"
|
|
68
|
-
style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
|
|
69
|
-
>
|
|
70
|
-
{section.title}
|
|
71
|
-
</h2>
|
|
72
|
-
)}
|
|
73
|
-
<div className={isGrid ? 'grid grid-cols-2 gap-3' : 'flex flex-col gap-3'}>
|
|
74
|
-
{section.links.map((item, i) => (
|
|
75
|
-
<LinkCard key={item.url + i} item={item} />
|
|
76
|
-
))}
|
|
77
|
-
</div>
|
|
78
|
-
</section>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { Play } from 'lucide-react';
|
|
2
|
-
import type { MusicReleasesSection, MusicReleaseItem } from '../../types';
|
|
3
|
-
import { Carousel } from '../components/Carousel';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
section: MusicReleasesSection;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function ReleaseCard({ item, cta, wide }: { item: MusicReleaseItem; cta: string; wide?: boolean }) {
|
|
10
|
-
return (
|
|
11
|
-
<a
|
|
12
|
-
href={item.url}
|
|
13
|
-
target="_blank"
|
|
14
|
-
rel="noopener noreferrer"
|
|
15
|
-
className={`group ${wide ? 'flex-none w-40 snap-start' : ''} flex flex-col transition-transform hover:scale-[1.02]`}
|
|
16
|
-
>
|
|
17
|
-
<div
|
|
18
|
-
className="relative w-full aspect-square overflow-hidden"
|
|
19
|
-
style={{ borderRadius: 'var(--pk-radius)', backgroundColor: 'rgba(255,255,255,0.05)' }}
|
|
20
|
-
>
|
|
21
|
-
<img src={item.artworkUrl} alt={item.title} className="w-full h-full object-cover" loading="lazy" />
|
|
22
|
-
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
|
23
|
-
style={{ background: 'rgba(0,0,0,0.35)' }}
|
|
24
|
-
>
|
|
25
|
-
<span
|
|
26
|
-
className="flex items-center justify-center w-11 h-11 rounded-full"
|
|
27
|
-
style={{ backgroundColor: 'var(--pk-primary)', color: 'var(--pk-bg)' }}
|
|
28
|
-
>
|
|
29
|
-
<Play size={20} fill="currentColor" />
|
|
30
|
-
</span>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
<div className="mt-2 px-0.5">
|
|
34
|
-
<p className="text-sm font-semibold truncate" style={{ color: 'var(--pk-text)' }}>
|
|
35
|
-
{item.title}
|
|
36
|
-
</p>
|
|
37
|
-
{item.artist && (
|
|
38
|
-
<p className="text-xs truncate" style={{ color: 'var(--pk-muted)' }}>
|
|
39
|
-
{item.artist}
|
|
40
|
-
</p>
|
|
41
|
-
)}
|
|
42
|
-
<span
|
|
43
|
-
className="inline-flex items-center gap-1 mt-1.5 px-3 py-1 text-xs font-semibold rounded-full"
|
|
44
|
-
style={{ backgroundColor: 'rgba(255,255,255,0.08)', color: 'var(--pk-text)' }}
|
|
45
|
-
>
|
|
46
|
-
<Play size={11} fill="currentColor" /> {cta}
|
|
47
|
-
</span>
|
|
48
|
-
</div>
|
|
49
|
-
</a>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export default function MusicReleasesBlock({ section }: Props) {
|
|
54
|
-
const cta = section.ctaLabel ?? 'Ouvir';
|
|
55
|
-
if (section.layout === 'grid') {
|
|
56
|
-
return (
|
|
57
|
-
<section className="py-6" id={section.id}>
|
|
58
|
-
{section.title && (
|
|
59
|
-
<h2
|
|
60
|
-
className="text-xl md:text-2xl font-bold mb-4 px-1"
|
|
61
|
-
style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
|
|
62
|
-
>
|
|
63
|
-
{section.title}
|
|
64
|
-
</h2>
|
|
65
|
-
)}
|
|
66
|
-
<div className="grid grid-cols-2 gap-4">
|
|
67
|
-
{section.releases.map((item, i) => (
|
|
68
|
-
<ReleaseCard key={item.url + i} item={item} cta={cta} />
|
|
69
|
-
))}
|
|
70
|
-
</div>
|
|
71
|
-
</section>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
return (
|
|
75
|
-
<section className="py-6" id={section.id}>
|
|
76
|
-
<Carousel title={section.title}>
|
|
77
|
-
{section.releases.map((item, i) => (
|
|
78
|
-
<ReleaseCard key={item.url + i} item={item} cta={cta} wide />
|
|
79
|
-
))}
|
|
80
|
-
</Carousel>
|
|
81
|
-
</section>
|
|
82
|
-
);
|
|
83
|
-
}
|