create-start-app 0.6.12 → 0.7.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 (30) hide show
  1. package/dist/create-app.js +1 -1
  2. package/package.json +2 -11
  3. package/src/create-app.ts +1 -1
  4. package/templates/react/base/README.md.ejs +2 -2
  5. package/templates/react/example/tanchat/assets/public/example-guitar-dune.jpg +0 -0
  6. package/templates/react/example/tanchat/assets/public/example-guitar-motherboard.jpg +0 -0
  7. package/templates/react/example/tanchat/assets/public/example-guitar-racing.jpg +0 -0
  8. package/templates/react/example/tanchat/assets/public/example-guitar-steamer-trunk.jpg +0 -0
  9. package/templates/react/example/tanchat/assets/public/example-guitar-steampunk.jpg +0 -0
  10. package/templates/react/example/tanchat/assets/public/example-guitar-underwater.jpg +0 -0
  11. package/templates/react/example/tanchat/assets/src/components/example-AIAssistant.tsx +173 -0
  12. package/templates/react/example/tanchat/assets/src/components/example-GuitarRecommendation.tsx +47 -0
  13. package/templates/react/example/tanchat/assets/src/data/example-guitars.ts +73 -0
  14. package/templates/react/example/tanchat/assets/src/integrations/tanchat/header-user.tsx +5 -0
  15. package/templates/react/example/tanchat/assets/src/routes/example.chat.tsx +119 -397
  16. package/templates/react/example/tanchat/assets/src/routes/example.guitars/$guitarId.tsx +50 -0
  17. package/templates/react/example/tanchat/assets/src/routes/example.guitars/index.tsx +54 -0
  18. package/templates/react/example/tanchat/assets/src/store/example-assistant.ts +3 -0
  19. package/templates/react/example/tanchat/assets/src/utils/demo.ai.ts +14 -70
  20. package/templates/react/example/tanchat/assets/src/utils/demo.tools.ts +43 -0
  21. package/templates/react/example/tanchat/info.json +4 -0
  22. package/templates/react/example/tanchat/package.json +4 -1
  23. package/tests/snapshots/cra/cr-js-npm.json +1 -1
  24. package/tests/snapshots/cra/cr-ts-npm.json +1 -1
  25. package/tests/snapshots/cra/fr-ts-npm.json +1 -1
  26. package/tests/snapshots/cra/fr-ts-tw-npm.json +1 -1
  27. package/.github/workflows/auto.yml +0 -46
  28. package/templates/react/example/tanchat/assets/src/components/demo.SettingsDialog.tsx +0 -148
  29. package/templates/react/example/tanchat/assets/src/store/demo.hooks.ts +0 -21
  30. package/templates/react/example/tanchat/assets/src/store/demo.store.ts +0 -133
@@ -1,437 +1,159 @@
1
1
  import { createFileRoute } from '@tanstack/react-router'
2
- import { useEffect, useState, useRef } from 'react'
3
- import {
4
- PlusCircle,
5
- MessageCircle,
6
- Trash2,
7
- Send,
8
- Settings,
9
- Edit2,
10
- } from 'lucide-react'
2
+ import { useEffect, useRef } from 'react'
3
+ import { Send } from 'lucide-react'
11
4
  import ReactMarkdown from 'react-markdown'
12
5
  import rehypeRaw from 'rehype-raw'
13
6
  import rehypeSanitize from 'rehype-sanitize'
14
7
  import rehypeHighlight from 'rehype-highlight'
8
+ import remarkGfm from 'remark-gfm'
9
+ import { useChat } from '@ai-sdk/react'
15
10
 
16
- import { SettingsDialog } from '../components/demo.SettingsDialog'
17
- import { useAppState } from '../store/demo.hooks'
18
- import { store } from '../store/demo.store'
19
11
  import { genAIResponse } from '../utils/demo.ai'
20
12
 
21
- import type { Message } from '../utils/demo.ai'
13
+ import type { UIMessage } from 'ai'
22
14
 
