@tanstack/cta-framework-react-cra 0.27.1 → 0.29.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.
- package/ADD-ON-AUTHORING.md +168 -0
- package/add-ons/prisma/assets/_dot_env.local.append.ejs +7 -0
- package/add-ons/prisma/assets/public/prisma.svg +1 -0
- package/add-ons/prisma/assets/schema.prisma.ejs +14 -0
- package/add-ons/prisma/assets/seed.ts +30 -0
- package/add-ons/prisma/assets/src/db.ts +11 -0
- package/add-ons/prisma/assets/src/routes/demo/prisma.tsx +186 -0
- package/add-ons/prisma/info.json +32 -0
- package/add-ons/prisma/package.json.ejs +27 -0
- package/add-ons/prisma/small-logo.svg +1 -0
- package/add-ons/strapi/README.md +14 -0
- package/add-ons/strapi/assets/_dot_env.local.append +2 -0
- package/add-ons/strapi/assets/src/lib/strapiClient.ts +7 -0
- package/add-ons/strapi/assets/src/routes/demo/strapi.tsx +66 -0
- package/add-ons/strapi/assets/src/routes/demo/strapi_.$articleId.tsx +78 -0
- package/add-ons/strapi/info.json +18 -0
- package/add-ons/strapi/package.json +5 -0
- package/add-ons/strapi/small-logo.svg +8 -0
- package/add-ons/workos/README.md +3 -0
- package/add-ons/workos/assets/_dot_env.local.append +4 -0
- package/add-ons/workos/assets/src/components/workos-user.tsx +41 -0
- package/add-ons/workos/assets/src/hooks/useUser.tsx +23 -0
- package/add-ons/workos/assets/src/integrations/workos/provider.tsx +34 -0
- package/add-ons/workos/assets/src/routes/demo/workos.tsx +109 -0
- package/add-ons/workos/info.json +30 -0
- package/add-ons/workos/package.json +6 -0
- package/add-ons/workos/small-logo.svg +5 -0
- package/package.json +2 -2
package/ADD-ON-AUTHORING.md
CHANGED
|
@@ -193,3 +193,171 @@ If you don't want a header link you can omit the `url` and `name` properties.
|
|
|
193
193
|
You **MUST** specify routes in the `info.json` file if your add-on supports the `code-router` mode. This is because the `code-routers` setup needs to import the routes in order to add them to the router.
|
|
194
194
|
|
|
195
195
|
By convension you should prefix demo routes with `demo` to make it clear that they are demo routes so they can be easily identified and removed.
|
|
196
|
+
|
|
197
|
+
# Add-on Options
|
|
198
|
+
|
|
199
|
+
The CTA framework supports configurable add-ons through an options system that allows users to customize add-on behavior during creation. This enables more flexible and reusable add-ons that can adapt to different use cases.
|
|
200
|
+
|
|
201
|
+
## Overview
|
|
202
|
+
|
|
203
|
+
Add-on options allow developers to create configurable add-ons where users can select from predefined choices that affect:
|
|
204
|
+
|
|
205
|
+
- Which files are included in the generated project
|
|
206
|
+
- Template variable values used during file generation
|
|
207
|
+
- Package dependencies that get installed
|
|
208
|
+
- Configuration file contents
|
|
209
|
+
|
|
210
|
+
## Configuration Format
|
|
211
|
+
|
|
212
|
+
Options are defined in the `info.json` file using the following schema:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"name": "My Add-on",
|
|
217
|
+
"description": "A configurable add-on",
|
|
218
|
+
"options": {
|
|
219
|
+
"optionName": {
|
|
220
|
+
"type": "select",
|
|
221
|
+
"label": "Display Label",
|
|
222
|
+
"description": "Optional description shown to users",
|
|
223
|
+
"default": "defaultValue",
|
|
224
|
+
"options": [
|
|
225
|
+
{ "value": "option1", "label": "Option 1" },
|
|
226
|
+
{ "value": "option2", "label": "Option 2" }
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Option Types
|
|
234
|
+
|
|
235
|
+
#### Select Options
|
|
236
|
+
|
|
237
|
+
The `select` type allows users to choose from a predefined list of options:
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
"database": {
|
|
241
|
+
"type": "select",
|
|
242
|
+
"label": "Database Provider",
|
|
243
|
+
"description": "Choose your database provider",
|
|
244
|
+
"default": "postgres",
|
|
245
|
+
"options": [
|
|
246
|
+
{ "value": "postgres", "label": "PostgreSQL" },
|
|
247
|
+
{ "value": "mysql", "label": "MySQL" },
|
|
248
|
+
{ "value": "sqlite", "label": "SQLite" }
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Properties:**
|
|
254
|
+
|
|
255
|
+
- `type`: Must be `"select"`
|
|
256
|
+
- `label`: Display text shown to users
|
|
257
|
+
- `description`: Optional help text
|
|
258
|
+
- `default`: Default value that must match one of the option values
|
|
259
|
+
- `options`: Array of value/label pairs
|
|
260
|
+
|
|
261
|
+
## Template Usage
|
|
262
|
+
|
|
263
|
+
Option values are available in EJS templates through the `addOnOption` variable:
|
|
264
|
+
|
|
265
|
+
```ejs
|
|
266
|
+
<!-- Access option value -->
|
|
267
|
+
<% if (addOnOption.myAddOnId.database === 'postgres') { %>
|
|
268
|
+
PostgreSQL specific code
|
|
269
|
+
<% } %>
|
|
270
|
+
|
|
271
|
+
<!-- Use option value in output -->
|
|
272
|
+
const driver = '<%= addOnOption.myAddOnId.database %>'
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The structure is: `addOnOption.{addOnId}.{optionName}`
|
|
276
|
+
|
|
277
|
+
### Template Conditional Logic
|
|
278
|
+
|
|
279
|
+
Within template files, use `ignoreFile()` to skip file generation:
|
|
280
|
+
|
|
281
|
+
```ejs
|
|
282
|
+
<% if (addOnOption.prisma.database !== 'postgres') { ignoreFile() } %>
|
|
283
|
+
import { PrismaClient } from '@prisma/client'
|
|
284
|
+
|
|
285
|
+
declare global {
|
|
286
|
+
var __prisma: PrismaClient | undefined
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const prisma = globalThis.__prisma || new PrismaClient()
|
|
290
|
+
|
|
291
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
292
|
+
globalThis.__prisma = prisma
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Complete Example: Prisma Add-on
|
|
297
|
+
|
|
298
|
+
Here's how the Prisma add-on implements configurable database support:
|
|
299
|
+
|
|
300
|
+
### Examples
|
|
301
|
+
|
|
302
|
+
Configuration in `info.json`:
|
|
303
|
+
|
|
304
|
+
```json
|
|
305
|
+
{
|
|
306
|
+
"name": "Prisma ORM",
|
|
307
|
+
"description": "Add Prisma ORM with configurable database support to your application.",
|
|
308
|
+
"options": {
|
|
309
|
+
"database": {
|
|
310
|
+
"type": "select",
|
|
311
|
+
"label": "Database Provider",
|
|
312
|
+
"description": "Choose your database provider",
|
|
313
|
+
"default": "postgres",
|
|
314
|
+
"options": [
|
|
315
|
+
{ "value": "postgres", "label": "PostgreSQL" },
|
|
316
|
+
{ "value": "mysql", "label": "MySQL" },
|
|
317
|
+
{ "value": "sqlite", "label": "SQLite" }
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Code in `package.json.ejs`:
|
|
325
|
+
|
|
326
|
+
```ejs
|
|
327
|
+
{
|
|
328
|
+
"prisma": "^6.16.3",
|
|
329
|
+
"@prisma/client": "^6.16.3"<% if (addOnOption.prisma.database === 'postgres') { %>,
|
|
330
|
+
"pg": "^8.11.0",
|
|
331
|
+
"@types/pg": "^8.10.0"<% } else if (addOnOption.prisma.database === 'mysql') { %>,
|
|
332
|
+
"mysql2": "^3.6.0"<% } else if (addOnOption.prisma.database === 'sqlite') { %><% } %>
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## CLI Usage
|
|
337
|
+
|
|
338
|
+
### Interactive Mode
|
|
339
|
+
|
|
340
|
+
When using the CLI interactively, users are prompted for each option:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
create-tsrouter-app my-app
|
|
344
|
+
# User selects Prisma add-on
|
|
345
|
+
# CLI prompts: "Prisma ORM: Database Provider" with options
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Non-Interactive Mode
|
|
349
|
+
|
|
350
|
+
Options can be specified via JSON configuration:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
create-tsrouter-app my-app --add-ons prisma --add-on-config '{"prisma":{"database":"mysql"}}'
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Best Practices
|
|
357
|
+
|
|
358
|
+
1. **Use descriptive labels** - Make option purposes clear to users
|
|
359
|
+
2. **Provide sensible defaults** - Choose the most common use case
|
|
360
|
+
3. **Group related files** - Use consistent prefixing for option-specific files
|
|
361
|
+
4. **Document options** - Include descriptions to help users understand choices
|
|
362
|
+
5. **Test all combinations** - Ensure each option value generates working code
|
|
363
|
+
6. **Use validation** - The system validates options against the schema automatically
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<% if (addOnOption.prisma.database === 'postgres') { %>
|
|
2
|
+
# Database URL for PostgreSQL
|
|
3
|
+
DATABASE_URL="postgresql://username:password@localhost:5432/mydb"<% } else if (addOnOption.prisma.database === 'mysql') { %>
|
|
4
|
+
# Database URL for MySQL
|
|
5
|
+
DATABASE_URL="mysql://username:password@localhost:3306/mydb"<% } else if (addOnOption.prisma.database === 'sqlite') { %>
|
|
6
|
+
# Database URL for SQLite
|
|
7
|
+
DATABASE_URL="file:./dev.db"<% } %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 256 310" width="256" height="310" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path fill="#fff" d="M254.313 235.519L148 9.749A17.063 17.063 0 00133.473.037a16.87 16.87 0 00-15.533 8.052L2.633 194.848a17.465 17.465 0 00.193 18.747L59.2 300.896a18.13 18.13 0 0020.363 7.489l163.599-48.392a17.929 17.929 0 0011.26-9.722 17.542 17.542 0 00-.101-14.76l-.008.008zm-23.802 9.683l-138.823 41.05c-4.235 1.26-8.3-2.411-7.419-6.685l49.598-237.484c.927-4.443 7.063-5.147 9.003-1.035l91.814 194.973a6.63 6.63 0 01-4.18 9.18h.007z"/></svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "<%= addOnOption.prisma.database === "postgres" ? "postgresql" : addOnOption.prisma.database %>"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model Todo {
|
|
11
|
+
id Int @id @default(autoincrement())
|
|
12
|
+
title String
|
|
13
|
+
createdAt DateTime @default(now())
|
|
14
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client'
|
|
2
|
+
|
|
3
|
+
const prisma = new PrismaClient()
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
console.log('🌱 Seeding database...')
|
|
7
|
+
|
|
8
|
+
// Clear existing todos
|
|
9
|
+
await prisma.todo.deleteMany()
|
|
10
|
+
|
|
11
|
+
// Create example todos
|
|
12
|
+
const todos = await prisma.todo.createMany({
|
|
13
|
+
data: [
|
|
14
|
+
{ title: 'Buy groceries' },
|
|
15
|
+
{ title: 'Read a book' },
|
|
16
|
+
{ title: 'Workout' },
|
|
17
|
+
],
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
console.log(`✅ Created ${todos.count} todos`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main()
|
|
24
|
+
.catch((e) => {
|
|
25
|
+
console.error('❌ Error seeding database:', e)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
})
|
|
28
|
+
.finally(async () => {
|
|
29
|
+
await prisma.$disconnect()
|
|
30
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client'
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
var __prisma: PrismaClient | undefined
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const prisma = globalThis.__prisma || new PrismaClient()
|
|
8
|
+
|
|
9
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
10
|
+
globalThis.__prisma = prisma
|
|
11
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createFileRoute, useRouter } from '@tanstack/react-router'
|
|
2
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
3
|
+
import { prisma } from '@/db'
|
|
4
|
+
|
|
5
|
+
const getTodos = createServerFn({
|
|
6
|
+
method: 'GET',
|
|
7
|
+
}).handler(async () => {
|
|
8
|
+
return await prisma.todo.findMany({
|
|
9
|
+
orderBy: { createdAt: 'desc' },
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const createTodo = createServerFn({
|
|
14
|
+
method: 'POST',
|
|
15
|
+
})
|
|
16
|
+
.inputValidator((data: { title: string }) => data)
|
|
17
|
+
.handler(async ({ data }) => {
|
|
18
|
+
return await prisma.todo.create({
|
|
19
|
+
data,
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const Route = createFileRoute('/demo/prisma')({
|
|
24
|
+
component: DemoPrisma,
|
|
25
|
+
loader: async () => await getTodos(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
function DemoPrisma() {
|
|
29
|
+
const router = useRouter()
|
|
30
|
+
const todos = Route.useLoaderData()
|
|
31
|
+
|
|
32
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
33
|
+
e.preventDefault()
|
|
34
|
+
const formData = new FormData(e.target as HTMLFormElement)
|
|
35
|
+
const title = formData.get('title') as string
|
|
36
|
+
|
|
37
|
+
if (!title) return
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await createTodo({ data: { title } })
|
|
41
|
+
router.invalidate()
|
|
42
|
+
;(e.target as HTMLFormElement).reset()
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Failed to create todo:', error)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className="flex items-center justify-center min-h-screen p-4 text-white"
|
|
51
|
+
style={{
|
|
52
|
+
background:
|
|
53
|
+
'linear-gradient(135deg, #0c1a2b 0%, #1a2332 50%, #16202e 100%)',
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<div
|
|
57
|
+
className="w-full max-w-2xl p-8 rounded-xl shadow-2xl border border-white/10"
|
|
58
|
+
style={{
|
|
59
|
+
background:
|
|
60
|
+
'linear-gradient(135deg, rgba(22, 32, 46, 0.95) 0%, rgba(12, 26, 43, 0.95) 100%)',
|
|
61
|
+
backdropFilter: 'blur(10px)',
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
className="flex items-center justify-center gap-4 mb-8 p-4 rounded-lg"
|
|
66
|
+
style={{
|
|
67
|
+
background:
|
|
68
|
+
'linear-gradient(90deg, rgba(93, 103, 227, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
|
|
69
|
+
border: '1px solid rgba(93, 103, 227, 0.2)',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<div className="relative group">
|
|
73
|
+
<div className="absolute -inset-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-indigo-500 rounded-lg blur-lg opacity-60 group-hover:opacity-100 transition duration-500"></div>
|
|
74
|
+
<div className="relative bg-gradient-to-br from-indigo-600 to-purple-600 p-3 rounded-lg">
|
|
75
|
+
<img
|
|
76
|
+
src="/prisma.svg"
|
|
77
|
+
alt="Prisma Logo"
|
|
78
|
+
className="w-8 h-8 transform group-hover:scale-110 transition-transform duration-300"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<h1 className="text-3xl font-bold bg-gradient-to-r from-indigo-300 via-purple-300 to-indigo-300 text-transparent bg-clip-text">
|
|
83
|
+
Prisma Database Demo
|
|
84
|
+
</h1>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<h2 className="text-2xl font-bold mb-4 text-indigo-200">Todos</h2>
|
|
88
|
+
|
|
89
|
+
<ul className="space-y-3 mb-6">
|
|
90
|
+
{todos.map((todo) => (
|
|
91
|
+
<li
|
|
92
|
+
key={todo.id}
|
|
93
|
+
className="rounded-lg p-4 shadow-md border transition-all hover:scale-[1.02] cursor-pointer group"
|
|
94
|
+
style={{
|
|
95
|
+
background:
|
|
96
|
+
'linear-gradient(135deg, rgba(93, 103, 227, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%)',
|
|
97
|
+
borderColor: 'rgba(93, 103, 227, 0.3)',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<div className="flex items-center justify-between">
|
|
101
|
+
<span className="text-lg font-medium text-white group-hover:text-indigo-200 transition-colors">
|
|
102
|
+
{todo.title}
|
|
103
|
+
</span>
|
|
104
|
+
<span className="text-xs text-indigo-300/70">#{todo.id}</span>
|
|
105
|
+
</div>
|
|
106
|
+
</li>
|
|
107
|
+
))}
|
|
108
|
+
{todos.length === 0 && (
|
|
109
|
+
<li className="text-center py-8 text-indigo-300/70">
|
|
110
|
+
No todos yet. Create one below!
|
|
111
|
+
</li>
|
|
112
|
+
)}
|
|
113
|
+
</ul>
|
|
114
|
+
|
|
115
|
+
<form onSubmit={handleSubmit} className="flex gap-2">
|
|
116
|
+
<input
|
|
117
|
+
type="text"
|
|
118
|
+
name="title"
|
|
119
|
+
placeholder="Add a new todo..."
|
|
120
|
+
className="flex-1 px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 transition-all text-white placeholder-indigo-300/50"
|
|
121
|
+
style={{
|
|
122
|
+
background: 'rgba(93, 103, 227, 0.1)',
|
|
123
|
+
borderColor: 'rgba(93, 103, 227, 0.3)',
|
|
124
|
+
focusRing: 'rgba(93, 103, 227, 0.5)',
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
<button
|
|
128
|
+
type="submit"
|
|
129
|
+
className="px-6 py-3 font-semibold rounded-lg shadow-lg transition-all duration-200 hover:shadow-xl hover:scale-105 active:scale-95 whitespace-nowrap"
|
|
130
|
+
style={{
|
|
131
|
+
background: 'linear-gradient(135deg, #5d67e3 0%, #8b5cf6 100%)',
|
|
132
|
+
color: 'white',
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
Add Todo
|
|
136
|
+
</button>
|
|
137
|
+
</form>
|
|
138
|
+
|
|
139
|
+
<div
|
|
140
|
+
className="mt-8 p-6 rounded-lg border"
|
|
141
|
+
style={{
|
|
142
|
+
background: 'rgba(93, 103, 227, 0.05)',
|
|
143
|
+
borderColor: 'rgba(93, 103, 227, 0.2)',
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<h3 className="text-lg font-semibold mb-2 text-indigo-200">
|
|
147
|
+
Powered by Prisma ORM
|
|
148
|
+
</h3>
|
|
149
|
+
<p className="text-sm text-indigo-300/80 mb-4">
|
|
150
|
+
Next-generation ORM for Node.js & TypeScript with PostgreSQL
|
|
151
|
+
</p>
|
|
152
|
+
<div className="space-y-2 text-sm">
|
|
153
|
+
<p className="text-indigo-200 font-medium">Setup Instructions:</p>
|
|
154
|
+
<ol className="list-decimal list-inside space-y-2 text-indigo-300/80">
|
|
155
|
+
<li>
|
|
156
|
+
Configure your{' '}
|
|
157
|
+
<code className="px-2 py-1 rounded bg-black/30 text-purple-300">
|
|
158
|
+
DATABASE_URL
|
|
159
|
+
</code>{' '}
|
|
160
|
+
in .env.local
|
|
161
|
+
</li>
|
|
162
|
+
<li>
|
|
163
|
+
Run:{' '}
|
|
164
|
+
<code className="px-2 py-1 rounded bg-black/30 text-purple-300">
|
|
165
|
+
npx prisma generate
|
|
166
|
+
</code>
|
|
167
|
+
</li>
|
|
168
|
+
<li>
|
|
169
|
+
Run:{' '}
|
|
170
|
+
<code className="px-2 py-1 rounded bg-black/30 text-purple-300">
|
|
171
|
+
npx prisma db push
|
|
172
|
+
</code>
|
|
173
|
+
</li>
|
|
174
|
+
<li>
|
|
175
|
+
Optional:{' '}
|
|
176
|
+
<code className="px-2 py-1 rounded bg-black/30 text-purple-300">
|
|
177
|
+
npx prisma studio
|
|
178
|
+
</code>
|
|
179
|
+
</li>
|
|
180
|
+
</ol>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Prisma",
|
|
3
|
+
"description": "Add Prisma Postgres, or Prisma ORM with other DBs to your application.",
|
|
4
|
+
"phase": "add-on",
|
|
5
|
+
"type": "add-on",
|
|
6
|
+
"link": "https://www.prisma.io/",
|
|
7
|
+
"modes": ["file-router"],
|
|
8
|
+
"dependsOn": ["start"],
|
|
9
|
+
"postInitSpecialSteps": ["post-init-script"],
|
|
10
|
+
"options": {
|
|
11
|
+
"database": {
|
|
12
|
+
"type": "select",
|
|
13
|
+
"label": "Database Provider",
|
|
14
|
+
"description": "Choose your database provider",
|
|
15
|
+
"default": "postgres",
|
|
16
|
+
"options": [
|
|
17
|
+
{ "value": "postgres", "label": "Prisma PostgreSQL" },
|
|
18
|
+
{ "value": "sqlite", "label": "SQLite" },
|
|
19
|
+
{ "value": "mysql", "label": "MySQL" }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"routes": [
|
|
24
|
+
{
|
|
25
|
+
"icon": "Database",
|
|
26
|
+
"url": "/demo/prisma",
|
|
27
|
+
"name": "Prisma",
|
|
28
|
+
"path": "src/routes/demo/prisma.tsx",
|
|
29
|
+
"jsName": "DemoPrisma"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"prisma": "^6.16.3",
|
|
4
|
+
"@prisma/client": "^6.16.3"<% if (addOnOption.prisma.database === 'postgres') { %>,
|
|
5
|
+
"pg": "^8.11.0"<% } %><% if (addOnOption.prisma.database === 'mysql') { %>,
|
|
6
|
+
"mysql2": "^3.6.0"<% } %>
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"dotenv-cli": "^10.0.0",
|
|
10
|
+
"ts-node": "^10.9.2",
|
|
11
|
+
<% if (addOnOption.prisma.database === 'postgres') { %>
|
|
12
|
+
"@types/pg": "^8.10.0"<% } %><% if (addOnOption.prisma.database === 'mysql') { %>
|
|
13
|
+
"@types/mysql2": "^3.6.0"<% } %><% if (addOnOption.prisma.database === 'sqlite') { %>
|
|
14
|
+
"@types/better-sqlite3": "^7.6.0"<% } %>
|
|
15
|
+
},
|
|
16
|
+
"scripts": {<% if (addOnOption.prisma.database === 'postgres') { %>
|
|
17
|
+
"post-cta-init": "npx create-db@latest",<% } %>
|
|
18
|
+
"db:generate": "dotenv -e .env.local -- prisma generate",
|
|
19
|
+
"db:push": "dotenv -e .env.local -- prisma db push",
|
|
20
|
+
"db:migrate": "dotenv -e .env.local -- prisma migrate dev",
|
|
21
|
+
"db:studio": "dotenv -e .env.local -- prisma studio",
|
|
22
|
+
"db:seed": "dotenv -e .env.local -- prisma db seed"
|
|
23
|
+
},
|
|
24
|
+
"prisma": {
|
|
25
|
+
"seed": "ts-node seed.ts"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 256 310" width="256" height="310" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path fill="#000" d="M254.313 235.519L148 9.749A17.063 17.063 0 00133.473.037a16.87 16.87 0 00-15.533 8.052L2.633 194.848a17.465 17.465 0 00.193 18.747L59.2 300.896a18.13 18.13 0 0020.363 7.489l163.599-48.392a17.929 17.929 0 0011.26-9.722 17.542 17.542 0 00-.101-14.76l-.008.008zm-23.802 9.683l-138.823 41.05c-4.235 1.26-8.3-2.411-7.419-6.685l49.598-237.484c.927-4.443 7.063-5.147 9.003-1.035l91.814 194.973a6.63 6.63 0 01-4.18 9.18h.007z"/></svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Setting up Strapi
|
|
2
|
+
|
|
3
|
+
The current setup shows an example of how to use Strapi with an articles collection which is part of the example structure & data.
|
|
4
|
+
|
|
5
|
+
- Create a local running copy of the strapi admin
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpx create-strapi@latest my-strapi-project
|
|
9
|
+
cd my-strapi-project
|
|
10
|
+
pnpm dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Login and publish the example articles to see them on the strapi demo page.
|
|
14
|
+
- Set the `VITE_STRAPI_URL` environment variable in your `.env.local`. (For local it should be http://localhost:1337/api)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { articles } from '@/lib/strapiClient'
|
|
2
|
+
import { createFileRoute, Link } from '@tanstack/react-router'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/demo/strapi')({
|
|
5
|
+
component: RouteComponent,
|
|
6
|
+
loader: async () => {
|
|
7
|
+
const { data: strapiArticles } = await articles.find()
|
|
8
|
+
return strapiArticles
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
function RouteComponent() {
|
|
13
|
+
const strapiArticles = Route.useLoaderData()
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 p-8">
|
|
17
|
+
<div className="max-w-7xl mx-auto">
|
|
18
|
+
<h1 className="text-4xl font-bold mb-8 text-white">
|
|
19
|
+
<span className="bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">
|
|
20
|
+
Strapi
|
|
21
|
+
</span>{' '}
|
|
22
|
+
<span className="text-gray-300">Articles</span>
|
|
23
|
+
</h1>
|
|
24
|
+
|
|
25
|
+
{strapiArticles && strapiArticles.length > 0 ? (
|
|
26
|
+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
27
|
+
{strapiArticles.map((article) => (
|
|
28
|
+
<Link
|
|
29
|
+
key={article.id}
|
|
30
|
+
to="/demo/strapi/$articleId"
|
|
31
|
+
params={{ articleId: article.documentId }}
|
|
32
|
+
className="block"
|
|
33
|
+
>
|
|
34
|
+
<article className="bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-xl p-6 hover:border-cyan-500/50 transition-all duration-300 hover:shadow-lg hover:shadow-cyan-500/10 cursor-pointer h-full">
|
|
35
|
+
<h2 className="text-xl font-semibold text-white mb-3">
|
|
36
|
+
{article.title || 'Untitled'}
|
|
37
|
+
</h2>
|
|
38
|
+
|
|
39
|
+
{article.description && (
|
|
40
|
+
<p className="text-gray-400 mb-4 leading-relaxed">
|
|
41
|
+
{article.description}
|
|
42
|
+
</p>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
{article.content && (
|
|
46
|
+
<p className="text-gray-400 line-clamp-3 leading-relaxed">
|
|
47
|
+
{article.content}
|
|
48
|
+
</p>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{article.createdAt && (
|
|
52
|
+
<p className="text-sm text-cyan-400/70 mt-4">
|
|
53
|
+
{new Date(article.createdAt).toLocaleDateString()}
|
|
54
|
+
</p>
|
|
55
|
+
)}
|
|
56
|
+
</article>
|
|
57
|
+
</Link>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
) : (
|
|
61
|
+
<p className="text-gray-400">No articles found.</p>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { articles } from '@/lib/strapiClient'
|
|
2
|
+
import { createFileRoute, Link } from '@tanstack/react-router'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/demo/strapi_/$articleId')({
|
|
5
|
+
component: RouteComponent,
|
|
6
|
+
loader: async ({ params }) => {
|
|
7
|
+
const { data: article } = await articles.findOne(params.articleId)
|
|
8
|
+
return article
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
function RouteComponent() {
|
|
13
|
+
const article = Route.useLoaderData()
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 p-8">
|
|
17
|
+
<div className="max-w-4xl mx-auto">
|
|
18
|
+
<Link
|
|
19
|
+
to="/demo/strapi"
|
|
20
|
+
className="inline-flex items-center text-cyan-400 hover:text-cyan-300 mb-6 transition-colors"
|
|
21
|
+
>
|
|
22
|
+
<svg
|
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
+
className="h-5 w-5 mr-2"
|
|
25
|
+
viewBox="0 0 20 20"
|
|
26
|
+
fill="currentColor"
|
|
27
|
+
>
|
|
28
|
+
<path
|
|
29
|
+
fillRule="evenodd"
|
|
30
|
+
d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
|
|
31
|
+
clipRule="evenodd"
|
|
32
|
+
/>
|
|
33
|
+
</svg>
|
|
34
|
+
Back to Articles
|
|
35
|
+
</Link>
|
|
36
|
+
|
|
37
|
+
<article className="bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-xl p-8">
|
|
38
|
+
<h1 className="text-4xl font-bold text-white mb-4">
|
|
39
|
+
{article?.title || 'Untitled'}
|
|
40
|
+
</h1>
|
|
41
|
+
|
|
42
|
+
{article?.createdAt && (
|
|
43
|
+
<p className="text-sm text-cyan-400/70 mb-6">
|
|
44
|
+
Published on{' '}
|
|
45
|
+
{new Date(article?.createdAt).toLocaleDateString('en-US', {
|
|
46
|
+
year: 'numeric',
|
|
47
|
+
month: 'long',
|
|
48
|
+
day: 'numeric',
|
|
49
|
+
})}
|
|
50
|
+
</p>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
{article?.description && (
|
|
54
|
+
<div className="mb-6">
|
|
55
|
+
<h2 className="text-xl font-semibold text-gray-300 mb-3">
|
|
56
|
+
Description
|
|
57
|
+
</h2>
|
|
58
|
+
<p className="text-gray-400 leading-relaxed">
|
|
59
|
+
{article?.description}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{article?.content && (
|
|
65
|
+
<div>
|
|
66
|
+
<h2 className="text-xl font-semibold text-gray-300 mb-3">
|
|
67
|
+
Content
|
|
68
|
+
</h2>
|
|
69
|
+
<div className="text-gray-400 leading-relaxed whitespace-pre-wrap">
|
|
70
|
+
{article?.content}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</article>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Strapi",
|
|
3
|
+
"description": "Use the Strapi CMS to manage your content.",
|
|
4
|
+
"link": "https://strapi.io/",
|
|
5
|
+
"phase": "add-on",
|
|
6
|
+
"type": "add-on",
|
|
7
|
+
"modes": [
|
|
8
|
+
"file-router"
|
|
9
|
+
],
|
|
10
|
+
"routes": [
|
|
11
|
+
{
|
|
12
|
+
"url": "/demo/strapi",
|
|
13
|
+
"name": "Strapi",
|
|
14
|
+
"path": "src/routes/demo.strapi.tsx",
|
|
15
|
+
"jsName": "StrapiDemo"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M0 208C0 109.948 0 60.9218 30.4609 30.4609C60.9218 0 109.948 0 208 0H392C490.052 0 539.078 0 569.539 30.4609C600 60.9218 600 109.948 600 208V392C600 490.052 600 539.078 569.539 569.539C539.078 600 490.052 600 392 600H208C109.948 600 60.9218 600 30.4609 569.539C0 539.078 0 490.052 0 392V208Z" fill="#4945FF"/>
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M414 182H212V285H315V388H418V186C418 183.791 416.209 182 414 182Z" fill="white"/>
|
|
4
|
+
<rect x="311" y="285" width="4" height="4" fill="white"/>
|
|
5
|
+
<path d="M212 285H311C313.209 285 315 286.791 315 289V388H216C213.791 388 212 386.209 212 384V285Z" fill="#9593FF"/>
|
|
6
|
+
<path d="M315 388H418L318.414 487.586C317.154 488.846 315 487.953 315 486.172V388Z" fill="#9593FF"/>
|
|
7
|
+
<path d="M212 285H113.828C112.046 285 111.154 282.846 112.414 281.586L212 182V285Z" fill="#9593FF"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useAuth } from '@workos-inc/authkit-react'
|
|
2
|
+
|
|
3
|
+
export default function SignInButton({ large }: { large?: boolean }) {
|
|
4
|
+
const { user, isLoading, signIn, signOut } = useAuth()
|
|
5
|
+
|
|
6
|
+
const buttonClasses = `${
|
|
7
|
+
large ? 'px-6 py-3 text-base' : 'px-4 py-2 text-sm'
|
|
8
|
+
} bg-blue-600 hover:bg-blue-700 text-white font-medium rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed`
|
|
9
|
+
|
|
10
|
+
if (user) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex flex-col gap-3">
|
|
13
|
+
<div className="flex items-center gap-2">
|
|
14
|
+
{user.profilePictureUrl && (
|
|
15
|
+
<img
|
|
16
|
+
src={user.profilePictureUrl}
|
|
17
|
+
alt={`Avatar of ${user.firstName} ${user.lastName}`}
|
|
18
|
+
className="w-10 h-10 rounded-full"
|
|
19
|
+
/>
|
|
20
|
+
)}
|
|
21
|
+
{user.firstName} {user.lastName}
|
|
22
|
+
</div>
|
|
23
|
+
<button onClick={() => signOut()} className={buttonClasses}>
|
|
24
|
+
Sign Out
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<button
|
|
32
|
+
onClick={() => {
|
|
33
|
+
signIn()
|
|
34
|
+
}}
|
|
35
|
+
className={buttonClasses}
|
|
36
|
+
disabled={isLoading}
|
|
37
|
+
>
|
|
38
|
+
Sign In {large && 'with AuthKit'}
|
|
39
|
+
</button>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useAuth } from "@workos-inc/authkit-react";
|
|
3
|
+
import { useLocation } from "@tanstack/react-router";
|
|
4
|
+
|
|
5
|
+
type UserOrNull = ReturnType<typeof useAuth>["user"];
|
|
6
|
+
|
|
7
|
+
// redirects to the sign-in page if the user is not signed in
|
|
8
|
+
export const useUser = (): UserOrNull => {
|
|
9
|
+
const { user, isLoading, signIn } = useAuth();
|
|
10
|
+
const location = useLocation();
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!isLoading && !user) {
|
|
14
|
+
signIn({
|
|
15
|
+
state: { returnTo: location.pathname },
|
|
16
|
+
});
|
|
17
|
+
} else {
|
|
18
|
+
console.log(user);
|
|
19
|
+
}
|
|
20
|
+
}, [isLoading, user]);
|
|
21
|
+
|
|
22
|
+
return user;
|
|
23
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AuthKitProvider } from '@workos-inc/authkit-react'
|
|
2
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
3
|
+
|
|
4
|
+
const VITE_WORKOS_CLIENT_ID = import.meta.env.VITE_WORKOS_CLIENT_ID
|
|
5
|
+
if (!VITE_WORKOS_CLIENT_ID) {
|
|
6
|
+
throw new Error('Add your WorkOS Client ID to the .env.local file')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const VITE_WORKOS_API_HOSTNAME = import.meta.env.VITE_WORKOS_API_HOSTNAME
|
|
10
|
+
if (!VITE_WORKOS_API_HOSTNAME) {
|
|
11
|
+
throw new Error('Add your WorkOS API Hostname to the .env.local file')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function AppWorkOSProvider({
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
children: React.ReactNode
|
|
18
|
+
}) {
|
|
19
|
+
const navigate = useNavigate()
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<AuthKitProvider
|
|
23
|
+
clientId={VITE_WORKOS_CLIENT_ID}
|
|
24
|
+
apiHostname={VITE_WORKOS_API_HOSTNAME}
|
|
25
|
+
onRedirectCallback={({ state }) => {
|
|
26
|
+
if (state?.returnTo) {
|
|
27
|
+
navigate(state.returnTo)
|
|
28
|
+
}
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</AuthKitProvider>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { useAuth } from '@workos-inc/authkit-react'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/demo/workos')({
|
|
5
|
+
ssr: false,
|
|
6
|
+
component: App,
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
function App() {
|
|
10
|
+
const { user, isLoading, signIn, signOut } = useAuth()
|
|
11
|
+
|
|
12
|
+
if (isLoading) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex items-center justify-center p-4">
|
|
15
|
+
<div className="bg-gray-800/50 backdrop-blur-sm rounded-2xl shadow-2xl p-8 w-full max-w-md border border-gray-700/50">
|
|
16
|
+
<p className="text-gray-400 text-center">Loading...</p>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (user) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex items-center justify-center p-4">
|
|
25
|
+
<div className="bg-gray-800/50 backdrop-blur-sm rounded-2xl shadow-2xl p-8 w-full max-w-md border border-gray-700/50">
|
|
26
|
+
<h1 className="text-2xl font-bold text-white mb-6 text-center">
|
|
27
|
+
User Profile
|
|
28
|
+
</h1>
|
|
29
|
+
|
|
30
|
+
<div className="space-y-6">
|
|
31
|
+
{/* Profile Picture */}
|
|
32
|
+
{user.profilePictureUrl && (
|
|
33
|
+
<div className="flex justify-center">
|
|
34
|
+
<img
|
|
35
|
+
src={user.profilePictureUrl}
|
|
36
|
+
alt={`Avatar of ${user.firstName} ${user.lastName}`}
|
|
37
|
+
className="w-24 h-24 rounded-full border-4 border-gray-700 shadow-lg"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
{/* User Information */}
|
|
43
|
+
<div className="space-y-4">
|
|
44
|
+
<div className="bg-gray-700/30 rounded-lg p-4 border border-gray-600/30">
|
|
45
|
+
<label className="text-gray-400 text-sm font-medium block mb-1">
|
|
46
|
+
First Name
|
|
47
|
+
</label>
|
|
48
|
+
<p className="text-white text-lg">{user.firstName || 'N/A'}</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className="bg-gray-700/30 rounded-lg p-4 border border-gray-600/30">
|
|
52
|
+
<label className="text-gray-400 text-sm font-medium block mb-1">
|
|
53
|
+
Last Name
|
|
54
|
+
</label>
|
|
55
|
+
<p className="text-white text-lg">{user.lastName || 'N/A'}</p>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="bg-gray-700/30 rounded-lg p-4 border border-gray-600/30">
|
|
59
|
+
<label className="text-gray-400 text-sm font-medium block mb-1">
|
|
60
|
+
Email
|
|
61
|
+
</label>
|
|
62
|
+
<p className="text-white text-lg break-all">
|
|
63
|
+
{user.email || 'N/A'}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="bg-gray-700/30 rounded-lg p-4 border border-gray-600/30">
|
|
68
|
+
<label className="text-gray-400 text-sm font-medium block mb-1">
|
|
69
|
+
User ID
|
|
70
|
+
</label>
|
|
71
|
+
<p className="text-gray-300 text-sm font-mono break-all">
|
|
72
|
+
{user.id || 'N/A'}
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Sign Out Button */}
|
|
78
|
+
<button
|
|
79
|
+
onClick={() => signOut()}
|
|
80
|
+
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors shadow-lg hover:shadow-xl"
|
|
81
|
+
>
|
|
82
|
+
Sign Out
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex items-center justify-center p-4">
|
|
92
|
+
<div className="bg-gray-800/50 backdrop-blur-sm rounded-2xl shadow-2xl p-8 w-full max-w-md border border-gray-700/50">
|
|
93
|
+
<h1 className="text-2xl font-bold text-white mb-6 text-center">
|
|
94
|
+
WorkOS Authentication
|
|
95
|
+
</h1>
|
|
96
|
+
<p className="text-gray-400 text-center mb-6">
|
|
97
|
+
Sign in to view your profile information
|
|
98
|
+
</p>
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => signIn()}
|
|
101
|
+
disabled={isLoading}
|
|
102
|
+
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
|
|
103
|
+
>
|
|
104
|
+
Sign In with AuthKit
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "WorkOS",
|
|
3
|
+
"description": "Add WorkOS authentication to your application.",
|
|
4
|
+
"phase": "add-on",
|
|
5
|
+
"modes": ["file-router"],
|
|
6
|
+
"type": "add-on",
|
|
7
|
+
"link": "https://workos.com",
|
|
8
|
+
"tailwind": true,
|
|
9
|
+
"routes": [
|
|
10
|
+
{
|
|
11
|
+
"icon": "CircleUserRound",
|
|
12
|
+
"url": "/demo/workos",
|
|
13
|
+
"name": "WorkOS",
|
|
14
|
+
"path": "src/routes/demo.workos.tsx",
|
|
15
|
+
"jsName": "WorkOSDemo"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"integrations": [
|
|
19
|
+
{
|
|
20
|
+
"type": "header-user",
|
|
21
|
+
"jsName": "WorkOSHeader",
|
|
22
|
+
"path": "src/components/workos-user.tsx"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "provider",
|
|
26
|
+
"jsName": "WorkOSProvider",
|
|
27
|
+
"path": "src/integrations/workos/provider.tsx"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<ellipse cx="16.0003" cy="16" rx="4.99998" ry="5" fill="#9785FF" style="fill:#9785FF;fill:color(display-p3 0.5922 0.5216 1.0000);fill-opacity:1;"/>
|
|
3
|
+
<path d="M25.0091 27.8382C25.4345 28.2636 25.3918 28.9679 24.8919 29.3027C22.3488 31.0062 19.2899 31.9997 15.9991 31.9997C12.7082 31.9997 9.64935 31.0062 7.10616 29.3027C6.60633 28.9679 6.56361 28.2636 6.98901 27.8382L10.6429 24.1843C10.9732 23.854 11.4855 23.8019 11.9012 24.0148C13.1303 24.6445 14.5232 24.9997 15.9991 24.9997C17.4749 24.9997 18.8678 24.6445 20.0969 24.0148C20.5126 23.8019 21.0249 23.854 21.3552 24.1843L25.0091 27.8382Z" fill="#9785FF" style="fill:#9785FF;fill:color(display-p3 0.5922 0.5216 1.0000);fill-opacity:1;"/>
|
|
4
|
+
<path opacity="0.6" d="M24.8928 2.697C25.3926 3.0318 25.4353 3.73609 25.0099 4.16149L21.356 7.81544C21.0258 8.14569 20.5134 8.19785 20.0978 7.98491C18.8687 7.35525 17.4758 7 15.9999 7C11.0294 7 6.99997 11.0294 6.99997 16C6.99997 17.4759 7.35522 18.8688 7.98488 20.0979C8.19782 20.5136 8.14565 21.0259 7.81541 21.3561L4.16147 25.0101C3.73607 25.4355 3.03178 25.3927 2.69698 24.8929C0.993522 22.3497 0 19.2909 0 16C0 7.16344 7.16341 0 15.9999 0C19.2908 0 22.3496 0.993529 24.8928 2.697Z" fill="#9785FF" style="fill:#9785FF;fill:color(display-p3 0.5922 0.5216 1.0000);fill-opacity:1;"/>
|
|
5
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/cta-framework-react-cra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "CTA Framework for React (Create React App)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"author": "Jack Herrington <jherr@pobox.com>",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@tanstack/cta-engine": "0.
|
|
26
|
+
"@tanstack/cta-engine": "0.29.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^24.6.0",
|