@odvi/create-dtt-framework 0.1.3 → 0.1.6

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 (111) hide show
  1. package/dist/commands/create.d.ts.map +1 -1
  2. package/dist/commands/create.js +16 -13
  3. package/dist/commands/create.js.map +1 -1
  4. package/package.json +3 -2
  5. package/template/.env.example +106 -0
  6. package/template/components.json +22 -0
  7. package/template/docs/framework/01-overview.md +289 -0
  8. package/template/docs/framework/02-techstack.md +503 -0
  9. package/template/docs/framework/api-layer.md +681 -0
  10. package/template/docs/framework/clerk-authentication.md +649 -0
  11. package/template/docs/framework/cli-installation.md +564 -0
  12. package/template/docs/framework/deployment/ci-cd.md +907 -0
  13. package/template/docs/framework/deployment/digitalocean.md +991 -0
  14. package/template/docs/framework/deployment/domain-setup.md +972 -0
  15. package/template/docs/framework/deployment/environment-variables.md +862 -0
  16. package/template/docs/framework/deployment/monitoring.md +927 -0
  17. package/template/docs/framework/deployment/production-checklist.md +649 -0
  18. package/template/docs/framework/deployment/vercel.md +791 -0
  19. package/template/docs/framework/environment-variables.md +646 -0
  20. package/template/docs/framework/health-check-system.md +583 -0
  21. package/template/docs/framework/implementation.md +559 -0
  22. package/template/docs/framework/snowflake-integration.md +594 -0
  23. package/template/docs/framework/state-management.md +615 -0
  24. package/template/docs/framework/supabase-integration.md +582 -0
  25. package/template/docs/framework/testing-guide.md +544 -0
  26. package/template/docs/framework/what-did-i-miss.md +526 -0
  27. package/template/drizzle.config.ts +11 -0
  28. package/template/next.config.js +21 -0
  29. package/template/postcss.config.js +5 -0
  30. package/template/prettier.config.js +4 -0
  31. package/template/public/favicon.ico +0 -0
  32. package/template/src/app/(auth)/layout.tsx +4 -0
  33. package/template/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +10 -0
  34. package/template/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +10 -0
  35. package/template/src/app/(dashboard)/dashboard/page.tsx +8 -0
  36. package/template/src/app/(dashboard)/health/page.tsx +16 -0
  37. package/template/src/app/(dashboard)/layout.tsx +17 -0
  38. package/template/src/app/api/[[...route]]/route.ts +11 -0
  39. package/template/src/app/api/debug-files/route.ts +33 -0
  40. package/template/src/app/api/webhooks/clerk/route.ts +112 -0
  41. package/template/src/app/layout.tsx +28 -0
  42. package/template/src/app/page.tsx +12 -0
  43. package/template/src/app/providers.tsx +20 -0
  44. package/template/src/components/layouts/navbar.tsx +14 -0
  45. package/template/src/components/shared/loading-spinner.tsx +6 -0
  46. package/template/src/components/ui/badge.tsx +46 -0
  47. package/template/src/components/ui/button.tsx +62 -0
  48. package/template/src/components/ui/card.tsx +92 -0
  49. package/template/src/components/ui/collapsible.tsx +33 -0
  50. package/template/src/components/ui/scroll-area.tsx +58 -0
  51. package/template/src/components/ui/sheet.tsx +139 -0
  52. package/template/src/config/__tests__/env.test.ts +164 -0
  53. package/template/src/config/__tests__/site.test.ts +46 -0
  54. package/template/src/config/env.ts +36 -0
  55. package/template/src/config/site.ts +10 -0
  56. package/template/src/env.js +44 -0
  57. package/template/src/features/__tests__/health-check-config.test.ts +142 -0
  58. package/template/src/features/__tests__/health-check-types.test.ts +201 -0
  59. package/template/src/features/documentation/components/doc-sidebar.tsx +109 -0
  60. package/template/src/features/documentation/components/doc-viewer.tsx +70 -0
  61. package/template/src/features/documentation/index.tsx +92 -0
  62. package/template/src/features/documentation/utils/doc-loader.ts +177 -0
  63. package/template/src/features/health-check/components/health-dashboard.tsx +374 -0
  64. package/template/src/features/health-check/config.ts +71 -0
  65. package/template/src/features/health-check/index.ts +4 -0
  66. package/template/src/features/health-check/stores/health-store.ts +14 -0
  67. package/template/src/features/health-check/types.ts +18 -0
  68. package/template/src/hooks/__tests__/use-debounce.test.tsx +28 -0
  69. package/template/src/hooks/queries/use-health-checks.ts +16 -0
  70. package/template/src/hooks/utils/use-debounce.ts +20 -0
  71. package/template/src/lib/__tests__/utils.test.ts +52 -0
  72. package/template/src/lib/__tests__/validators.test.ts +114 -0
  73. package/template/src/lib/nextbank/client.ts +67 -0
  74. package/template/src/lib/snowflake/client.ts +102 -0
  75. package/template/src/lib/supabase/admin.ts +7 -0
  76. package/template/src/lib/supabase/client.ts +7 -0
  77. package/template/src/lib/supabase/server.ts +23 -0
  78. package/template/src/lib/utils.ts +6 -0
  79. package/template/src/lib/validators.ts +9 -0
  80. package/template/src/middleware.ts +22 -0
  81. package/template/src/server/api/index.ts +22 -0
  82. package/template/src/server/api/middleware/auth.ts +19 -0
  83. package/template/src/server/api/middleware/logger.ts +4 -0
  84. package/template/src/server/api/routes/health/clerk.ts +214 -0
  85. package/template/src/server/api/routes/health/database.ts +141 -0
  86. package/template/src/server/api/routes/health/edge-functions.ts +107 -0
  87. package/template/src/server/api/routes/health/framework.ts +48 -0
  88. package/template/src/server/api/routes/health/index.ts +102 -0
  89. package/template/src/server/api/routes/health/nextbank.ts +46 -0
  90. package/template/src/server/api/routes/health/snowflake.ts +83 -0
  91. package/template/src/server/api/routes/health/storage.ts +177 -0
  92. package/template/src/server/api/routes/users.ts +79 -0
  93. package/template/src/server/db/index.ts +17 -0
  94. package/template/src/server/db/queries/users.ts +8 -0
  95. package/template/src/server/db/schema/__tests__/health-checks.test.ts +31 -0
  96. package/template/src/server/db/schema/__tests__/users.test.ts +46 -0
  97. package/template/src/server/db/schema/health-checks.ts +11 -0
  98. package/template/src/server/db/schema/index.ts +2 -0
  99. package/template/src/server/db/schema/users.ts +16 -0
  100. package/template/src/server/db/schema.ts +1 -0
  101. package/template/src/stores/__tests__/ui-store.test.ts +87 -0
  102. package/template/src/stores/ui-store.ts +14 -0
  103. package/template/src/styles/globals.css +129 -0
  104. package/template/src/test/mocks/clerk.ts +35 -0
  105. package/template/src/test/mocks/snowflake.ts +28 -0
  106. package/template/src/test/mocks/supabase.ts +37 -0
  107. package/template/src/test/setup.ts +69 -0
  108. package/template/src/test/utils/test-helpers.ts +158 -0
  109. package/template/src/types/index.ts +14 -0
  110. package/template/tsconfig.json +43 -0
  111. package/template/vitest.config.ts +44 -0
