@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,174 +0,0 @@
1
- import { useEffect, useRef, useState } from 'react';
2
- import { ChevronDown } from 'lucide-react';
3
- import type { ProfileHeaderSection, BioPage } from '../../types';
4
- import { platformLabels } from '../utils';
5
- import { PlatformIcon } from '../components/PlatformIcon';
6
-
7
- interface Props {
8
- section: ProfileHeaderSection;
9
- page: BioPage;
10
- }
11
-
12
- export default function ProfileHeader({ section, page }: Props) {
13
- const { identity, branding } = page;
14
- const isHub = page.layout === 'hub';
15
- const socials = section.socials ?? page.socialLinks ?? [];
16
- const hasLogo = section.showLogo && branding.logoUrl;
17
-
18
- // Scroll-driven parallax: the image lags while the logo lifts + fades on its
19
- // own curve, so the hero opens clean (just the logo) and dissolves on scroll.
20
- const heroRef = useRef<HTMLDivElement>(null);
21
- const [sy, setSy] = useState(0);
22
- useEffect(() => {
23
- const container = document.querySelector('.fixed.inset-0') as HTMLElement | null;
24
- if (!container) return;
25
- const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
26
- if (reduce) return;
27
- let raf = 0;
28
- const onScroll = () => {
29
- if (raf) return;
30
- raf = requestAnimationFrame(() => {
31
- setSy(container.scrollTop);
32
- raf = 0;
33
- });
34
- };
35
- container.addEventListener('scroll', onScroll, { passive: true });
36
- return () => {
37
- container.removeEventListener('scroll', onScroll);
38
- if (raf) cancelAnimationFrame(raf);
39
- };
40
- }, []);
41
-
42
- const heroH = heroRef.current?.offsetHeight ?? 700;
43
- const p = Math.min(1, sy / heroH); // 0 → 1 over the hero's height
44
- // imageOffsetTop nudges the image content DOWN inside the frame (not a margin);
45
- // the baseline scale (>1) leaves overflow room so no gap opens at the top.
46
- const imageOffsetTop = section.imageOffsetTop ?? '0px';
47
- const imageTransform = `translateY(calc(${imageOffsetTop} + ${sy * 0.22}px)) scale(1.18)`;
48
- const logoTransform = `translateY(${-sy * 0.5}px) scale(${1 - p * 0.12})`;
49
- const logoOpacity = Math.max(0, 1 - p * 1.25);
50
- const hintOpacity = Math.max(0, 1 - p * 4);
51
-
52
- const scrollTo = (id: string) => {
53
- const el = typeof document !== 'undefined' ? document.getElementById(id) : null;
54
- el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
55
- };
56
-
57
- return (
58
- <header id={section.id} className="relative">
59
- {/* Hero — fills the viewport so the page opens clean on just the logo. */}
60
- <div
61
- ref={heroRef}
62
- className={`relative w-full h-[86vh] max-h-[880px] overflow-hidden ${
63
- isHub ? 'sm:rounded-[26px]' : ''
64
- }`}
65
- >
66
- {section.imageUrl && (
67
- <img
68
- src={section.imageUrl}
69
- alt={identity.name}
70
- className="absolute inset-0 h-full w-full object-cover will-change-transform"
71
- style={{ transform: imageTransform, objectPosition: section.imageObjectPosition }}
72
- />
73
- )}
74
- <div
75
- className="absolute inset-0"
76
- style={{
77
- background:
78
- section.overlayGradient ??
79
- 'linear-gradient(to bottom, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.35) 60%, var(--pk-bg) 100%)',
80
- }}
81
- />
82
-
83
- {/* Logo (or name) overlaid — its own motion curve */}
84
- <div className="absolute inset-x-0 bottom-0 flex flex-col items-center text-center px-6 pb-14">
85
- {hasLogo ? (
86
- <img
87
- src={branding.logoUrl}
88
- alt={`${identity.name} logo`}
89
- className="w-[82%] max-w-[380px] h-auto object-contain drop-shadow-[0_4px_24px_rgba(0,0,0,0.55)] will-change-transform"
90
- style={{ transform: logoTransform, opacity: logoOpacity }}
91
- />
92
- ) : (
93
- <h1
94
- className="text-5xl sm:text-6xl font-bold tracking-tight drop-shadow-lg will-change-transform"
95
- style={{
96
- fontFamily: 'var(--pk-font-heading)',
97
- color: 'var(--pk-text)',
98
- transform: logoTransform,
99
- opacity: logoOpacity,
100
- }}
101
- >
102
- {identity.name}
103
- </h1>
104
- )}
105
- </div>
106
-
107
- {/* Scroll hint */}
108
- <div
109
- className="absolute bottom-4 left-1/2 -translate-x-1/2 animate-bounce"
110
- style={{ opacity: hintOpacity, color: 'var(--pk-text)' }}
111
- >
112
- <ChevronDown size={22} />
113
- </div>
114
- </div>
115
-
116
- {/* Below the hero (revealed on scroll): tagline, socials, nav */}
117
- {(identity.tagline || (section.showLocation && identity.location)) && (
118
- <div className="text-center mt-5 px-4">
119
- {identity.tagline && (
120
- <p className="text-base font-medium" style={{ color: 'var(--pk-text)' }}>
121
- {identity.tagline}
122
- </p>
123
- )}
124
- {section.showLocation && identity.location && (
125
- <p className="text-xs mt-1 opacity-60" style={{ color: 'var(--pk-muted)' }}>
126
- {identity.location}
127
- </p>
128
- )}
129
- </div>
130
- )}
131
-
132
- {/* Social + media-kit icon row */}
133
- <div className="flex flex-wrap items-center justify-center gap-3 mt-4 px-4">
134
- {socials.map((link) => (
135
- <a
136
- key={link.platform + link.url}
137
- href={link.url}
138
- target="_blank"
139
- rel="noopener noreferrer"
140
- aria-label={link.label ?? platformLabels[link.platform] ?? 'Link'}
141
- className="flex items-center justify-center w-11 h-11 rounded-full transition-transform hover:scale-110"
142
- style={{ backgroundColor: 'rgba(255,255,255,0.07)', color: 'var(--pk-text)' }}
143
- >
144
- <PlatformIcon platform={link.platform} size={19} />
145
- </a>
146
- ))}
147
- </div>
148
-
149
- {/* Quick-nav pills */}
150
- {section.navPills && section.navPills.length > 0 && (
151
- <nav
152
- className="flex gap-2 overflow-x-auto scrollbar-hide mt-4 px-4 pb-1 justify-start sm:justify-center"
153
- aria-label="Seções"
154
- >
155
- {section.navPills.map((pill) => (
156
- <button
157
- key={pill.targetId}
158
- type="button"
159
- onClick={() => scrollTo(pill.targetId)}
160
- className="whitespace-nowrap px-4 py-2 text-sm font-medium rounded-full transition-colors shrink-0"
161
- style={{
162
- backgroundColor: 'rgba(255,255,255,0.06)',
163
- color: 'var(--pk-text)',
164
- border: '1px solid rgba(255,255,255,0.08)',
165
- }}
166
- >
167
- {pill.label}
168
- </button>
169
- ))}
170
- </nav>
171
- )}
172
- </header>
173
- );
174
- }
@@ -1,158 +0,0 @@
1
- import type { ProjectSection as ProjectSectionType } from '../../types';
2
- import { renderMarkdown } from '../utils';
3
-
4
- interface Props {
5
- section: ProjectSectionType;
6
- }
7
-
8
- export default function ProjectSection({ section }: Props) {
9
- const isSideBySide = section.layout === 'side-by-side';
10
- const hasMedia = section.media && section.media.length > 0;
11
- const firstMedia = hasMedia ? section.media![0] : null;
12
- const isVideo = firstMedia?.type === 'video';
13
-
14
- const MediaBlock = () => {
15
- if (!firstMedia) return null;
16
- if (isVideo) {
17
- return (
18
- <div className="relative w-full" style={{ paddingBottom: '56.25%', borderRadius: 'var(--pk-radius)', overflow: 'hidden' }}>
19
- <iframe
20
- src={firstMedia.url}
21
- className="absolute inset-0 w-full h-full"
22
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
23
- allowFullScreen
24
- loading="lazy"
25
- title={firstMedia.alt ?? section.title ?? 'Video'}
26
- />
27
- </div>
28
- );
29
- }
30
- return (
31
- <img
32
- src={firstMedia.url}
33
- alt={firstMedia.alt ?? ''}
34
- className="w-full h-full object-cover"
35
- style={{ borderRadius: 'var(--pk-radius)' }}
36
- loading="lazy"
37
- />
38
- );
39
- };
40
-
41
- const ContentBlock = () => (
42
- <div className="flex flex-col justify-center">
43
- {section.label && (
44
- <span
45
- className="inline-block self-start px-4 py-1.5 text-xs font-bold uppercase tracking-[0.15em] mb-5"
46
- style={{
47
- backgroundColor: 'transparent',
48
- color: 'var(--pk-accent)',
49
- border: '1px solid var(--pk-accent)',
50
- borderRadius: '4px',
51
- }}
52
- >
53
- {section.label}
54
- </span>
55
- )}
56
-
57
- {section.title && (
58
- <h2
59
- className="text-3xl md:text-4xl lg:text-5xl font-bold mb-5"
60
- style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
61
- >
62
- {section.title}
63
- </h2>
64
- )}
65
-
66
- {section.content && (
67
- <div
68
- className="text-base md:text-lg leading-relaxed [&_p]:mb-4 [&_strong]:font-semibold [&_strong]:text-white [&_em]:italic"
69
- style={{ color: 'var(--pk-muted)' }}
70
- dangerouslySetInnerHTML={{ __html: renderMarkdown(section.content) }}
71
- />
72
- )}
73
-
74
- {section.tags && section.tags.length > 0 && (
75
- <div className="flex flex-wrap gap-2 mt-5">
76
- {section.tags.map((tag) => (
77
- <span
78
- key={tag}
79
- className="px-3 py-1 text-xs font-medium"
80
- style={{
81
- color: 'var(--pk-muted)',
82
- border: '1px solid rgba(255,255,255,0.1)',
83
- borderRadius: 'var(--pk-radius)',
84
- }}
85
- >
86
- {tag}
87
- </span>
88
- ))}
89
- </div>
90
- )}
91
-
92
- {section.links && section.links.length > 0 && (
93
- <div className="flex flex-wrap gap-4 mt-6">
94
- {section.links.map((link) => (
95
- <a
96
- key={link.url}
97
- href={link.url}
98
- target="_blank"
99
- rel="noopener noreferrer"
100
- className="text-sm font-medium underline underline-offset-4 hover:opacity-80 transition-opacity"
101
- style={{ color: 'var(--pk-primary)' }}
102
- >
103
- {link.label}
104
- </a>
105
- ))}
106
- </div>
107
- )}
108
- </div>
109
- );
110
-
111
- return (
112
- <section className="py-20 md:py-28 px-6" id={section.id}>
113
- <div className="max-w-5xl mx-auto">
114
- {isSideBySide && hasMedia ? (
115
- <div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
116
- <div className="order-1">
117
- <ContentBlock />
118
- </div>
119
- <div className="order-2">
120
- <MediaBlock />
121
- </div>
122
- </div>
123
- ) : (
124
- <div>
125
- <ContentBlock />
126
- {hasMedia && (
127
- <div className={`grid grid-cols-1 gap-4 mt-8 ${section.media!.length > 1 ? 'md:grid-cols-2' : ''}`}>
128
- {section.media!.map((item, i) =>
129
- item.type === 'video' ? (
130
- <div key={i} className="aspect-video overflow-hidden" style={{ borderRadius: 'var(--pk-radius)' }}>
131
- <iframe
132
- src={item.url}
133
- className="h-full w-full"
134
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
135
- allowFullScreen
136
- loading="lazy"
137
- title={item.alt ?? 'Video'}
138
- />
139
- </div>
140
- ) : (
141
- <img
142
- key={i}
143
- src={item.url}
144
- alt={item.alt ?? ''}
145
- className="w-full aspect-[4/3] object-cover"
146
- style={{ borderRadius: 'var(--pk-radius)' }}
147
- loading="lazy"
148
- />
149
- )
150
- )}
151
- </div>
152
- )}
153
- </div>
154
- )}
155
- </div>
156
- </section>
157
- );
158
- }
@@ -1,125 +0,0 @@
1
- import { ExternalLink } from 'lucide-react';
2
- import type { SocialLinksSection as SocialLinksSectionType } from '../../types';
3
- import { platformColors, platformLabels } from '../utils';
4
- import { PlatformIcon } from '../components/PlatformIcon';
5
-
6
- interface Props {
7
- section: SocialLinksSectionType;
8
- }
9
-
10
- export default function SocialLinksSection({ section }: Props) {
11
- const layout = section.layout ?? 'buttons';
12
-
13
- return (
14
- <section className="py-16 md:py-24 px-6" id={section.id}>
15
- <div className="max-w-xl mx-auto">
16
- {section.title && (
17
- <h2
18
- className="text-3xl md:text-4xl font-bold mb-10 text-center"
19
- style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
20
- >
21
- {section.title}
22
- </h2>
23
- )}
24
-
25
- {layout === 'icons' && (
26
- <div className="flex flex-wrap justify-center gap-5">
27
- {section.links.map((link) => {
28
- const color = platformColors[link.platform];
29
- return (
30
- <a
31
- key={link.platform + link.url}
32
- href={link.url}
33
- target="_blank"
34
- rel="noopener noreferrer"
35
- className="group flex items-center justify-center w-16 h-16 rounded-full transition-all duration-300 hover:scale-110 hover:shadow-lg"
36
- style={{
37
- backgroundColor: color + '15',
38
- boxShadow: `0 0 0 1px ${color}25`,
39
- }}
40
- title={link.label ?? platformLabels[link.platform]}
41
- >
42
- <PlatformIcon platform={link.platform} size={26} style={{ color }} />
43
- </a>
44
- );
45
- })}
46
- </div>
47
- )}
48
-
49
- {layout === 'buttons' && (
50
- <div className="flex flex-col gap-3">
51
- {section.links.map((link) => {
52
- const color = platformColors[link.platform];
53
- return (
54
- <a
55
- key={link.platform + link.url}
56
- href={link.url}
57
- target="_blank"
58
- rel="noopener noreferrer"
59
- className="group relative flex items-center gap-5 px-6 py-4 font-medium transition-all duration-300 hover:scale-[1.02] overflow-hidden"
60
- style={{
61
- backgroundColor: color + '10',
62
- color: 'var(--pk-text)',
63
- borderRadius: 'var(--pk-radius)',
64
- border: `1px solid ${color}30`,
65
- }}
66
- >
67
- {/* Hover glow */}
68
- <div
69
- className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
70
- style={{ background: `linear-gradient(135deg, ${color}18, transparent 60%)` }}
71
- />
72
-
73
- <div className="relative z-10 flex items-center justify-center w-10 h-10 rounded-full shrink-0"
74
- style={{ backgroundColor: color + '20' }}
75
- >
76
- <PlatformIcon platform={link.platform} size={20} style={{ color }} />
77
- </div>
78
-
79
- <span className="relative z-10 text-base">{link.label ?? platformLabels[link.platform]}</span>
80
-
81
- <ExternalLink
82
- size={16}
83
- className="relative z-10 ml-auto opacity-0 group-hover:opacity-50 transition-opacity duration-300"
84
- style={{ color: 'var(--pk-text)' }}
85
- />
86
- </a>
87
- );
88
- })}
89
- </div>
90
- )}
91
-
92
- {layout === 'list' && (
93
- <div className="space-y-1">
94
- {section.links.map((link) => {
95
- const color = platformColors[link.platform];
96
- return (
97
- <a
98
- key={link.platform + link.url}
99
- href={link.url}
100
- target="_blank"
101
- rel="noopener noreferrer"
102
- className="group flex items-center gap-4 py-4 px-3 transition-all duration-200 hover:bg-white/5 rounded-lg"
103
- >
104
- <div className="flex items-center justify-center w-9 h-9 rounded-full shrink-0"
105
- style={{ backgroundColor: color + '18' }}
106
- >
107
- <PlatformIcon platform={link.platform} size={18} style={{ color }} />
108
- </div>
109
- <span className="text-sm font-medium" style={{ color: 'var(--pk-text)' }}>
110
- {link.label ?? platformLabels[link.platform]}
111
- </span>
112
- <ExternalLink
113
- size={14}
114
- className="ml-auto opacity-0 group-hover:opacity-40 transition-opacity"
115
- style={{ color: 'var(--pk-text)' }}
116
- />
117
- </a>
118
- );
119
- })}
120
- </div>
121
- )}
122
- </div>
123
- </section>
124
- );
125
- }
@@ -1,46 +0,0 @@
1
- import type { StatsSection as StatsSectionType } from '../../types';
2
-
3
- interface Props {
4
- section: StatsSectionType;
5
- }
6
-
7
- export default function StatsSection({ section }: Props) {
8
- const isGrid = section.layout === 'grid';
9
-
10
- return (
11
- <section className="py-16 md:py-24 px-6" id={section.id}>
12
- <div className="max-w-4xl mx-auto">
13
- {section.title && (
14
- <h2
15
- className="text-3xl md:text-4xl font-bold mb-8 text-center"
16
- style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
17
- >
18
- {section.title}
19
- </h2>
20
- )}
21
-
22
- <div
23
- className={
24
- isGrid
25
- ? 'grid grid-cols-2 md:grid-cols-4 gap-6'
26
- : 'flex flex-wrap justify-center gap-8 md:gap-12'
27
- }
28
- >
29
- {section.stats.map((stat, i) => (
30
- <div key={i} className="text-center">
31
- <div
32
- className="text-3xl md:text-4xl font-bold mb-1"
33
- style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-primary)' }}
34
- >
35
- {stat.value}
36
- </div>
37
- <div className="text-sm" style={{ color: 'var(--pk-muted)' }}>
38
- {stat.label}
39
- </div>
40
- </div>
41
- ))}
42
- </div>
43
- </div>
44
- </section>
45
- );
46
- }
@@ -1,29 +0,0 @@
1
- import type { TextSection as TextSectionType } from '../../types';
2
- import { renderMarkdown } from '../utils';
3
-
4
- interface Props {
5
- section: TextSectionType;
6
- }
7
-
8
- export default function TextSection({ section }: Props) {
9
- return (
10
- <section className="py-16 md:py-24 px-6" id={section.id}>
11
- <div className="max-w-3xl mx-auto">
12
- {section.title && (
13
- <h2
14
- className="text-3xl md:text-4xl font-bold mb-8"
15
- style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
16
- >
17
- {section.title}
18
- </h2>
19
- )}
20
-
21
- <div
22
- className="text-base md:text-lg leading-relaxed [&_p]:mb-4 [&_strong]:font-semibold [&_em]:italic [&_a]:underline [&_a]:decoration-[var(--pk-primary)]"
23
- style={{ color: 'var(--pk-muted)' }}
24
- dangerouslySetInnerHTML={{ __html: renderMarkdown(section.content) }}
25
- />
26
- </div>
27
- </section>
28
- );
29
- }
@@ -1,83 +0,0 @@
1
- import { MapPin } from 'lucide-react';
2
- import type { TourDatesSection, TourDateItem } from '../../types';
3
-
4
- interface Props {
5
- section: TourDatesSection;
6
- }
7
-
8
- const MONTHS_PT = ['JAN', 'FEV', 'MAR', 'ABR', 'MAI', 'JUN', 'JUL', 'AGO', 'SET', 'OUT', 'NOV', 'DEZ'];
9
-
10
- function parseDate(date: string): { month: string; day: string; year: string } {
11
- const m = date.match(/^(\d{4})-(\d{2})-(\d{2})/);
12
- if (m) {
13
- const monthIdx = Math.min(11, Math.max(0, parseInt(m[2], 10) - 1));
14
- return { month: MONTHS_PT[monthIdx], day: String(parseInt(m[3], 10)), year: m[1] };
15
- }
16
- return { month: '', day: date, year: '' };
17
- }
18
-
19
- function DateRow({ item, cta }: { item: TourDateItem; cta: string }) {
20
- const { month, day, year } = parseDate(item.date);
21
- const hasLink = !!item.url;
22
- const label = hasLink ? cta : item.soldOut ? 'Esgotado' : 'Em breve';
23
- const Tag = hasLink ? 'a' : 'div';
24
- return (
25
- <Tag
26
- {...(hasLink ? { href: item.url, target: '_blank', rel: 'noopener noreferrer' } : {})}
27
- className={`flex items-center gap-4 p-3 transition-all duration-200 ${hasLink ? 'hover:scale-[1.01]' : 'cursor-default'}`}
28
- style={{
29
- backgroundColor: 'rgba(255,255,255,0.04)',
30
- border: '1px solid rgba(255,255,255,0.07)',
31
- borderRadius: 'var(--pk-radius)',
32
- }}
33
- >
34
- <div
35
- className="shrink-0 flex flex-col items-center justify-center w-14 h-16"
36
- style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 'calc(var(--pk-radius) - 4px)' }}
37
- >
38
- <span className="text-[10px] font-bold tracking-wider" style={{ color: 'var(--pk-primary)' }}>{month}</span>
39
- <span className="text-xl font-bold leading-none" style={{ color: 'var(--pk-text)', fontFamily: 'var(--pk-font-heading)' }}>{day}</span>
40
- <span className="text-[10px] opacity-60" style={{ color: 'var(--pk-muted)' }}>{year}</span>
41
- </div>
42
- <div className="min-w-0 flex-1">
43
- <p className="text-sm font-semibold truncate" style={{ color: 'var(--pk-text)' }}>{item.venue}</p>
44
- {item.city && (
45
- <p className="text-xs truncate flex items-center gap-1 mt-0.5" style={{ color: 'var(--pk-muted)' }}>
46
- <MapPin size={11} /> {item.city}
47
- </p>
48
- )}
49
- </div>
50
- <span
51
- className="shrink-0 px-4 py-1.5 text-xs font-semibold rounded-full"
52
- style={
53
- hasLink
54
- ? { backgroundColor: 'var(--pk-primary)', color: 'var(--pk-bg)' }
55
- : { backgroundColor: 'transparent', color: 'var(--pk-muted)', border: '1px solid rgba(255,255,255,0.12)' }
56
- }
57
- >
58
- {label}
59
- </span>
60
- </Tag>
61
- );
62
- }
63
-
64
- export default function TourDatesBlock({ section }: Props) {
65
- const cta = section.ctaLabel ?? 'Ingressos';
66
- return (
67
- <section className="py-6" id={section.id}>
68
- {section.title && (
69
- <h2
70
- className="text-xl md:text-2xl font-bold mb-4 px-1"
71
- style={{ fontFamily: 'var(--pk-font-heading)', color: 'var(--pk-text)' }}
72
- >
73
- {section.title}
74
- </h2>
75
- )}
76
- <div className="flex flex-col gap-3">
77
- {section.dates.map((item, i) => (
78
- <DateRow key={item.date + item.venue + i} item={item} cta={cta} />
79
- ))}
80
- </div>
81
- </section>
82
- );
83
- }