@fayz-ai/plugin-linkinbio 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,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
- }