@tanstack/cta-framework-react-cra 0.44.3 → 0.45.0

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 (97) hide show
  1. package/{examples/tanchat/assets/src/components/example-AIAssistant.tsx → add-ons/ai/assets/src/components/demo-AIAssistant.tsx} +6 -8
  2. package/{examples/tanchat/assets/src/components/example-GuitarRecommendation.tsx → add-ons/ai/assets/src/components/demo-GuitarRecommendation.tsx} +2 -2
  3. package/{examples/tanchat/assets/src/lib/example.ai-hook.ts → add-ons/ai/assets/src/lib/demo-ai-hook.ts} +9 -9
  4. package/{examples/tanchat/assets/src/lib/example.guitar-tools.ts → add-ons/ai/assets/src/lib/demo-guitar-tools.ts} +1 -1
  5. package/{examples/tanchat/assets/src/routes/demo/tanchat.tsx → add-ons/ai/assets/src/routes/demo/ai-chat.tsx} +28 -148
  6. package/{examples/tanchat/assets/src/routes/demo/image.tsx → add-ons/ai/assets/src/routes/demo/ai-image.tsx} +2 -50
  7. package/add-ons/ai/assets/src/routes/demo/ai-structured.tsx +310 -0
  8. package/{examples/tanchat/assets/src/routes/demo/api.tanchat.ts → add-ons/ai/assets/src/routes/demo/api.ai.chat.ts} +16 -6
  9. package/{examples/tanchat/assets/src/routes/demo/api.image.ts → add-ons/ai/assets/src/routes/demo/api.ai.image.ts} +3 -5
  10. package/add-ons/ai/assets/src/routes/demo/api.ai.structured.ts +136 -0
  11. package/add-ons/ai/assets/src/routes/demo/api.ai.transcription.ts +89 -0
  12. package/{examples/tanchat/assets/src/routes/demo/api.tts.ts → add-ons/ai/assets/src/routes/demo/api.ai.tts.ts} +1 -1
  13. package/{examples/tanchat/assets/src/routes/example.guitars → add-ons/ai/assets/src/routes/demo/guitars}/$guitarId.tsx +3 -2
  14. package/{examples/tanchat/assets/src/routes/example.guitars → add-ons/ai/assets/src/routes/demo/guitars}/index.tsx +3 -2
  15. package/add-ons/ai/info.json +46 -0
  16. package/{examples/tanchat → add-ons/ai}/package.json +1 -1
  17. package/examples/events/README.md +110 -0
  18. package/examples/events/assets/content/speakers/andre-costa.md +22 -0
  19. package/examples/events/assets/content/speakers/hans-mueller.md +22 -0
  20. package/examples/events/assets/content/speakers/isabella-martinez.md +22 -0
  21. package/examples/events/assets/content/speakers/kenji-nakamura.md +22 -0
  22. package/examples/events/assets/content/speakers/marie-dubois.md +20 -0
  23. package/examples/events/assets/content/speakers/priya-sharma.md +22 -0
  24. package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
  25. package/examples/events/assets/content/talks/french-macaron-mastery.md +39 -0
  26. package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md +39 -0
  27. package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md +39 -0
  28. package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md +36 -0
  29. package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md +32 -0
  30. package/examples/events/assets/content/talks/the-science-of-sugar.md +39 -0
  31. package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md +39 -0
  32. package/examples/events/assets/content-collections.ts +56 -0
  33. package/examples/events/assets/public/background-1.jpg +0 -0
  34. package/examples/events/assets/public/background-2.jpg +0 -0
  35. package/examples/events/assets/public/background-3.jpg +0 -0
  36. package/examples/events/assets/public/background-4.jpg +0 -0
  37. package/examples/events/assets/public/conference-logo.png +0 -0
  38. package/examples/events/assets/public/favicon.ico +0 -0
  39. package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
  40. package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
  41. package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
  42. package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
  43. package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
  44. package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
  45. package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
  46. package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
  47. package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
  48. package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
  49. package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
  50. package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
  51. package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
  52. package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
  53. package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
  54. package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
  55. package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
  56. package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
  57. package/examples/events/assets/src/components/RemyButton.tsx +18 -0
  58. package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
  59. package/examples/events/assets/src/components/TalkCard.tsx +77 -0
  60. package/examples/events/assets/src/components/ui/card.tsx +92 -0
  61. package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
  62. package/examples/events/assets/src/lib/conference-tools.ts +210 -0
  63. package/examples/events/assets/src/lib/utils.ts +6 -0
  64. package/examples/events/assets/src/routes/api.remy-chat.ts +121 -0
  65. package/examples/events/assets/src/routes/index.tsx +192 -0
  66. package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
  67. package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
  68. package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
  69. package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
  70. package/examples/events/assets/src/routes/talks.index.tsx +40 -0
  71. package/examples/events/assets/src/styles.css +194 -0
  72. package/examples/events/info.json +63 -0
  73. package/examples/events/package.json +23 -0
  74. package/package.json +2 -2
  75. package/examples/tanchat/assets/src/lib/model-selection.ts +0 -78
  76. package/examples/tanchat/assets/src/lib/vendor-capabilities.ts +0 -55
  77. package/examples/tanchat/assets/src/routes/demo/api.available-providers.ts +0 -35
  78. package/examples/tanchat/assets/src/routes/demo/api.structured.ts +0 -168
  79. package/examples/tanchat/assets/src/routes/demo/api.transcription.ts +0 -89
  80. package/examples/tanchat/assets/src/routes/demo/structured.tsx +0 -460
  81. package/examples/tanchat/info.json +0 -46
  82. /package/{examples/tanchat → add-ons/ai}/README.md +0 -0
  83. /package/{examples/tanchat → add-ons/ai}/assets/_dot_env.local.append +0 -0
  84. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-flowers.jpg +0 -0
  85. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-motherboard.jpg +0 -0
  86. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-racing.jpg +0 -0
  87. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-steamer-trunk.jpg +0 -0
  88. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-superhero.jpg +0 -0
  89. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-traveling.jpg +0 -0
  90. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-video-games.jpg +0 -0
  91. /package/{examples/tanchat → add-ons/ai}/assets/public/example-ukelele-tanstack.jpg +0 -0
  92. /package/{examples/tanchat/assets/src/data/example-guitars.ts → add-ons/ai/assets/src/data/demo-guitars.ts} +0 -0
  93. /package/{examples/tanchat/assets/src/hooks/useAudioRecorder.ts → add-ons/ai/assets/src/hooks/demo-useAudioRecorder.ts} +0 -0
  94. /package/{examples/tanchat/assets/src/hooks/useTTS.ts → add-ons/ai/assets/src/hooks/demo-useTTS.ts} +0 -0
  95. /package/{examples/tanchat → add-ons/ai}/assets/src/lib/ai-devtools.tsx +0 -0
  96. /package/{examples/tanchat/assets/src/routes/demo/tanchat.css → add-ons/ai/assets/src/routes/demo/ai-chat.css} +0 -0
  97. /package/{examples/tanchat → add-ons/ai}/small-logo.svg +0 -0
