@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,91 +0,0 @@
|
|
|
1
|
-
import type { VenueListSection as VenueListSectionType } from '../../types';
|
|
2
|
-
|
|
3
|
-
interface Props {
|
|
4
|
-
section: VenueListSectionType;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export default function VenueListSection({ section }: Props) {
|
|
8
|
-
const layout = section.layout ?? 'grid';
|
|
9
|
-
|
|
10
|
-
return (
|
|
11
|
-
<section className="py-16 md:py-24 px-6" id={section.id}>
|
|
12
|
-
<div className="max-w-5xl mx-auto">
|
|
13
|
-
{section.title && (
|
|
14
|
-
<h2
|
|
15
|
-
className="text-3xl md:text-4xl font-bold mb-10"
|
|
16
|
-
style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
|
|
17
|
-
>
|
|
18
|
-
{section.title}
|
|
19
|
-
</h2>
|
|
20
|
-
)}
|
|
21
|
-
|
|
22
|
-
{layout === 'logo-cloud' ? (
|
|
23
|
-
<div className="flex flex-wrap justify-center gap-6 md:gap-8">
|
|
24
|
-
{section.venues.map((venue, i) => (
|
|
25
|
-
<div key={i} className="flex flex-col items-center gap-2">
|
|
26
|
-
{venue.logoUrl ? (
|
|
27
|
-
<img src={venue.logoUrl} alt={venue.name} className="h-12 w-12 object-contain opacity-70" loading="lazy" />
|
|
28
|
-
) : (
|
|
29
|
-
<div
|
|
30
|
-
className="h-12 w-12 flex items-center justify-center text-lg font-bold opacity-70"
|
|
31
|
-
style={{ backgroundColor: 'rgba(255,255,255,0.06)', color: 'var(--pk-text)', borderRadius: 'var(--pk-radius)' }}
|
|
32
|
-
>
|
|
33
|
-
{venue.name[0]}
|
|
34
|
-
</div>
|
|
35
|
-
)}
|
|
36
|
-
<span className="text-xs text-center" style={{ color: 'var(--pk-muted)' }}>{venue.name}</span>
|
|
37
|
-
</div>
|
|
38
|
-
))}
|
|
39
|
-
</div>
|
|
40
|
-
) : layout === 'list' ? (
|
|
41
|
-
<div className="space-y-3">
|
|
42
|
-
{section.venues.map((venue, i) => (
|
|
43
|
-
<div
|
|
44
|
-
key={i}
|
|
45
|
-
className="flex items-center justify-between py-3 px-4"
|
|
46
|
-
style={{ backgroundColor: 'rgba(255,255,255,0.04)', borderRadius: 'var(--pk-radius)' }}
|
|
47
|
-
>
|
|
48
|
-
<span className="font-medium" style={{ color: 'var(--pk-text)' }}>{venue.name}</span>
|
|
49
|
-
<span className="text-sm" style={{ color: 'var(--pk-muted)' }}>
|
|
50
|
-
{[venue.city, venue.year].filter(Boolean).join(' · ')}
|
|
51
|
-
</span>
|
|
52
|
-
</div>
|
|
53
|
-
))}
|
|
54
|
-
</div>
|
|
55
|
-
) : (
|
|
56
|
-
/* Organic flowing tag cloud */
|
|
57
|
-
<div className="flex flex-wrap gap-3">
|
|
58
|
-
{section.venues.map((venue, i) => {
|
|
59
|
-
// Vary sizes based on name length and position for organic feel
|
|
60
|
-
const isLong = venue.name.length > 12;
|
|
61
|
-
const isShort = venue.name.length < 6;
|
|
62
|
-
const sizeClass = isLong ? 'text-base md:text-lg px-6 py-3' : isShort ? 'text-sm px-4 py-2.5' : 'text-sm md:text-base px-5 py-3';
|
|
63
|
-
// Alternate subtle visual weight
|
|
64
|
-
const isHighlight = i % 5 === 0 || i % 7 === 0;
|
|
65
|
-
return (
|
|
66
|
-
<div
|
|
67
|
-
key={i}
|
|
68
|
-
className={`inline-flex flex-col items-start transition-all duration-300 hover:scale-105 ${sizeClass}`}
|
|
69
|
-
style={{
|
|
70
|
-
backgroundColor: isHighlight ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.03)',
|
|
71
|
-
border: isHighlight ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(255,255,255,0.06)',
|
|
72
|
-
borderRadius: '9999px',
|
|
73
|
-
}}
|
|
74
|
-
>
|
|
75
|
-
<span className="font-semibold whitespace-nowrap" style={{ color: 'var(--pk-text)' }}>
|
|
76
|
-
{venue.name}
|
|
77
|
-
{venue.city && (
|
|
78
|
-
<span className="font-normal ml-2 opacity-50" style={{ color: 'var(--pk-muted)', fontSize: '0.8em' }}>
|
|
79
|
-
{venue.city}
|
|
80
|
-
</span>
|
|
81
|
-
)}
|
|
82
|
-
</span>
|
|
83
|
-
</div>
|
|
84
|
-
);
|
|
85
|
-
})}
|
|
86
|
-
</div>
|
|
87
|
-
)}
|
|
88
|
-
</div>
|
|
89
|
-
</section>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import type { VideoGridSection as VideoGridSectionType } from '../../types';
|
|
2
|
-
|
|
3
|
-
interface Props {
|
|
4
|
-
section: VideoGridSectionType;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export default function VideoGridSection({ section }: Props) {
|
|
8
|
-
const isFeatured = section.layout === 'featured';
|
|
9
|
-
const isList = section.layout === 'list';
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<section className="py-16 md:py-24 px-6" id={section.id}>
|
|
13
|
-
<div className="max-w-5xl 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
|
-
{isFeatured && section.videos.length > 0 ? (
|
|
24
|
-
<div className="space-y-6">
|
|
25
|
-
{/* Featured video */}
|
|
26
|
-
<div className="aspect-video w-full overflow-hidden" style={{ borderRadius: 'var(--pk-radius)' }}>
|
|
27
|
-
<iframe
|
|
28
|
-
src={`https://www.youtube.com/embed/${section.videos[0].videoId}`}
|
|
29
|
-
title={section.videos[0].title ?? 'Video'}
|
|
30
|
-
className="h-full w-full"
|
|
31
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
32
|
-
allowFullScreen
|
|
33
|
-
loading="lazy"
|
|
34
|
-
/>
|
|
35
|
-
</div>
|
|
36
|
-
{section.videos[0].title && (
|
|
37
|
-
<p className="text-sm font-medium" style={{ color: 'var(--pk-muted)' }}>
|
|
38
|
-
{section.videos[0].title}
|
|
39
|
-
</p>
|
|
40
|
-
)}
|
|
41
|
-
{/* Remaining videos */}
|
|
42
|
-
{section.videos.length > 1 && (
|
|
43
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
44
|
-
{section.videos.slice(1).map((video, i) => (
|
|
45
|
-
<div key={i}>
|
|
46
|
-
<div className="aspect-video overflow-hidden" style={{ borderRadius: 'var(--pk-radius)' }}>
|
|
47
|
-
<iframe
|
|
48
|
-
src={`https://www.youtube.com/embed/${video.videoId}`}
|
|
49
|
-
title={video.title ?? 'Video'}
|
|
50
|
-
className="h-full w-full"
|
|
51
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
52
|
-
allowFullScreen
|
|
53
|
-
loading="lazy"
|
|
54
|
-
/>
|
|
55
|
-
</div>
|
|
56
|
-
{video.title && (
|
|
57
|
-
<p className="mt-2 text-sm" style={{ color: 'var(--pk-muted)' }}>
|
|
58
|
-
{video.title}
|
|
59
|
-
</p>
|
|
60
|
-
)}
|
|
61
|
-
</div>
|
|
62
|
-
))}
|
|
63
|
-
</div>
|
|
64
|
-
)}
|
|
65
|
-
</div>
|
|
66
|
-
) : (
|
|
67
|
-
<div className={`grid gap-4 md:gap-6 ${isList ? 'grid-cols-1' : 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'}`}>
|
|
68
|
-
{section.videos.map((video, i) => (
|
|
69
|
-
<div key={i}>
|
|
70
|
-
<div className="aspect-video overflow-hidden" style={{ borderRadius: 'var(--pk-radius)' }}>
|
|
71
|
-
<iframe
|
|
72
|
-
src={`https://www.youtube.com/embed/${video.videoId}`}
|
|
73
|
-
title={video.title ?? 'Video'}
|
|
74
|
-
className="h-full w-full"
|
|
75
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
76
|
-
allowFullScreen
|
|
77
|
-
loading="lazy"
|
|
78
|
-
/>
|
|
79
|
-
</div>
|
|
80
|
-
{video.title && (
|
|
81
|
-
<p className="mt-2 text-sm" style={{ color: 'var(--pk-muted)' }}>
|
|
82
|
-
{video.title}
|
|
83
|
-
</p>
|
|
84
|
-
)}
|
|
85
|
-
</div>
|
|
86
|
-
))}
|
|
87
|
-
{section.channelUrl && (
|
|
88
|
-
<a
|
|
89
|
-
href={section.channelUrl}
|
|
90
|
-
target="_blank"
|
|
91
|
-
rel="noopener noreferrer"
|
|
92
|
-
className="aspect-video overflow-hidden flex items-center justify-center border border-white/10 hover:border-white/30 transition-colors"
|
|
93
|
-
style={{ borderRadius: 'var(--pk-radius)', background: 'var(--pk-alt-bg, rgba(255,255,255,0.05))' }}
|
|
94
|
-
>
|
|
95
|
-
<span className="text-lg font-semibold" style={{ color: 'var(--pk-text)', fontFamily: 'var(--pk-font-heading)' }}>
|
|
96
|
-
Ver todos →
|
|
97
|
-
</span>
|
|
98
|
-
</a>
|
|
99
|
-
)}
|
|
100
|
-
</div>
|
|
101
|
-
)}
|
|
102
|
-
</div>
|
|
103
|
-
</section>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export { default as HeroSection } from './HeroSection';
|
|
2
|
-
export { default as BioSection } from './BioSection';
|
|
3
|
-
export { default as GallerySection } from './GallerySection';
|
|
4
|
-
export { default as VideoGridSection } from './VideoGridSection';
|
|
5
|
-
export { default as SocialLinksSection } from './SocialLinksSection';
|
|
6
|
-
export { default as EmbedSection } from './EmbedSection';
|
|
7
|
-
export { default as ProjectSection } from './ProjectSection';
|
|
8
|
-
export { default as StatsSection } from './StatsSection';
|
|
9
|
-
export { default as VenueListSection } from './VenueListSection';
|
|
10
|
-
export { default as CTASection } from './CTASection';
|
|
11
|
-
export { default as TextSection } from './TextSection';
|
|
12
|
-
export { default as ProfileHeader } from './ProfileHeader';
|
|
13
|
-
export { default as LinksBlock } from './LinksBlock';
|
|
14
|
-
export { default as MusicReleasesBlock } from './MusicReleasesBlock';
|
|
15
|
-
export { default as TourDatesBlock } from './TourDatesBlock';
|
|
16
|
-
export { default as FeaturedBlock } from './FeaturedBlock';
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
-
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Komi-style horizontal collection: a section title with prev/next controls and
|
|
6
|
-
* a snap-scrolling track. Mobile-first — the track scrolls by touch and the
|
|
7
|
-
* arrows nudge it by ~80% of the viewport. Arrows disable at the track ends.
|
|
8
|
-
*/
|
|
9
|
-
export function Carousel({ title, children }: { title?: string; children: ReactNode }) {
|
|
10
|
-
const trackRef = useRef<HTMLDivElement>(null);
|
|
11
|
-
const [atStart, setAtStart] = useState(true);
|
|
12
|
-
const [atEnd, setAtEnd] = useState(false);
|
|
13
|
-
|
|
14
|
-
const update = () => {
|
|
15
|
-
const el = trackRef.current;
|
|
16
|
-
if (!el) return;
|
|
17
|
-
setAtStart(el.scrollLeft <= 2);
|
|
18
|
-
setAtEnd(el.scrollLeft + el.clientWidth >= el.scrollWidth - 2);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
update();
|
|
23
|
-
const el = trackRef.current;
|
|
24
|
-
if (!el) return;
|
|
25
|
-
el.addEventListener('scroll', update, { passive: true });
|
|
26
|
-
window.addEventListener('resize', update);
|
|
27
|
-
return () => {
|
|
28
|
-
el.removeEventListener('scroll', update);
|
|
29
|
-
window.removeEventListener('resize', update);
|
|
30
|
-
};
|
|
31
|
-
}, []);
|
|
32
|
-
|
|
33
|
-
const nudge = (dir: 1 | -1) => {
|
|
34
|
-
const el = trackRef.current;
|
|
35
|
-
if (!el) return;
|
|
36
|
-
el.scrollBy({ left: dir * el.clientWidth * 0.8, behavior: 'smooth' });
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const arrowStyle = (disabled: boolean) => ({
|
|
40
|
-
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
41
|
-
color: 'var(--pk-text)',
|
|
42
|
-
opacity: disabled ? 0.25 : 1,
|
|
43
|
-
borderRadius: '9999px',
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<div>
|
|
48
|
-
{(title || true) && (
|
|
49
|
-
<div className="flex items-center justify-between mb-4 px-1">
|
|
50
|
-
{title && (
|
|
51
|
-
<h2
|
|
52
|
-
className="text-xl md:text-2xl font-bold"
|
|
53
|
-
style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
|
|
54
|
-
>
|
|
55
|
-
{title}
|
|
56
|
-
</h2>
|
|
57
|
-
)}
|
|
58
|
-
<div className="flex gap-2">
|
|
59
|
-
<button
|
|
60
|
-
type="button"
|
|
61
|
-
aria-label="Anterior"
|
|
62
|
-
onClick={() => nudge(-1)}
|
|
63
|
-
disabled={atStart}
|
|
64
|
-
className="flex items-center justify-center w-9 h-9 transition-opacity"
|
|
65
|
-
style={arrowStyle(atStart)}
|
|
66
|
-
>
|
|
67
|
-
<ChevronLeft size={18} />
|
|
68
|
-
</button>
|
|
69
|
-
<button
|
|
70
|
-
type="button"
|
|
71
|
-
aria-label="Próximo"
|
|
72
|
-
onClick={() => nudge(1)}
|
|
73
|
-
disabled={atEnd}
|
|
74
|
-
className="flex items-center justify-center w-9 h-9 transition-opacity"
|
|
75
|
-
style={arrowStyle(atEnd)}
|
|
76
|
-
>
|
|
77
|
-
<ChevronRight size={18} />
|
|
78
|
-
</button>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
)}
|
|
82
|
-
<div
|
|
83
|
-
ref={trackRef}
|
|
84
|
-
className="flex gap-3 overflow-x-auto snap-x snap-mandatory scrollbar-hide pb-1"
|
|
85
|
-
>
|
|
86
|
-
{children}
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
@@ -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
|
-
}
|