23
15
  import '../demo.index.css'
24
16
 
25
- function Home() {
26
- const {
27
- conversations,
28
- currentConversationId,
29
- isLoading,
30
- setCurrentConversationId,
31
- addConversation,
32
- deleteConversation,
33
- updateConversationTitle,
34
- addMessage,
35
- setLoading,
36
- getCurrentConversation,
37
- getActivePrompt,
38
- } = useAppState()
17
+ function InitalLayout({ children }: { children: React.ReactNode }) {
18
+ return (
19
+ <div className="flex-1 flex items-center justify-center px-4">
20
+ <div className="text-center max-w-3xl mx-auto w-full">
21
+ <h1 className="text-6xl font-bold mb-4 bg-gradient-to-r from-orange-500 to-red-600 text-transparent bg-clip-text uppercase">
22
+ <span className="text-white">TanStack</span> Chat
23
+ </h1>
24
+ <p className="text-gray-400 mb-6 w-2/3 mx-auto text-lg">
25
+ You can ask me about anything, I might or might not have a good
26
+ answer, but you can still ask.
27
+ </p>
28
+ {children}
29
+ </div>
30
+ </div>
31
+ )
32
+ }
39
33
 
40
- const currentConversation = getCurrentConversation(store.state)
41
- const messages = currentConversation?.messages || []
34
+ function ChattingLayout({ children }: { children: React.ReactNode }) {
35
+ return (
36
+ <div className="absolute bottom-0 right-0 left-64 bg-gray-900/80 backdrop-blur-sm border-t border-orange-500/10">
37
+ <div className="max-w-3xl mx-auto w-full px-4 py-3">{children}</div>
38
+ </div>
39
+ )
40
+ }
42
41
 
43
- // Local state
44
- const [input, setInput] = useState('')
45
- const [editingChatId, setEditingChatId] = useState<string | null>(null)
46
- const [isSettingsOpen, setIsSettingsOpen] = useState(false)
42
+ function Messages({ messages }: { messages: Array<UIMessage> }) {
47
43
  const messagesContainerRef = useRef<HTMLDivElement>(null)
48
- const [pendingMessage, setPendingMessage] = useState<Message | null>(null)
49
44
 
50
- const scrollToBottom = () => {
45
+ useEffect(() => {
51
46
  if (messagesContainerRef.current) {
52
47
  messagesContainerRef.current.scrollTop =
53
48
  messagesContainerRef.current.scrollHeight
54
49
  }
55
- }
56
-
57
- // Scroll to bottom when messages change or loading state changes
58
- useEffect(() => {
59
- scrollToBottom()
60
- }, [messages, isLoading])
61
-
62
- const handleSubmit = async (e: React.FormEvent) => {
63
- e.preventDefault()
64
- if (!input.trim() || isLoading) return
65
-
66
- const currentInput = input
67
- setInput('') // Clear input early for better UX
68
- setLoading(true)
69
-
70
- try {
71
- let conversationId = currentConversationId
72
-
73
- // If no current conversation, create one
74
- if (!conversationId) {
75
- conversationId = Date.now().toString()
76
- const newConversation = {
77
- id: conversationId,
78
- title: currentInput.trim().slice(0, 30),
79
- messages: [],
80
- }
81
- addConversation(newConversation)
82
- }
83
-
84
- const userMessage: Message = {
85
- id: Date.now().toString(),
86
- role: 'user' as const,
87
- content: currentInput.trim(),
88
- }
89
-
90
- // Add user message
91
- addMessage(conversationId, userMessage)
92
-
93
- // Get active prompt
94
- const activePrompt = getActivePrompt(store.state)
95
- let systemPrompt
96
- if (activePrompt) {
97
- systemPrompt = {
98
- value: activePrompt.content,
99
- enabled: true,
100
- }
101
- }
102
-
103
- // Get AI response
104
- const response = await genAIResponse({
105
- data: {
106
- messages: [...messages, userMessage],
107
- systemPrompt,
108
- },
109
- })
110
-
111
- const reader = response.body?.getReader()
112
- if (!reader) {
113
- throw new Error('No reader found in response')
114
- }
115
-
116
- const decoder = new TextDecoder()
117
-
118
- let done = false
119
- let newMessage = {
120
- id: (Date.now() + 1).toString(),
121
- role: 'assistant' as const,
122
- content: '',
123
- }
124
- while (!done) {
125
- const out = await reader.read()
126
- done = out.done
127
- if (!done) {
128
- try {
129
- const json = JSON.parse(decoder.decode(out.value))
130
- if (json.type === 'content_block_delta') {
131
- newMessage = {
132
- ...newMessage,
133
- content: newMessage.content + json.delta.text,
134
- }
135
- setPendingMessage(newMessage)
136
- }
137
- } catch (e) {}
138
- }
139
- }
140
-
141
- setPendingMessage(null)
142
- if (newMessage.content.trim()) {
143
- addMessage(conversationId, newMessage)
144
- }
145
- } catch (error) {
146
- console.error('Error:', error)
147
- const errorMessage: Message = {
148
- id: (Date.now() + 1).toString(),
149
- role: 'assistant' as const,
150
- content: 'Sorry, I encountered an error processing your request.',
151
- }
152
- if (currentConversationId) {
153
- addMessage(currentConversationId, errorMessage)
154
- }
155
- } finally {
156
- setLoading(false)
157
- }
158
- }
159
-
160
- const handleNewChat = () => {
161
- const newConversation = {
162
- id: Date.now().toString(),
163
- title: 'New Chat',
164
- messages: [],
165
- }
166
- addConversation(newConversation)
167
- }
50
+ }, [messages])
168
51
 
169
- const handleDeleteChat = (id: string) => {
170
- deleteConversation(id)
171
- }
172
-
173
- const handleUpdateChatTitle = (id: string, title: string) => {
174
- updateConversationTitle(id, title)
175
- setEditingChatId(null)
176
- }
177
-
178
- // Handle input change
179
- const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
180
- setInput(e.target.value)
52
+ if (!messages.length) {
53
+ return null
181
54
  }
182
55
 
183
56
  return (
184
- <div className="relative flex h-[calc(100vh-32px)] bg-gray-900">
185
- {/* Settings Button */}
186
- <div className="absolute top-5 right-5 z-50">
187
- <button
188
- onClick={() => setIsSettingsOpen(true)}
189
- className="w-10 h-10 rounded-full bg-gradient-to-r from-orange-500 to-red-600 flex items-center justify-center text-white hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-orange-500"
190
- >
191
- <Settings className="w-5 h-5" />
192
- </button>
193
- </div>
194
-
195
- {/* Sidebar */}
196
- <div className="flex flex-col w-64 bg-gray-800 border-r border-gray-700">
197
- <div className="p-4 border-b border-gray-700">
198
- <button
199
- onClick={handleNewChat}
200
- className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-gradient-to-r from-orange-500 to-red-600 rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-orange-500 w-full justify-center"
57
+ <div ref={messagesContainerRef} className="flex-1 overflow-y-auto pb-24">
58
+ <div className="max-w-3xl mx-auto w-full px-4">
59
+ {messages.map(({ id, role, content }) => (
60
+ <div
61
+ key={id}
62
+ className={`py-6 ${
63
+ role === 'assistant'
64
+ ? 'bg-gradient-to-r from-orange-500/5 to-red-600/5'
65
+ : 'bg-transparent'
66
+ }`}
201
67
  >
202
- <PlusCircle className="w-4 h-4" />
203
- New Chat
204
- </button>
205
- </div>
206
-
207
- {/* Chat List */}
208
- <div className="flex-1 overflow-y-auto">
209
- {conversations.map((chat) => (
210
- <div
211
- key={chat.id}
212
- className={`group flex items-center gap-3 px-3 py-2 cursor-pointer hover:bg-gray-700/50 ${
213
- chat.id === currentConversationId ? 'bg-gray-700/50' : ''
214
- }`}
215
- onClick={() => setCurrentConversationId(chat.id)}
216
- >
217
- <MessageCircle className="w-4 h-4 text-gray-400" />
218
- {editingChatId === chat.id ? (
219
- <input
220
- type="text"
221
- value={chat.title}
222
- onChange={(e) =>
223
- handleUpdateChatTitle(chat.id, e.target.value)
224
- }
225
- onBlur={() => setEditingChatId(null)}
226
- onKeyDown={(e) => {
227
- if (e.key === 'Enter') {
228
- handleUpdateChatTitle(chat.id, chat.title)
229
- }
230
- }}
231
- className="flex-1 bg-transparent text-sm text-white focus:outline-none"
232
- autoFocus
233
- />
68
+ <div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
69
+ {role === 'assistant' ? (
70
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 mt-2 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
71
+ AI
72
+ </div>
234
73
  ) : (
235
- <span className="flex-1 text-sm text-gray-300 truncate">
236
- {chat.title}
237
- </span>
74
+ <div className="w-8 h-8 rounded-lg bg-gray-700 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
75
+ Y
76
+ </div>
238
77
  )}
239
- <div className="hidden group-hover:flex items-center gap-1">
240
- <button
241
- onClick={(e) => {
242
- e.stopPropagation()
243
- setEditingChatId(chat.id)
244
- }}
245
- className="p-1 text-gray-400 hover:text-white"
246
- >
247
- <Edit2 className="w-3 h-3" />
248
- </button>
249
- <button
250
- onClick={(e) => {
251
- e.stopPropagation()
252
- handleDeleteChat(chat.id)
253
- }}
254
- className="p-1 text-gray-400 hover:text-red-500"
78
+ <div className="flex-1 min-w-0">
79
+ <ReactMarkdown
80
+ className="prose dark:prose-invert max-w-none"
81
+ rehypePlugins={[
82
+ rehypeRaw,
83
+ rehypeSanitize,
84
+ rehypeHighlight,
85
+ remarkGfm,
86
+ ]}
255
87
  >
256
- <Trash2 className="w-3 h-3" />
257
- </button>
88
+ {content}
89
+ </ReactMarkdown>
258
90
  </div>
259
91
  </div>
260
- ))}
261
- </div>
92
+ </div>
93
+ ))}
262
94
  </div>
95
+ </div>
96
+ )
97
+ }
263
98
 
264
- {/* Main Content */}
265
- <div className="flex-1 flex flex-col">
266
- {currentConversationId ? (
267
- <>
268
- {/* Messages */}
269
- <div
270
- ref={messagesContainerRef}
271
- className="flex-1 overflow-y-auto pb-24"
272
- >
273
- <div className="max-w-3xl mx-auto w-full px-4">
274
- {[...messages, pendingMessage]
275
- .filter((v) => v)
276
- .map((message) => (
277
- <div
278
- key={message!.id}
279
- className={`py-6 ${
280
- message!.role === 'assistant'
281
- ? 'bg-gradient-to-r from-orange-500/5 to-red-600/5'
282
- : 'bg-transparent'
283
- }`}
284
- >
285
- <div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
286
- {message!.role === 'assistant' ? (
287
- <div className="w-8 h-8 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 mt-2 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
288
- AI
289
- </div>
290
- ) : (
291
- <div className="w-8 h-8 rounded-lg bg-gray-700 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
292
- Y
293
- </div>
294
- )}
295
- <div className="flex-1 min-w-0">
296
- <ReactMarkdown
297
- className="prose dark:prose-invert max-w-none"
298
- rehypePlugins={[
299
- rehypeRaw,
300
- rehypeSanitize,
301
- rehypeHighlight,
302
- ]}
303
- >
304
- {message!.content}
305
- </ReactMarkdown>
306
- </div>
307
- </div>
308
- </div>
309
- ))}
310
- {isLoading && (
311
- <div className="py-6 bg-gradient-to-r from-orange-500/5 to-red-600/5">
312
- <div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
313
- <div className="relative w-8 h-8 flex-shrink-0">
314
- <div className="absolute inset-0 rounded-lg bg-gradient-to-r from-orange-500 via-red-500 to-orange-500 animate-[spin_2s_linear_infinite]"></div>
315
- <div className="absolute inset-[2px] rounded-lg bg-gray-900 flex items-center justify-center">
316
- <div className="relative w-full h-full rounded-lg bg-gradient-to-r from-orange-500 to-red-600 flex items-center justify-center">
317
- <div className="absolute inset-0 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 animate-pulse"></div>
318
- <span className="relative z-10 text-sm font-medium text-white">
319
- AI
320
- </span>
321
- </div>
322
- </div>
323
- </div>
324
- <div className="flex items-center gap-3">
325
- <div className="text-gray-400 font-medium text-lg">
326
- Thinking
327
- </div>
328
- <div className="flex gap-2">
329
- <div
330
- className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
331
- style={{ animationDelay: '0ms' }}
332
- ></div>
333
- <div
334
- className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
335
- style={{ animationDelay: '200ms' }}
336
- ></div>
337
- <div
338
- className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
339
- style={{ animationDelay: '400ms' }}
340
- ></div>
341
- </div>
342
- </div>
343
- </div>
344
- </div>
345
- )}
346
- </div>
347
- </div>
99
+ function ChatPage() {
100
+ const { messages, input, handleInputChange, handleSubmit } = useChat({
101
+ initialMessages: [],
102
+ fetch: (_url, options) => {
103
+ const { messages } = JSON.parse(options!.body! as string)
104
+ return genAIResponse({
105
+ data: {
106
+ messages,
107
+ },
108
+ })
109
+ },
110
+ })
348
111
 
349
- {/* Input */}
350
- <div className="absolute bottom-0 right-0 left-64 bg-gray-900/80 backdrop-blur-sm border-t border-orange-500/10">
351
- <div className="max-w-3xl mx-auto w-full px-4 py-3">
352
- <form onSubmit={handleSubmit}>
353
- <div className="relative">
354
- <textarea
355
- value={input}
356
- onChange={handleInputChange}
357
- onKeyDown={(e) => {
358
- if (e.key === 'Enter' && !e.shiftKey) {
359
- e.preventDefault()
360
- handleSubmit(e)
361
- }
362
- }}
363
- placeholder="Type something clever (or don't, we won't judge)..."
364
- className="w-full rounded-lg border border-orange-500/20 bg-gray-800/50 pl-4 pr-12 py-3 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:border-transparent resize-none overflow-hidden shadow-lg"
365
- rows={1}
366
- style={{ minHeight: '44px', maxHeight: '200px' }}
367
- onInput={(e) => {
368
- const target = e.target as HTMLTextAreaElement
369
- target.style.height = 'auto'
370
- target.style.height =
371
- Math.min(target.scrollHeight, 200) + 'px'
372
- }}
373
- />
374
- <button
375
- type="submit"
376
- disabled={!input.trim() || isLoading}
377
- className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-orange-500 hover:text-orange-400 disabled:text-gray-500 transition-colors focus:outline-none"
378
- >
379
- <Send className="w-4 h-4" />
380
- </button>
381
- </div>
382
- </form>
383
- </div>
384
- </div>
385
- </>
386
- ) : (
387
- <div className="flex-1 flex items-center justify-center px-4">
388
- <div className="text-center max-w-3xl mx-auto w-full">
389
- <h1 className="text-6xl font-bold mb-4 bg-gradient-to-r from-orange-500 to-red-600 text-transparent bg-clip-text uppercase">
390
- <span className="text-white">TanStack</span> Chat
391
- </h1>
392
- <p className="text-gray-400 mb-6 w-2/3 mx-auto text-lg">
393
- You can ask me about anything, I might or might not have a good
394
- answer, but you can still ask.
395
- </p>
396
- <form onSubmit={handleSubmit}>
397
- <div className="relative max-w-xl mx-auto">
398
- <textarea
399
- value={input}
400
- onChange={handleInputChange}
401
- onKeyDown={(e) => {
402
- if (e.key === 'Enter' && !e.shiftKey) {
403
- e.preventDefault()
404
- handleSubmit(e)
405
- }
406
- }}
407
- placeholder="Type something clever (or don't, we won't judge)..."
408
- className="w-full rounded-lg border border-orange-500/20 bg-gray-800/50 pl-4 pr-12 py-3 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:border-transparent resize-none overflow-hidden"
409
- rows={1}
410
- style={{ minHeight: '88px' }}
411
- />
412
- <button
413
- type="submit"
414
- disabled={!input.trim() || isLoading}
415
- className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-orange-500 hover:text-orange-400 disabled:text-gray-500 transition-colors focus:outline-none"
416
- >
417
- <Send className="w-4 h-4" />
418
- </button>
419
- </div>
420
- </form>
112
+ const Layout = messages.length ? ChattingLayout : InitalLayout
113
+
114
+ return (
115
+ <div className="relative flex h-[calc(100vh-32px)] bg-gray-900">
116
+ <div className="flex-1 flex flex-col">
117
+ <Messages messages={messages} />
118
+
119
+ <Layout>
120
+ <form onSubmit={handleSubmit}>
121
+ <div className="relative max-w-xl mx-auto">
122
+ <textarea
123
+ value={input}
124
+ onChange={handleInputChange}
125
+ placeholder="Type something clever (or don't, we won't judge)..."
126
+ className="w-full rounded-lg border border-orange-500/20 bg-gray-800/50 pl-4 pr-12 py-3 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:border-transparent resize-none overflow-hidden shadow-lg"
127
+ rows={1}
128
+ style={{ minHeight: '44px', maxHeight: '200px' }}
129
+ onInput={(e) => {
130
+ const target = e.target as HTMLTextAreaElement
131
+ target.style.height = 'auto'
132
+ target.style.height =
133
+ Math.min(target.scrollHeight, 200) + 'px'
134
+ }}
135
+ onKeyDown={(e) => {
136
+ if (e.key === 'Enter' && !e.shiftKey) {
137
+ e.preventDefault()
138
+ handleSubmit(e)
139
+ }
140
+ }}
141
+ />
142
+ <button
143
+ type="submit"
144
+ disabled={!input.trim()}
145
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-orange-500 hover:text-orange-400 disabled:text-gray-500 transition-colors focus:outline-none"
146
+ >
147
+ <Send className="w-4 h-4" />
148
+ </button>
421
149
  </div>
422
- </div>
423
- )}
150
+ </form>
151
+ </Layout>
424
152
  </div>
425
-
426
- {/* Settings Dialog */}
427
- <SettingsDialog
428
- isOpen={isSettingsOpen}
429
- onClose={() => setIsSettingsOpen(false)}
430
- />
431
153
  </div>
432
154
  )
433
155
  }
434
156
 
435
157
  export const Route = createFileRoute('/example/chat')({
436
- component: Home,
158
+ component: ChatPage,
437
159
  })
@@ -0,0 +1,50 @@
1
+ import { Link, createFileRoute } from '@tanstack/react-router'
2
+ import guitars from '../../data/example-guitars'
3
+
4
+ export const Route = createFileRoute('/example/guitars/$guitarId')({
5
+ component: RouteComponent,
6
+ loader: async ({ params }) => {
7
+ const guitar = guitars.find((guitar) => guitar.id === +params.guitarId)
8
+ if (!guitar) {
9
+ throw new Error('Guitar not found')
10
+ }
11
+ return guitar
12
+ },
13
+ })
14
+
15
+ function RouteComponent() {
16
+ const guitar = Route.useLoaderData()
17
+
18
+ return (
19
+ <div className="relative min-h-[100vh] flex items-center bg-black text-white p-5">
20
+ <div className="relative z-10 w-[60%] bg-gray-900/60 backdrop-blur-md rounded-2xl p-8 border border-gray-800/50 shadow-xl">
21
+ <Link
22
+ to="/example/guitars"
23
+ className="inline-block mb-4 text-emerald-400 hover:text-emerald-300"
24
+ >
25
+ &larr; Back to all guitars
26
+ </Link>
27
+ <h1 className="text-3xl font-bold mb-4">{guitar.name}</h1>
28
+ <p className="text-gray-300 mb-6">{guitar.description}</p>
29
+ <div className="flex items-center justify-between">
30
+ <div className="text-2xl font-bold text-emerald-400">
31
+ ${guitar.price}
32
+ </div>
33
+ <button className="bg-emerald-600 hover:bg-emerald-500 text-white px-6 py-2 rounded-lg transition-colors">
34
+ Add to Cart
35
+ </button>
36
+ </div>
37
+ </div>
38
+
39
+ <div className="absolute top-0 right-0 w-[55%] h-full z-0">
40
+ <div className="w-full h-full overflow-hidden rounded-2xl border-4 border-gray-800 shadow-2xl">
41
+ <img
42
+ src={guitar.image}
43
+ alt={guitar.name}
44
+ className="w-full h-full object-cover guitar-image"
45
+ />
46
+ </div>
47
+ </div>
48
+ </div>
49
+ )
50
+ }
@@ -0,0 +1,54 @@
1
+ import { Link, createFileRoute } from '@tanstack/react-router'
2
+ import guitars from '../../data/example-guitars'
3
+
4
+ export const Route = createFileRoute('/example/guitars/')({
5
+ component: App,
6
+ })
7
+
8
+ export default function App() {
9
+ return (
10
+ <div className="bg-black text-white p-5">
11
+ <h1 className="text-3xl font-bold mb-8 text-center">Featured Guitars</h1>
12
+ <div className="flex flex-wrap gap-12 justify-center">
13
+ {guitars.map((guitar) => (
14
+ <div
15
+ key={guitar.id}
16
+ className="w-full md:w-[calc(50%-1.5rem)] xl:w-[calc(33.333%-2rem)] relative mb-24"
17
+ >
18
+ <Link
19
+ to="/example/guitars/$guitarId"
20
+ params={{
21
+ guitarId: guitar.id.toString(),
22
+ }}
23
+ >
24
+ <div className="relative z-0 w-full aspect-square mb-8">
25
+ <div className="w-full h-full overflow-hidden rounded-2xl border-4 border-gray-800 shadow-2xl">
26
+ <img
27
+ src={guitar.image}
28
+ alt={guitar.name}
29
+ className="w-full h-full object-cover guitar-image group-hover:scale-105 transition-transform duration-500"
30
+ />
31
+ <div className="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
32
+ </div>
33
+
34
+ <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-emerald-500/80 text-white px-4 py-2 rounded-full text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-300 backdrop-blur-sm">
35
+ View Details
36
+ </div>
37
+ </div>
38
+
39
+ <div className="absolute bottom-0 right-0 z-10 w-[80%] bg-gray-900/60 backdrop-blur-md rounded-2xl p-5 border border-gray-800/50 shadow-xl transform translate-y-[40%]">
40
+ <h2 className="text-xl font-bold mb-2">{guitar.name}</h2>
41
+ <p className="text-gray-300 mb-3 line-clamp-2">
42
+ {guitar.shortDescription}
43
+ </p>
44
+ <div className="text-xl font-bold text-emerald-400">
45
+ ${guitar.price}
46
+ </div>
47
+ </div>
48
+ </Link>
49
+ </div>
50
+ ))}
51
+ </div>
52
+ </div>
53
+ )
54
+ }
@@ -0,0 +1,3 @@
1
+ import { Store } from '@tanstack/store'
2
+
3
+ export const showAIAssistant = new Store(false)