@@ -0,0 +1,274 @@
1
+ import { createFileRoute, Link } from '@tanstack/react-router'
2
+ import { Clock, Calendar, MapPin, ChevronRight } from 'lucide-react'
3
+ import { useState } from 'react'
4
+
5
+ import { allTalks, allSpeakers } from 'content-collections'
6
+
7
+ import RemyAssistant from '@/components/RemyAssistant'
8
+
9
+ export const Route = createFileRoute('/schedule/')({
10
+ component: SchedulePage,
11
+ })
12
+
13
+ // Helper to get speaker data by name
14
+ function getSpeakerByName(name: string) {
15
+ return allSpeakers.find(
16
+ (s) => s.name.toLowerCase() === name.toLowerCase()
17
+ )
18
+ }
19
+
20
+ // Define the conference schedule with time slots
21
+ const scheduleData = [
22
+ {
23
+ day: 1,
24
+ date: 'March 15, 2026',
25
+ dayName: 'Day One',
26
+ theme: 'French Foundations',
27
+ sessions: [
28
+ { time: '9:00 AM', talkSlug: 'french-macaron-mastery' },
29
+ { time: '11:30 AM', talkSlug: 'croissant-lamination-secrets' },
30
+ { time: '3:00 PM', talkSlug: 'the-science-of-sugar' },
31
+ ],
32
+ },
33
+ {
34
+ day: 2,
35
+ date: 'March 16, 2026',
36
+ dayName: 'Day Two',
37
+ theme: 'Global Traditions',
38
+ sessions: [
39
+ { time: '9:00 AM', talkSlug: 'sourdough-from-starter-to-masterpiece' },
40
+ { time: '11:30 AM', talkSlug: 'umami-in-pastry-east-meets-west' },
41
+ { time: '2:30 PM', talkSlug: 'savory-breads-of-the-mediterranean' },
42
+ ],
43
+ },
44
+ {
45
+ day: 3,
46
+ date: 'March 17, 2026',
47
+ dayName: 'Day Three',
48
+ theme: 'Artisan Mastery',
49
+ sessions: [
50
+ { time: '9:00 AM', talkSlug: 'the-art-of-the-perfect-tart' },
51
+ { time: '11:00 AM', talkSlug: 'neapolitan-pizza-tradition-meets-innovation' },
52
+ ],
53
+ },
54
+ ]
55
+
56
+ function SchedulePage() {
57
+ const [selectedDay, setSelectedDay] = useState(1)
58
+
59
+ const currentDayData = scheduleData.find((d) => d.day === selectedDay)!
60
+
61
+ return (
62
+ <>
63
+ <RemyAssistant />
64
+ <div className="min-h-screen">
65
+ {/* Hero section */}
66
+ <div className="relative py-16 px-6">
67
+ <div className="max-w-7xl mx-auto text-center">
68
+ <div className="inline-flex items-center gap-2 px-4 py-2 mb-6 rounded-full bg-copper/10 border border-copper/30 text-copper-light text-sm font-medium">
69
+ <Calendar className="w-4 h-4" />
70
+ <span>March 15-17, 2026</span>
71
+ <span className="mx-2 text-copper/40">•</span>
72
+ <MapPin className="w-4 h-4" />
73
+ <span>Paris, France</span>
74
+ </div>
75
+ <h1 className="font-display text-5xl md:text-6xl font-bold text-cream mb-4">
76
+ Conference <span className="text-gold italic">Schedule</span>
77
+ </h1>
78
+ <p className="text-xl text-cream/70 max-w-2xl mx-auto font-body">
79
+ Three days of masterclasses, demonstrations, and culinary inspiration from the world's finest pastry artisans.
80
+ </p>
81
+ </div>
82
+ </div>
83
+
84
+ {/* Day selector tabs */}
85
+ <div className="max-w-7xl mx-auto px-6 mb-12">
86
+ <div className="flex justify-center">
87
+ <div className="inline-flex bg-card/50 rounded-2xl p-2 border border-border/50">
88
+ {scheduleData.map((day) => (
89
+ <button
90
+ key={day.day}
91
+ onClick={() => setSelectedDay(day.day)}
92
+ className={`relative px-8 py-4 rounded-xl font-display font-semibold transition-all duration-300 ${
93
+ selectedDay === day.day
94
+ ? 'bg-gradient-to-br from-copper to-copper-dark text-charcoal shadow-lg shadow-copper/20'
95
+ : 'text-cream/70 hover:text-cream hover:bg-card'
96
+ }`}
97
+ >
98
+ <span className="block text-xs uppercase tracking-wider opacity-75">
99
+ {day.dayName}
100
+ </span>
101
+ <span className="block text-lg">{day.date.split(',')[0].split(' ').slice(0, 2).join(' ')}</span>
102
+ </button>
103
+ ))}
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ {/* Day theme header */}
109
+ <div className="max-w-7xl mx-auto px-6 mb-8">
110
+ <div className="text-center">
111
+ <h2 className="font-display text-3xl font-bold text-cream mb-2">
112
+ {currentDayData.dayName}: <span className="text-gold italic">{currentDayData.theme}</span>
113
+ </h2>
114
+ <p className="text-cream/50 font-body">{currentDayData.date}</p>
115
+ </div>
116
+ </div>
117
+
118
+ {/* Schedule timeline */}
119
+ <div className="max-w-5xl mx-auto px-6 pb-20">
120
+ <div className="relative">
121
+ {/* Timeline line */}
122
+ <div className="absolute left-8 md:left-12 top-0 bottom-0 w-px bg-gradient-to-b from-copper via-gold to-copper/30" />
123
+
124
+ {/* Sessions */}
125
+ <div className="space-y-8">
126
+ {currentDayData.sessions.map((session, index) => {
127
+ const talk = allTalks.find((t) => t.slug === session.talkSlug)
128
+ if (!talk) return null
129
+
130
+ const speaker = getSpeakerByName(talk.speaker)
131
+
132
+ return (
133
+ <Link
134
+ key={session.talkSlug}
135
+ to={`/talks/${talk.slug}`}
136
+ className="group block"
137
+ >
138
+ <div className="relative flex gap-6 md:gap-10">
139
+ {/* Time marker */}
140
+ <div className="flex-shrink-0 w-16 md:w-24 pt-6">
141
+ <div className="relative">
142
+ {/* Timeline dot */}
143
+ <div className="absolute -right-[13px] md:-right-[17px] top-0 w-6 h-6 rounded-full bg-charcoal border-2 border-gold flex items-center justify-center group-hover:border-copper group-hover:scale-110 transition-all">
144
+ <div className="w-2 h-2 rounded-full bg-gold group-hover:bg-copper transition-colors" />
145
+ </div>
146
+ <span className="block text-right text-sm md:text-base font-display font-semibold text-copper-light">
147
+ {session.time}
148
+ </span>
149
+ </div>
150
+ </div>
151
+
152
+ {/* Session card */}
153
+ <div
154
+ className="flex-1 relative overflow-hidden rounded-2xl bg-card border border-border/50
155
+ group-hover:border-gold/50 transition-all duration-300 group-hover:shadow-xl group-hover:shadow-gold/5
156
+ group-hover:-translate-y-1"
157
+ style={{
158
+ animationDelay: `${index * 100}ms`,
159
+ }}
160
+ >
161
+ {/* Background image with overlay */}
162
+ <div className="absolute inset-0">
163
+ <img
164
+ src={`/${talk.image}`}
165
+ alt={talk.title}
166
+ className="w-full h-full object-cover opacity-30 group-hover:opacity-40 group-hover:scale-105 transition-all duration-500"
167
+ />
168
+ <div className="absolute inset-0 bg-gradient-to-r from-charcoal via-charcoal/95 to-charcoal/80" />
169
+ </div>
170
+
171
+ {/* Content */}
172
+ <div className="relative p-6 md:p-8 flex flex-col md:flex-row gap-6 items-start">
173
+ {/* Speaker image */}
174
+ {speaker && (
175
+ <div className="flex-shrink-0">
176
+ <div className="relative w-20 h-20 md:w-24 md:h-24 rounded-xl overflow-hidden border-2 border-gold/30 group-hover:border-gold/60 transition-colors shadow-lg">
177
+ <img
178
+ src={`/${speaker.headshot}`}
179
+ alt={speaker.name}
180
+ className="w-full h-full object-cover"
181
+ />
182
+ <div className="absolute inset-0 bg-gradient-to-t from-charcoal/40 to-transparent" />
183
+ </div>
184
+ </div>
185
+ )}
186
+
187
+ {/* Talk info */}
188
+ <div className="flex-1 min-w-0">
189
+ {/* Topics */}
190
+ <div className="flex flex-wrap gap-2 mb-3">
191
+ {talk.topics.slice(0, 3).map((topic) => (
192
+ <span
193
+ key={topic}
194
+ className="px-2.5 py-0.5 text-xs font-medium tracking-wide uppercase bg-gold/10 text-gold border border-gold/20 rounded-full"
195
+ >
196
+ {topic}
197
+ </span>
198
+ ))}
199
+ </div>
200
+
201
+ {/* Title */}
202
+ <h3 className="font-display text-xl md:text-2xl font-semibold text-cream group-hover:text-gold transition-colors mb-2 leading-tight">
203
+ {talk.title}
204
+ </h3>
205
+
206
+ {/* Speaker & Duration */}
207
+ <div className="flex flex-wrap items-center gap-4 text-cream/60 text-sm mb-3">
208
+ <span className="font-medium text-copper-light">
209
+ {talk.speaker}
210
+ </span>
211
+ <div className="flex items-center gap-1.5">
212
+ <Clock className="w-3.5 h-3.5" />
213
+ <span>{talk.duration}</span>
214
+ </div>
215
+ </div>
216
+
217
+ {/* Speaker title if available */}
218
+ {speaker && (
219
+ <p className="text-cream/50 text-sm font-body">
220
+ {speaker.title} at {speaker.restaurant}
221
+ </p>
222
+ )}
223
+ </div>
224
+
225
+ {/* Arrow indicator */}
226
+ <div className="flex-shrink-0 self-center">
227
+ <div className="w-10 h-10 rounded-full bg-gold/10 border border-gold/20 flex items-center justify-center group-hover:bg-gold/20 group-hover:border-gold/40 transition-all">
228
+ <ChevronRight className="w-5 h-5 text-gold group-hover:translate-x-0.5 transition-transform" />
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ </Link>
235
+ )
236
+ })}
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ {/* Bottom CTA */}
242
+ <div className="max-w-4xl mx-auto px-6 pb-20">
243
+ <div className="relative p-8 md:p-12 rounded-3xl bg-gradient-to-br from-card to-charcoal border border-border/50 overflow-hidden text-center">
244
+ <div className="absolute top-0 right-0 w-64 h-64 bg-copper/5 rounded-full blur-3xl" />
245
+ <div className="absolute bottom-0 left-0 w-48 h-48 bg-gold/5 rounded-full blur-3xl" />
246
+
247
+ <div className="relative">
248
+ <h3 className="font-display text-2xl md:text-3xl font-bold text-cream mb-3">
249
+ Don't Miss a Single Session
250
+ </h3>
251
+ <p className="text-cream/60 font-body mb-6 max-w-xl mx-auto">
252
+ Each masterclass offers hands-on learning from the world's finest pastry artisans.
253
+ </p>
254
+ <div className="flex flex-wrap justify-center gap-4">
255
+ <Link
256
+ to="/talks"
257
+ className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-gradient-to-r from-copper to-copper-dark text-charcoal font-semibold transition-all hover:shadow-lg hover:shadow-copper/30 hover:scale-[1.02]"
258
+ >
259
+ Browse All Sessions
260
+ </Link>
261
+ <Link
262
+ to="/speakers"
263
+ className="inline-flex items-center gap-2 px-6 py-3 rounded-full border-2 border-gold/50 text-gold font-semibold transition-all hover:bg-gold/10 hover:border-gold"
264
+ >
265
+ Meet the Speakers
266
+ </Link>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ </>
273
+ )
274
+ }
@@ -0,0 +1,122 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { marked } from 'marked'
3
+ import { MapPin, Award, ArrowLeft } from 'lucide-react'
4
+ import { Link } from '@tanstack/react-router'
5
+
6
+ import { allSpeakers, allTalks } from 'content-collections'
7
+
8
+ import RemyAssistant from '@/components/RemyAssistant'
9
+ import TalkCard from '@/components/TalkCard'
10
+
11
+ export const Route = createFileRoute('/speakers/$slug')({
12
+ loader: async ({ params }) => {
13
+ const speaker = allSpeakers.find((s) => s.slug === params.slug)
14
+ if (!speaker) {
15
+ throw new Error('Speaker not found')
16
+ }
17
+ const speakerTalks = allTalks.filter((t) => t.speaker === speaker.name)
18
+ return { speaker, speakerTalks }
19
+ },
20
+ component: SpeakerDetailPage,
21
+ })
22
+
23
+ function SpeakerDetailPage() {
24
+ const { speaker, speakerTalks } = Route.useLoaderData()
25
+
26
+ return (
27
+ <div className="min-h-screen">
28
+ <RemyAssistant />
29
+
30
+ {/* Back navigation */}
31
+ <div className="max-w-7xl mx-auto px-6 py-4">
32
+ <Link
33
+ to="/speakers"
34
+ className="inline-flex items-center gap-2 text-cream/60 hover:text-gold transition-colors"
35
+ >
36
+ <ArrowLeft className="w-4 h-4" />
37
+ <span>All Speakers</span>
38
+ </Link>
39
+ </div>
40
+
41
+ {/* Hero section */}
42
+ <div className="relative py-12 px-6">
43
+ <div className="max-w-7xl mx-auto">
44
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
45
+ {/* Photo */}
46
+ <div className="lg:col-span-1">
47
+ <div className="aspect-square rounded-2xl overflow-hidden border border-border/50">
48
+ <img
49
+ src={`/${speaker.headshot}`}
50
+ alt={speaker.name}
51
+ className="w-full h-full object-cover"
52
+ />
53
+ </div>
54
+ </div>
55
+
56
+ {/* Info */}
57
+ <div className="lg:col-span-2 flex flex-col justify-center">
58
+ {/* Specialty tag */}
59
+ <span className="inline-block w-fit px-4 py-1.5 text-sm font-medium tracking-wider uppercase bg-copper/20 text-copper-light rounded-full border border-copper/30 mb-4">
60
+ {speaker.specialty}
61
+ </span>
62
+
63
+ <h1 className="font-display text-5xl md:text-6xl font-bold text-cream mb-3">
64
+ {speaker.name}
65
+ </h1>
66
+
67
+ <p className="text-2xl text-gold font-display italic mb-4">
68
+ {speaker.title}
69
+ </p>
70
+
71
+ <div className="flex items-center gap-2 text-cream/60 text-lg mb-8">
72
+ <MapPin className="w-5 h-5 text-copper" />
73
+ <span>{speaker.restaurant}, {speaker.location}</span>
74
+ </div>
75
+
76
+ {/* Awards */}
77
+ {speaker.awards && speaker.awards.length > 0 && (
78
+ <div className="space-y-3">
79
+ <h3 className="text-sm font-medium tracking-wider uppercase text-cream/50">Awards & Recognition</h3>
80
+ <div className="flex flex-wrap gap-2">
81
+ {speaker.awards.map((award) => (
82
+ <span
83
+ key={award}
84
+ className="inline-flex items-center gap-2 px-3 py-1.5 text-sm bg-gold/10 text-gold/90 rounded-lg border border-gold/20"
85
+ >
86
+ <Award className="w-3.5 h-3.5" />
87
+ {award}
88
+ </span>
89
+ ))}
90
+ </div>
91
+ </div>
92
+ )}
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ {/* Bio section */}
99
+ <div className="max-w-4xl mx-auto px-6 py-12">
100
+ <div className="prose prose-lg max-w-none prose-invert prose-p:text-cream/80 prose-headings:text-cream prose-headings:font-display prose-strong:text-cream prose-a:text-gold font-body text-lg leading-relaxed">
101
+ <div
102
+ dangerouslySetInnerHTML={{ __html: marked(speaker.content) }}
103
+ />
104
+ </div>
105
+ </div>
106
+
107
+ {/* Speaker's talks */}
108
+ {speakerTalks.length > 0 && (
109
+ <div className="max-w-7xl mx-auto px-6 py-12">
110
+ <h2 className="font-display text-3xl font-bold text-cream mb-8">
111
+ Sessions by {speaker.name}
112
+ </h2>
113
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
114
+ {speakerTalks.map((talk) => (
115
+ <TalkCard key={talk.slug} talk={talk} />
116
+ ))}
117
+ </div>
118
+ </div>
119
+ )}
120
+ </div>
121
+ )
122
+ }
@@ -0,0 +1,40 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { allSpeakers } from 'content-collections'
4
+
5
+ import SpeakerCard from '@/components/SpeakerCard'
6
+ import RemyAssistant from '@/components/RemyAssistant'
7
+
8
+ export const Route = createFileRoute('/speakers/')({
9
+ component: SpeakersPage,
10
+ })
11
+
12
+ function SpeakersPage() {
13
+ return (
14
+ <>
15
+ <RemyAssistant />
16
+ <div className="min-h-screen">
17
+ {/* Hero section */}
18
+ <div className="relative py-16 px-6">
19
+ <div className="max-w-7xl mx-auto text-center">
20
+ <h1 className="font-display text-5xl md:text-6xl font-bold text-cream mb-4">
21
+ Our <span className="text-gold italic">Distinguished</span> Speakers
22
+ </h1>
23
+ <p className="text-xl text-cream/70 max-w-2xl mx-auto font-body">
24
+ Meet the world-renowned pastry chefs and master bakers who will share their expertise at Haute Pâtisserie 2026.
25
+ </p>
26
+ </div>
27
+ </div>
28
+
29
+ {/* Speakers grid */}
30
+ <div className="max-w-7xl mx-auto px-6 pb-20">
31
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
32
+ {allSpeakers.map((speaker) => (
33
+ <SpeakerCard key={speaker.slug} speaker={speaker} />
34
+ ))}
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </>
39
+ )
40
+ }
@@ -0,0 +1,116 @@
1
+ import { createFileRoute, Link } from '@tanstack/react-router'
2
+ import { marked } from 'marked'
3
+ import { Clock, User, ArrowLeft, Tag } from 'lucide-react'
4
+
5
+ import { allTalks, allSpeakers } from 'content-collections'
6
+
7
+ import RemyAssistant from '@/components/RemyAssistant'
8
+
9
+ export const Route = createFileRoute('/talks/$slug')({
10
+ loader: async ({ params }) => {
11
+ const talk = allTalks.find((t) => t.slug === params.slug)
12
+ if (!talk) {
13
+ throw new Error('Talk not found')
14
+ }
15
+ const speaker = allSpeakers.find((s) => s.name === talk.speaker)
16
+ return { talk, speaker }
17
+ },
18
+ component: TalkDetailPage,
19
+ })
20
+
21
+ function TalkDetailPage() {
22
+ const { talk, speaker } = Route.useLoaderData()
23
+
24
+ return (
25
+ <div className="min-h-screen">
26
+ <RemyAssistant />
27
+
28
+ {/* Back navigation */}
29
+ <div className="max-w-7xl mx-auto px-6 py-4">
30
+ <Link
31
+ to="/talks"
32
+ className="inline-flex items-center gap-2 text-cream/60 hover:text-gold transition-colors"
33
+ >
34
+ <ArrowLeft className="w-4 h-4" />
35
+ <span>All Sessions</span>
36
+ </Link>
37
+ </div>
38
+
39
+ {/* Hero image */}
40
+ <div className="relative h-[40vh] max-w-7xl mx-auto px-6 mb-8">
41
+ <div className="w-full h-full rounded-2xl overflow-hidden border border-border/50">
42
+ <img
43
+ src={`/${talk.image}`}
44
+ alt={talk.title}
45
+ className="w-full h-full object-cover"
46
+ />
47
+ </div>
48
+ <div className="absolute inset-6 bg-gradient-to-t from-charcoal/60 to-transparent rounded-2xl pointer-events-none" />
49
+ </div>
50
+
51
+ {/* Content */}
52
+ <div className="max-w-4xl mx-auto px-6">
53
+ {/* Topics */}
54
+ <div className="flex flex-wrap gap-2 mb-6">
55
+ {talk.topics.map((topic) => (
56
+ <span
57
+ key={topic}
58
+ className="inline-flex items-center gap-1.5 px-3 py-1 text-sm font-medium tracking-wide uppercase bg-gold/15 text-gold border border-gold/30 rounded-full"
59
+ >
60
+ <Tag className="w-3 h-3" />
61
+ {topic}
62
+ </span>
63
+ ))}
64
+ </div>
65
+
66
+ {/* Title */}
67
+ <h1 className="font-display text-4xl md:text-5xl font-bold text-cream mb-6 leading-tight">
68
+ {talk.title}
69
+ </h1>
70
+
71
+ {/* Meta info */}
72
+ <div className="flex flex-wrap items-center gap-6 mb-10 pb-10 border-b border-border/50">
73
+ {/* Speaker link */}
74
+ {speaker ? (
75
+ <Link
76
+ to={`/speakers/${speaker.slug}`}
77
+ className="flex items-center gap-3 group"
78
+ >
79
+ <div className="w-12 h-12 rounded-full overflow-hidden border border-border/50">
80
+ <img
81
+ src={`/${speaker.headshot}`}
82
+ alt={speaker.name}
83
+ className="w-full h-full object-cover"
84
+ />
85
+ </div>
86
+ <div>
87
+ <p className="text-cream group-hover:text-gold transition-colors font-medium">
88
+ {talk.speaker}
89
+ </p>
90
+ <p className="text-cream/50 text-sm">{speaker.restaurant}</p>
91
+ </div>
92
+ </Link>
93
+ ) : (
94
+ <div className="flex items-center gap-2 text-cream/70">
95
+ <User className="w-5 h-5 text-copper" />
96
+ <span>{talk.speaker}</span>
97
+ </div>
98
+ )}
99
+
100
+ {/* Duration */}
101
+ <div className="flex items-center gap-2 text-cream/60">
102
+ <Clock className="w-5 h-5 text-copper" />
103
+ <span className="text-lg">{talk.duration}</span>
104
+ </div>
105
+ </div>
106
+
107
+ {/* Description content */}
108
+ <div className="prose prose-lg max-w-none prose-invert prose-p:text-cream/80 prose-headings:text-cream prose-headings:font-display prose-strong:text-cream prose-a:text-gold prose-li:text-cream/80 prose-ul:text-cream/80 font-body text-lg leading-relaxed pb-20">
109
+ <div
110
+ dangerouslySetInnerHTML={{ __html: marked(talk.content) }}
111
+ />
112
+ </div>
113
+ </div>
114
+ </div>
115
+ )
116
+ }
@@ -0,0 +1,40 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { allTalks } from 'content-collections'
4
+
5
+ import TalkCard from '@/components/TalkCard'
6
+ import RemyAssistant from '@/components/RemyAssistant'
7
+
8
+ export const Route = createFileRoute('/talks/')({
9
+ component: TalksPage,
10
+ })
11
+
12
+ function TalksPage() {
13
+ return (
14
+ <>
15
+ <RemyAssistant />
16
+ <div className="min-h-screen">
17
+ {/* Hero section */}
18
+ <div className="relative py-16 px-6">
19
+ <div className="max-w-7xl mx-auto text-center">
20
+ <h1 className="font-display text-5xl md:text-6xl font-bold text-cream mb-4">
21
+ Conference <span className="text-gold italic">Sessions</span>
22
+ </h1>
23
+ <p className="text-xl text-cream/70 max-w-2xl mx-auto font-body">
24
+ Immerse yourself in masterclasses and demonstrations covering every aspect of artisan baking and pastry.
25
+ </p>
26
+ </div>
27
+ </div>
28
+
29
+ {/* Talks grid */}
30
+ <div className="max-w-7xl mx-auto px-6 pb-20">
31
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
32
+ {allTalks.map((talk) => (
33
+ <TalkCard key={talk.slug} talk={talk} />
34
+ ))}
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </>
39
+ )
40
+ }