@@ -0,0 +1,177 @@
1
+ import { Hono } from 'hono'
2
+ import { supabaseAdmin } from '@/lib/supabase/admin'
3
+ import { env } from '@/config/env'
4
+
5
+ const BUCKET_NAME = 'health-check-bucket'
6
+ const TEST_FILE_PATH = 'health-check/test-file.txt'
7
+
8
+ export const storageHealthRoutes = new Hono()
9
+
10
+ storageHealthRoutes.post('/upload', async (c) => {
11
+ const start = performance.now()
12
+
13
+ try {
14
+ // Ensure bucket exists
15
+ const { data: buckets } = await supabaseAdmin.storage.listBuckets()
16
+ const bucketExists = buckets?.some((b) => b.name === BUCKET_NAME)
17
+
18
+ if (!bucketExists) {
19
+ // Create bucket if it doesn't exist
20
+ const { error: createError } = await supabaseAdmin.storage.createBucket(
21
+ BUCKET_NAME,
22
+ {
23
+ public: false,
24
+ }
25
+ )
26
+ if (createError) throw createError
27
+ }
28
+
29
+ // Upload test file
30
+ const testContent = `Health check test file - ${new Date().toISOString()}`
31
+ const { data, error } = await supabaseAdmin.storage
32
+ .from(BUCKET_NAME)
33
+ .upload(TEST_FILE_PATH, new Blob([testContent], { type: 'text/plain' }), {
34
+ upsert: true,
35
+ })
36
+
37
+ if (error) throw error
38
+
39
+ return c.json({
40
+ status: 'healthy',
41
+ responseTimeMs: Math.round(performance.now() - start),
42
+ message: 'Successfully uploaded test file to storage',
43
+ data: {
44
+ path: data?.path,
45
+ bucket: BUCKET_NAME,
46
+ },
47
+ })
48
+ } catch (error) {
49
+ return c.json(
50
+ {
51
+ status: 'error',
52
+ responseTimeMs: Math.round(performance.now() - start),
53
+ error: error instanceof Error ? error.message : 'Failed to upload to storage',
54
+ },
55
+ 500
56
+ )
57
+ }
58
+ })
59
+
60
+ storageHealthRoutes.get('/download', async (c) => {
61
+ const start = performance.now()
62
+
63
+ try {
64
+ // Check if file exists
65
+ const { data: fileData, error: checkError } = await supabaseAdmin.storage
66
+ .from(BUCKET_NAME)
67
+ .list('health-check', {
68
+ limit: 10,
69
+ })
70
+
71
+ if (checkError) throw checkError
72
+
73
+ const testFile = fileData?.find((f) => f.name === 'test-file.txt')
74
+
75
+ if (!testFile) {
76
+ return c.json({
77
+ status: 'healthy',
78
+ responseTimeMs: Math.round(performance.now() - start),
79
+ message: 'Storage connection successful, but no test file found',
80
+ data: { found: false },
81
+ })
82
+ }
83
+
84
+ // Get signed URL for download
85
+ const { data, error } = await supabaseAdmin.storage
86
+ .from(BUCKET_NAME)
87
+ .createSignedUrl(TEST_FILE_PATH, 60)
88
+
89
+ // Handle "Object not found" as a success state for health check
90
+ // since connectivity is working, but the file just isn't there
91
+ if (error && error.message.includes('Object not found')) {
92
+ return c.json({
93
+ status: 'healthy',
94
+ responseTimeMs: Math.round(performance.now() - start),
95
+ message: 'Storage connection successful, but no test file found to download',
96
+ data: {
97
+ found: false,
98
+ path: TEST_FILE_PATH,
99
+ },
100
+ })
101
+ }
102
+
103
+ if (error) throw error
104
+
105
+ return c.json({
106
+ status: 'healthy',
107
+ responseTimeMs: Math.round(performance.now() - start),
108
+ message: 'Successfully generated download URL for test file',
109
+ data: {
110
+ found: true,
111
+ signedUrl: data?.signedUrl,
112
+ path: TEST_FILE_PATH,
113
+ },
114
+ })
115
+ } catch (error) {
116
+ return c.json(
117
+ {
118
+ status: 'error',
119
+ responseTimeMs: Math.round(performance.now() - start),
120
+ error: error instanceof Error ? error.message : 'Failed to download from storage',
121
+ },
122
+ 500
123
+ )
124
+ }
125
+ })
126
+
127
+ storageHealthRoutes.delete('/delete', async (c) => {
128
+ const start = performance.now()
129
+
130
+ try {
131
+ // Check if file exists first
132
+ const { data: fileData, error: checkError } = await supabaseAdmin.storage
133
+ .from(BUCKET_NAME)
134
+ .list('health-check', {
135
+ limit: 10,
136
+ })
137
+
138
+ if (checkError) throw checkError
139
+
140
+ const testFile = fileData?.find((f) => f.name === 'test-file.txt')
141
+
142
+ if (!testFile) {
143
+ return c.json({
144
+ status: 'healthy',
145
+ responseTimeMs: Math.round(performance.now() - start),
146
+ message: 'Storage connection successful, but no test file to delete',
147
+ data: { deleted: false },
148
+ })
149
+ }
150
+
151
+ // Delete test file
152
+ const { error } = await supabaseAdmin.storage
153
+ .from(BUCKET_NAME)
154
+ .remove([TEST_FILE_PATH])
155
+
156
+ if (error) throw error
157
+
158
+ return c.json({
159
+ status: 'healthy',
160
+ responseTimeMs: Math.round(performance.now() - start),
161
+ message: 'Successfully deleted test file from storage',
162
+ data: {
163
+ deleted: true,
164
+ path: TEST_FILE_PATH,
165
+ },
166
+ })
167
+ } catch (error) {
168
+ return c.json(
169
+ {
170
+ status: 'error',
171
+ responseTimeMs: Math.round(performance.now() - start),
172
+ error: error instanceof Error ? error.message : 'Failed to delete from storage',
173
+ },
174
+ 500
175
+ )
176
+ }
177
+ })
@@ -0,0 +1,79 @@
1
+ import { Hono } from 'hono'
2
+ import { supabaseAdmin } from '@/lib/supabase/admin'
3
+
4
+ export const usersRoutes = new Hono()
5
+
6
+ usersRoutes.get('/', async (c) => {
7
+ const start = performance.now()
8
+
9
+ try {
10
+ const { data: allUsers, error } = await supabaseAdmin
11
+ .from('users')
12
+ .select('id, email, first_name, last_name, image_url, clerk_org_id, created_at, updated_at')
13
+ .order('created_at', { ascending: false })
14
+ .limit(100)
15
+
16
+ if (error) throw error
17
+
18
+ return c.json({
19
+ status: 'healthy',
20
+ responseTimeMs: Math.round(performance.now() - start),
21
+ message: 'Successfully retrieved users',
22
+ data: {
23
+ users: allUsers,
24
+ count: allUsers.length,
25
+ },
26
+ })
27
+ } catch (error) {
28
+ return c.json(
29
+ {
30
+ status: 'error',
31
+ responseTimeMs: Math.round(performance.now() - start),
32
+ error: error instanceof Error ? error.message : 'Failed to retrieve users',
33
+ },
34
+ 500
35
+ )
36
+ }
37
+ })
38
+
39
+ usersRoutes.get('/:id', async (c) => {
40
+ const start = performance.now()
41
+ const id = c.req.param('id')
42
+
43
+ try {
44
+ const { data: user, error } = await supabaseAdmin
45
+ .from('users')
46
+ .select('id, email, first_name, last_name, image_url, clerk_org_id, created_at, updated_at')
47
+ .eq('id', id)
48
+ .limit(1)
49
+
50
+ if (error) throw error
51
+
52
+ if (!user || user.length === 0) {
53
+ return c.json(
54
+ {
55
+ status: 'error',
56
+ responseTimeMs: Math.round(performance.now() - start),
57
+ error: 'User not found',
58
+ },
59
+ 404
60
+ )
61
+ }
62
+
63
+ return c.json({
64
+ status: 'healthy',
65
+ responseTimeMs: Math.round(performance.now() - start),
66
+ message: 'Successfully retrieved user',
67
+ data: { user: user[0] },
68
+ })
69
+ } catch (error) {
70
+ return c.json(
71
+ {
72
+ status: 'error',
73
+ responseTimeMs: Math.round(performance.now() - start),
74
+ error: error instanceof Error ? error.message : 'Failed to retrieve user',
75
+ },
76
+ 500
77
+ )
78
+ }
79
+ })
@@ -0,0 +1,17 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js'
2
+ import postgres from 'postgres'
3
+ import { env } from '@/config/env'
4
+ import * as schema from './schema'
5
+
6
+ /**
7
+ * Cache the database connection in development. This avoids creating a new connection on every HMR
8
+ * update.
9
+ */
10
+ const globalForDb = globalThis as unknown as {
11
+ conn: postgres.Sql | undefined
12
+ }
13
+
14
+ const conn = globalForDb.conn ?? postgres(env.DATABASE_URL, { prepare: false })
15
+ if (env.NODE_ENV !== 'production') globalForDb.conn = conn
16
+
17
+ export const db = drizzle(conn, { schema })
@@ -0,0 +1,8 @@
1
+ import { db } from '../index'
2
+ import { users } from '../schema/users'
3
+ import { eq } from 'drizzle-orm'
4
+
5
+ export async function getUserById(id: string) {
6
+ const user = await db.select().from(users).where(eq(users.id, id)).limit(1)
7
+ return user[0] ?? null
8
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { healthCheckTests, type HealthCheckTest } from '../health-checks';
3
+
4
+ describe('healthCheckTests schema', () => {
5
+ it('should have healthCheckTests table defined', () => {
6
+ expect(healthCheckTests).toBeDefined();
7
+ });
8
+
9
+ it('should export HealthCheckTest type', () => {
10
+ const healthCheckTest: HealthCheckTest = {
11
+ id: 'test-uuid',
12
+ testKey: 'test-key',
13
+ testValue: 'test-value',
14
+ createdAt: new Date(),
15
+ };
16
+ expect(healthCheckTest).toBeDefined();
17
+ expect(healthCheckTest.testKey).toBe('test-key');
18
+ });
19
+
20
+ it('should allow HealthCheckTest with null testValue', () => {
21
+ const healthCheckTest: HealthCheckTest = {
22
+ id: 'test-uuid',
23
+ testKey: 'test-key',
24
+ testValue: null,
25
+ createdAt: new Date(),
26
+ };
27
+ expect(healthCheckTest).toBeDefined();
28
+ expect(healthCheckTest.testKey).toBe('test-key');
29
+ expect(healthCheckTest.testValue).toBeNull();
30
+ });
31
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { users, type User, type NewUser } from '../users';
3
+
4
+ describe('users schema', () => {
5
+ it('should have users table defined', () => {
6
+ expect(users).toBeDefined();
7
+ });
8
+
9
+ it('should export User type', () => {
10
+ const user: User = {
11
+ id: 'test-id',
12
+ email: 'test@example.com',
13
+ firstName: 'Test',
14
+ lastName: 'User',
15
+ imageUrl: 'https://example.com/image.jpg',
16
+ clerkOrgId: 'org-123',
17
+ createdAt: new Date(),
18
+ updatedAt: new Date(),
19
+ };
20
+ expect(user).toBeDefined();
21
+ expect(user.id).toBe('test-id');
22
+ });
23
+
24
+ it('should export NewUser type', () => {
25
+ const newUser: NewUser = {
26
+ id: 'test-id',
27
+ email: 'test@example.com',
28
+ firstName: 'Test',
29
+ lastName: 'User',
30
+ imageUrl: 'https://example.com/image.jpg',
31
+ clerkOrgId: 'org-123',
32
+ };
33
+ expect(newUser).toBeDefined();
34
+ expect(newUser.email).toBe('test@example.com');
35
+ });
36
+
37
+ it('should allow NewUser without optional fields', () => {
38
+ const newUser: NewUser = {
39
+ id: 'test-id',
40
+ email: 'test@example.com',
41
+ };
42
+ expect(newUser).toBeDefined();
43
+ expect(newUser.id).toBe('test-id');
44
+ expect(newUser.email).toBe('test@example.com');
45
+ });
46
+ });
@@ -0,0 +1,11 @@
1
+ // Health checks table schema placeholder
2
+ import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
3
+
4
+ export const healthCheckTests = pgTable('health_check_tests', {
5
+ id: uuid('id').primaryKey().defaultRandom(),
6
+ testKey: text('test_key').notNull(),
7
+ testValue: text('test_value'),
8
+ createdAt: timestamp('created_at').defaultNow().notNull(),
9
+ })
10
+
11
+ export type HealthCheckTest = typeof healthCheckTests.$inferSelect
@@ -0,0 +1,2 @@
1
+ export * from './users'
2
+ export * from './health-checks'
@@ -0,0 +1,16 @@
1
+ // Users table schema placeholder
2
+ import { pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core'
3
+
4
+ export const users = pgTable('users', {
5
+ id: text('id').primaryKey(),
6
+ email: varchar('email', { length: 255 }).notNull().unique(),
7
+ firstName: varchar('first_name', { length: 255 }),
8
+ lastName: varchar('last_name', { length: 255 }),
9
+ imageUrl: text('image_url'),
10
+ clerkOrgId: text('clerk_org_id'),
11
+ createdAt: timestamp('created_at').defaultNow().notNull(),
12
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
13
+ })
14
+
15
+ export type User = typeof users.$inferSelect
16
+ export type NewUser = typeof users.$inferInsert
@@ -0,0 +1 @@
1
+ export * from "./schema/index";
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { useUIStore } from '../ui-store';
3
+
4
+ describe('useUIStore', () => {
5
+ beforeEach(() => {
6
+ // Reset store state before each test
7
+ useUIStore.setState({ sidebarOpen: false });
8
+ });
9
+
10
+ it('should have initial state with sidebar closed', () => {
11
+ const state = useUIStore.getState();
12
+ expect(state.sidebarOpen).toBe(false);
13
+ });
14
+
15
+ it('should toggle sidebar from closed to open', () => {
16
+ const { toggleSidebar } = useUIStore.getState();
17
+ toggleSidebar();
18
+
19
+ const state = useUIStore.getState();
20
+ expect(state.sidebarOpen).toBe(true);
21
+ });
22
+
23
+ it('should toggle sidebar from open to closed', () => {
24
+ useUIStore.setState({ sidebarOpen: true });
25
+ const { toggleSidebar } = useUIStore.getState();
26
+ toggleSidebar();
27
+
28
+ const state = useUIStore.getState();
29
+ expect(state.sidebarOpen).toBe(false);
30
+ });
31
+
32
+ it('should toggle sidebar multiple times', () => {
33
+ const { toggleSidebar } = useUIStore.getState();
34
+
35
+ toggleSidebar();
36
+ expect(useUIStore.getState().sidebarOpen).toBe(true);
37
+
38
+ toggleSidebar();
39
+ expect(useUIStore.getState().sidebarOpen).toBe(false);
40
+
41
+ toggleSidebar();
42
+ expect(useUIStore.getState().sidebarOpen).toBe(true);
43
+
44
+ toggleSidebar();
45
+ expect(useUIStore.getState().sidebarOpen).toBe(false);
46
+ });
47
+
48
+ it('should provide toggleSidebar function', () => {
49
+ const state = useUIStore.getState();
50
+ expect(state.toggleSidebar).toBeDefined();
51
+ expect(typeof state.toggleSidebar).toBe('function');
52
+ });
53
+
54
+ it('should maintain state consistency across multiple store accesses', () => {
55
+ const state1 = useUIStore.getState();
56
+ const state2 = useUIStore.getState();
57
+
58
+ expect(state1.sidebarOpen).toBe(state2.sidebarOpen);
59
+ expect(state1.toggleSidebar).toBe(state2.toggleSidebar);
60
+ });
61
+
62
+ it('should allow direct state setting', () => {
63
+ useUIStore.setState({ sidebarOpen: true });
64
+
65
+ const state = useUIStore.getState();
66
+ expect(state.sidebarOpen).toBe(true);
67
+ });
68
+
69
+ it('should handle rapid state changes', () => {
70
+ const { toggleSidebar } = useUIStore.getState();
71
+
72
+ for (let i = 0; i < 10; i++) {
73
+ toggleSidebar();
74
+ }
75
+
76
+ const state = useUIStore.getState();
77
+ expect(state.sidebarOpen).toBe(false); // 10 toggles from false = false
78
+ });
79
+
80
+ it('should reset to initial state when explicitly set', () => {
81
+ useUIStore.setState({ sidebarOpen: true });
82
+ expect(useUIStore.getState().sidebarOpen).toBe(true);
83
+
84
+ useUIStore.setState({ sidebarOpen: false });
85
+ expect(useUIStore.getState().sidebarOpen).toBe(false);
86
+ });
87
+ });
@@ -0,0 +1,14 @@
1
+ // UI store placeholder
2
+ 'use client'
3
+
4
+ import { create } from 'zustand'
5
+
6
+ interface UIState {
7
+ sidebarOpen: boolean
8
+ toggleSidebar: () => void
9
+ }
10
+
11
+ export const useUIStore = create<UIState>((set) => ({
12
+ sidebarOpen: false,
13
+ toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
14
+ }))
@@ -0,0 +1,129 @@
1
+ @import "tailwindcss";
2
+ @plugin "@tailwindcss/typography";
3
+ @import "tw-animate-css";
4
+
5
+ @custom-variant dark (&:is(.dark *));
6
+
7
+ @theme {
8
+ --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
9
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
10
+ }
11
+
12
+ @theme inline {
13
+ --radius-sm: calc(var(--radius) - 4px);
14
+ --radius-md: calc(var(--radius) - 2px);
15
+ --radius-lg: var(--radius);
16
+ --radius-xl: calc(var(--radius) + 4px);
17
+ --radius-2xl: calc(var(--radius) + 8px);
18
+ --radius-3xl: calc(var(--radius) + 12px);
19
+ --radius-4xl: calc(var(--radius) + 16px);
20
+ --color-background: var(--background);
21
+ --color-foreground: var(--foreground);
22
+ --color-card: var(--card);
23
+ --color-card-foreground: var(--card-foreground);
24
+ --color-popover: var(--popover);
25
+ --color-popover-foreground: var(--popover-foreground);
26
+ --color-primary: var(--primary);
27
+ --color-primary-foreground: var(--primary-foreground);
28
+ --color-secondary: var(--secondary);
29
+ --color-secondary-foreground: var(--secondary-foreground);
30
+ --color-muted: var(--muted);
31
+ --color-muted-foreground: var(--muted-foreground);
32
+ --color-accent: var(--accent);
33
+ --color-accent-foreground: var(--accent-foreground);
34
+ --color-destructive: var(--destructive);
35
+ --color-border: var(--border);
36
+ --color-input: var(--input);
37
+ --color-ring: var(--ring);
38
+ --color-chart-1: var(--chart-1);
39
+ --color-chart-2: var(--chart-2);
40
+ --color-chart-3: var(--chart-3);
41
+ --color-chart-4: var(--chart-4);
42
+ --color-chart-5: var(--chart-5);
43
+ --color-sidebar: var(--sidebar);
44
+ --color-sidebar-foreground: var(--sidebar-foreground);
45
+ --color-sidebar-primary: var(--sidebar-primary);
46
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
47
+ --color-sidebar-accent: var(--sidebar-accent);
48
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
49
+ --color-sidebar-border: var(--sidebar-border);
50
+ --color-sidebar-ring: var(--sidebar-ring);
51
+ }
52
+
53
+ :root {
54
+ --radius: 0.625rem;
55
+ --background: oklch(1 0 0);
56
+ --foreground: oklch(0.145 0 0);
57
+ --card: oklch(1 0 0);
58
+ --card-foreground: oklch(0.145 0 0);
59
+ --popover: oklch(1 0 0);
60
+ --popover-foreground: oklch(0.145 0 0);
61
+ --primary: oklch(0.205 0 0);
62
+ --primary-foreground: oklch(0.985 0 0);
63
+ --secondary: oklch(0.97 0 0);
64
+ --secondary-foreground: oklch(0.205 0 0);
65
+ --muted: oklch(0.97 0 0);
66
+ --muted-foreground: oklch(0.556 0 0);
67
+ --accent: oklch(0.97 0 0);
68
+ --accent-foreground: oklch(0.205 0 0);
69
+ --destructive: oklch(0.577 0.245 27.325);
70
+ --border: oklch(0.922 0 0);
71
+ --input: oklch(0.922 0 0);
72
+ --ring: oklch(0.708 0 0);
73
+ --chart-1: oklch(0.646 0.222 41.116);
74
+ --chart-2: oklch(0.6 0.118 184.704);
75
+ --chart-3: oklch(0.398 0.07 227.392);
76
+ --chart-4: oklch(0.828 0.189 84.429);
77
+ --chart-5: oklch(0.769 0.188 70.08);
78
+ --sidebar: oklch(0.985 0 0);
79
+ --sidebar-foreground: oklch(0.145 0 0);
80
+ --sidebar-primary: oklch(0.205 0 0);
81
+ --sidebar-primary-foreground: oklch(0.985 0 0);
82
+ --sidebar-accent: oklch(0.97 0 0);
83
+ --sidebar-accent-foreground: oklch(0.205 0 0);
84
+ --sidebar-border: oklch(0.922 0 0);
85
+ --sidebar-ring: oklch(0.708 0 0);
86
+ }
87
+
88
+ .dark {
89
+ --background: oklch(0.145 0 0);
90
+ --foreground: oklch(0.985 0 0);
91
+ --card: oklch(0.205 0 0);
92
+ --card-foreground: oklch(0.985 0 0);
93
+ --popover: oklch(0.205 0 0);
94
+ --popover-foreground: oklch(0.985 0 0);
95
+ --primary: oklch(0.922 0 0);
96
+ --primary-foreground: oklch(0.205 0 0);
97
+ --secondary: oklch(0.269 0 0);
98
+ --secondary-foreground: oklch(0.985 0 0);
99
+ --muted: oklch(0.269 0 0);
100
+ --muted-foreground: oklch(0.708 0 0);
101
+ --accent: oklch(0.269 0 0);
102
+ --accent-foreground: oklch(0.985 0 0);
103
+ --destructive: oklch(0.704 0.191 22.216);
104
+ --border: oklch(1 0 0 / 10%);
105
+ --input: oklch(1 0 0 / 15%);
106
+ --ring: oklch(0.556 0 0);
107
+ --chart-1: oklch(0.488 0.243 264.376);
108
+ --chart-2: oklch(0.696 0.17 162.48);
109
+ --chart-3: oklch(0.769 0.188 70.08);
110
+ --chart-4: oklch(0.627 0.265 303.9);
111
+ --chart-5: oklch(0.645 0.246 16.439);
112
+ --sidebar: oklch(0.205 0 0);
113
+ --sidebar-foreground: oklch(0.985 0 0);
114
+ --sidebar-primary: oklch(0.488 0.243 264.376);
115
+ --sidebar-primary-foreground: oklch(0.985 0 0);
116
+ --sidebar-accent: oklch(0.269 0 0);
117
+ --sidebar-accent-foreground: oklch(0.985 0 0);
118
+ --sidebar-border: oklch(1 0 0 / 10%);
119
+ --sidebar-ring: oklch(0.556 0 0);
120
+ }
121
+
122
+ @layer base {
123
+ * {
124
+ @apply border-border outline-ring/50;
125
+ }
126
+ body {
127
+ @apply bg-background text-foreground;
128
+ }
129
+ }
@@ -0,0 +1,35 @@
1
+ import { vi } from 'vitest';
2
+
3
+ // Mock Clerk
4
+ export const mockClerkClient = {
5
+ users: {
6
+ getUser: vi.fn(),
7
+ getUserList: vi.fn(),
8
+ },
9
+ organizations: {
10
+ getOrganizationMembershipList: vi.fn(),
11
+ getOrganizationList: vi.fn(),
12
+ },
13
+ };
14
+
15
+ vi.mock('@clerk/nextjs', () => ({
16
+ auth: vi.fn(),
17
+ currentUser: vi.fn(),
18
+ ClerkProvider: ({ children }: { children: React.ReactNode }) => children,
19
+ SignedIn: ({ children }: { children: React.ReactNode }) => children,
20
+ SignedOut: ({ children }: { children: React.ReactNode }) => null,
21
+ UserButton: vi.fn(() => null),
22
+ SignIn: vi.fn(() => null),
23
+ SignUp: vi.fn(() => null),
24
+ useUser: vi.fn(() => ({
25
+ user: null,
26
+ isLoaded: true,
27
+ })),
28
+ useAuth: vi.fn(() => ({
29
+ userId: null,
30
+ sessionId: null,
31
+ isLoaded: true,
32
+ })),
33
+ }));
34
+
35
+ export default mockClerkClient;
@@ -0,0 +1,28 @@
1
+ import { vi } from 'vitest';
2
+
3
+ // Mock Snowflake connection
4
+ export const mockSnowflakeConnection = {
5
+ connect: vi.fn((callback) => {
6
+ callback(null, mockSnowflakeConnection);
7
+ }),
8
+ execute: vi.fn((options: any) => {
9
+ if (options.complete) {
10
+ options.complete(null, null, []);
11
+ }
12
+ }),
13
+ destroy: vi.fn((callback) => {
14
+ callback(null);
15
+ }),
16
+ isUp: vi.fn(() => true),
17
+ getId: vi.fn(() => 'test-connection-id'),
18
+ };
19
+
20
+ // Mock Snowflake SDK
21
+ export const mockSnowflakeSDK = {
22
+ createConnection: vi.fn(() => mockSnowflakeConnection),
23
+ createConnector: vi.fn(),
24
+ };
25
+
26
+ vi.mock('snowflake-sdk', () => mockSnowflakeSDK);
27
+
28
+ export default mockSnowflakeConnection;