create-edhor-stack 0.1.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/LICENSE +21 -0
- package/README.md +75 -0
- package/STACK.md +1086 -0
- package/dist/index.js +3181 -0
- package/package.json +44 -0
- package/templates/apps/api-elysia/package.json +21 -0
- package/templates/apps/api-elysia/src/index.ts +59 -0
- package/templates/apps/api-elysia/src/lib/eden.ts +25 -0
- package/templates/apps/api-elysia/src/lib/env.ts +18 -0
- package/templates/apps/api-elysia/src/routes/health.ts +13 -0
- package/templates/apps/api-elysia/src/routes/users.ts +117 -0
- package/templates/apps/api-elysia/tsconfig.json +15 -0
- package/templates/apps/api-hono/package.json +20 -0
- package/templates/apps/api-hono/src/index.ts +66 -0
- package/templates/apps/api-hono/src/lib/env.ts +18 -0
- package/templates/apps/api-hono/src/routes/health.ts +20 -0
- package/templates/apps/api-hono/src/routes/users.ts +110 -0
- package/templates/apps/api-hono/tsconfig.json +15 -0
- package/templates/apps/mobile/.env.example +9 -0
- package/templates/apps/mobile/app/_layout.tsx +16 -0
- package/templates/apps/mobile/app/index.tsx +39 -0
- package/templates/apps/mobile/app.json +37 -0
- package/templates/apps/mobile/assets/adaptive-icon.png +0 -0
- package/templates/apps/mobile/assets/favicon.png +0 -0
- package/templates/apps/mobile/assets/icon.png +0 -0
- package/templates/apps/mobile/assets/splash-icon.png +0 -0
- package/templates/apps/mobile/package.json +39 -0
- package/templates/apps/mobile/src/api/client.ts +51 -0
- package/templates/apps/mobile/src/api/index.ts +3 -0
- package/templates/apps/mobile/src/api/queries.ts +24 -0
- package/templates/apps/mobile/src/api/schemas.ts +32 -0
- package/templates/apps/mobile/src/lib/env.ts +40 -0
- package/templates/apps/mobile/src/lib/query-client.ts +28 -0
- package/templates/apps/mobile/src/lib/result.ts +45 -0
- package/templates/apps/mobile/src/lib/store.ts +63 -0
- package/templates/apps/mobile/tsconfig.json +10 -0
- package/templates/apps/web/.env.example +11 -0
- package/templates/apps/web/package.json +29 -0
- package/templates/apps/web/src/lib/env.ts +52 -0
- package/templates/apps/web/src/lib/queries.ts +27 -0
- package/templates/apps/web/src/lib/query-client.ts +11 -0
- package/templates/apps/web/src/router.tsx +17 -0
- package/templates/apps/web/src/routes/__root.tsx +32 -0
- package/templates/apps/web/src/routes/index.tsx +16 -0
- package/templates/apps/web/src/styles.css +26 -0
- package/templates/apps/web/tsconfig.json +10 -0
- package/templates/apps/web/vite.config.ts +21 -0
- package/templates/base/.claude/settings.json +33 -0
- package/templates/base/.claude/skills/add-api-endpoint.md +137 -0
- package/templates/base/.claude/skills/add-component.md +79 -0
- package/templates/base/.claude/skills/add-route.md +134 -0
- package/templates/base/.claude/skills/add-store.md +158 -0
- package/templates/base/.husky/pre-commit +1 -0
- package/templates/base/.lintstagedrc +4 -0
- package/templates/base/.node-version +1 -0
- package/templates/base/AGENTS.md +135 -0
- package/templates/base/CLAUDE.md.hbs +139 -0
- package/templates/base/Dockerfile +32 -0
- package/templates/base/biome.json +52 -0
- package/templates/base/fly.toml.hbs +20 -0
- package/templates/base/gitignore +36 -0
- package/templates/base/package.json.hbs +22 -0
- package/templates/base/tsconfig.json +14 -0
- package/templates/base/turbo.json +22 -0
- package/templates/packages/shared/package.json +17 -0
- package/templates/packages/shared/src/index.ts +4 -0
- package/templates/packages/shared/src/schemas.ts +50 -0
- package/templates/packages/shared/src/types.ts +47 -0
- package/templates/packages/shared/src/utils.ts +87 -0
- package/templates/packages/shared/tsconfig.json +14 -0
- package/templates/packages/stripe/package.json +18 -0
- package/templates/packages/stripe/src/client.ts +110 -0
- package/templates/packages/stripe/src/index.ts +3 -0
- package/templates/packages/stripe/src/schemas.ts +65 -0
- package/templates/packages/stripe/src/webhooks.ts +91 -0
- package/templates/packages/stripe/tsconfig.json +14 -0
- package/templates/packages/ui/components.json +19 -0
- package/templates/packages/ui/package.json +29 -0
- package/templates/packages/ui/src/components/button.tsx +58 -0
- package/templates/packages/ui/src/index.ts +5 -0
- package/templates/packages/ui/src/lib/utils.ts +6 -0
- package/templates/packages/ui/src/styles.css +120 -0
- package/templates/packages/ui/tsconfig.json +10 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{name}}/mobile",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "expo-router/entry",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "expo start",
|
|
8
|
+
"android": "expo run:android",
|
|
9
|
+
"ios": "expo run:ios",
|
|
10
|
+
"build": "eas build"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"expo": "~54.0.0",
|
|
14
|
+
"expo-router": "~6.0.22",
|
|
15
|
+
"expo-status-bar": "~3.0.9",
|
|
16
|
+
"expo-linking": "~8.0.11",
|
|
17
|
+
"expo-constants": "~18.0.13",
|
|
18
|
+
"react": "19.1.0",
|
|
19
|
+
"react-native": "0.81.5",
|
|
20
|
+
"react-native-safe-area-context": "~5.6.0",
|
|
21
|
+
"react-native-screens": "~4.16.0",
|
|
22
|
+
"react-native-gesture-handler": "~2.28.0",
|
|
23
|
+
"react-native-reanimated": "~4.1.1",
|
|
24
|
+
"@tanstack/react-query": "^5.60.0",
|
|
25
|
+
"@tanstack/react-query-persist-client": "^5.60.0",
|
|
26
|
+
"@tanstack/query-async-storage-persister": "^5.60.0",
|
|
27
|
+
"@react-native-async-storage/async-storage": "^2.0.0",
|
|
28
|
+
"zustand": "^5.0.0",
|
|
29
|
+
"zod": "^3.24.0",
|
|
30
|
+
"@t3-oss/env-core": "^0.12.0",
|
|
31
|
+
"@shopify/flash-list": "2.0.2",
|
|
32
|
+
"lucide-react-native": "^0.460.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@babel/core": "^7.25.0",
|
|
36
|
+
"@types/react": "~19.1.10",
|
|
37
|
+
"typescript": "~5.9.2"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { env } from '@/lib/env';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetch with Zod validation - validates API responses at runtime.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const user = await fetchValidated('/users/123', UserSchema);
|
|
11
|
+
* // user is typed as User and validated at runtime
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export async function fetchValidated<T>(
|
|
15
|
+
path: string,
|
|
16
|
+
schema: z.ZodType<T>,
|
|
17
|
+
options?: RequestInit
|
|
18
|
+
): Promise<T> {
|
|
19
|
+
const url = path.startsWith('http') ? path : `${env.EXPO_PUBLIC_API_URL}${path}`;
|
|
20
|
+
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
...options,
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
...options?.headers,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
return schema.parse(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* POST with Zod validation
|
|
39
|
+
*/
|
|
40
|
+
export async function postValidated<T>(
|
|
41
|
+
path: string,
|
|
42
|
+
body: unknown,
|
|
43
|
+
schema: z.ZodType<T>,
|
|
44
|
+
options?: RequestInit
|
|
45
|
+
): Promise<T> {
|
|
46
|
+
return fetchValidated(path, schema, {
|
|
47
|
+
...options,
|
|
48
|
+
method: 'POST',
|
|
49
|
+
body: JSON.stringify(body),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { queryOptions } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
import { fetchValidated } from './client';
|
|
4
|
+
import { ItemSchema, PaginatedResponseSchema, UserSchema } from './schemas';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// QUERY OPTIONS - Replace with your actual queries
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export const userQueryOptions = (userId: string) =>
|
|
11
|
+
queryOptions({
|
|
12
|
+
queryKey: ['user', userId],
|
|
13
|
+
queryFn: () => fetchValidated(`/users/${userId}`, UserSchema),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const itemsQueryOptions = (cursor?: string) =>
|
|
17
|
+
queryOptions({
|
|
18
|
+
queryKey: ['items', { cursor }],
|
|
19
|
+
queryFn: () =>
|
|
20
|
+
fetchValidated(
|
|
21
|
+
`/items${cursor ? `?cursor=${cursor}` : ''}`,
|
|
22
|
+
PaginatedResponseSchema(ItemSchema)
|
|
23
|
+
),
|
|
24
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// EXAMPLE SCHEMAS - Replace with your actual API schemas
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export const UserSchema = z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
email: z.string().email(),
|
|
10
|
+
name: z.string(),
|
|
11
|
+
avatar: z.string().url().optional(),
|
|
12
|
+
createdAt: z.string().datetime(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type User = z.infer<typeof UserSchema>;
|
|
16
|
+
|
|
17
|
+
export const ItemSchema = z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
title: z.string(),
|
|
20
|
+
description: z.string().optional(),
|
|
21
|
+
createdAt: z.string().datetime(),
|
|
22
|
+
updatedAt: z.string().datetime(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type Item = z.infer<typeof ItemSchema>;
|
|
26
|
+
|
|
27
|
+
export const PaginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
|
|
28
|
+
z.object({
|
|
29
|
+
items: z.array(itemSchema),
|
|
30
|
+
nextCursor: z.string().nullable(),
|
|
31
|
+
hasMore: z.boolean(),
|
|
32
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createEnv } from '@t3-oss/env-core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const env = createEnv({
|
|
5
|
+
/**
|
|
6
|
+
* Server-side environment variables.
|
|
7
|
+
* These should only be used in API routes or server functions.
|
|
8
|
+
*/
|
|
9
|
+
server: {
|
|
10
|
+
// API_SECRET: z.string().min(32),
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Client-side environment variables (available everywhere).
|
|
15
|
+
* Must be prefixed with EXPO_PUBLIC_ in .env files.
|
|
16
|
+
*/
|
|
17
|
+
clientPrefix: 'EXPO_PUBLIC_',
|
|
18
|
+
client: {
|
|
19
|
+
EXPO_PUBLIC_API_URL: z.string().url().default('https://api.example.com'),
|
|
20
|
+
// EXPO_PUBLIC_SENTRY_DSN: z.string().url().optional(),
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Runtime environment - manually specify all variables.
|
|
25
|
+
*/
|
|
26
|
+
runtimeEnv: {
|
|
27
|
+
EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL,
|
|
28
|
+
// Add all variables here
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Treat empty strings as undefined.
|
|
33
|
+
*/
|
|
34
|
+
emptyStringAsUndefined: true,
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Skip validation during builds if needed.
|
|
38
|
+
*/
|
|
39
|
+
skipValidation: process.env.SKIP_ENV_VALIDATION === 'true',
|
|
40
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
|
|
3
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
4
|
+
|
|
5
|
+
export const queryClient = new QueryClient({
|
|
6
|
+
defaultOptions: {
|
|
7
|
+
queries: {
|
|
8
|
+
// Keep cached data for 7 days (offline support)
|
|
9
|
+
gcTime: 1000 * 60 * 60 * 24 * 7,
|
|
10
|
+
// Data considered fresh for 5 minutes
|
|
11
|
+
staleTime: 1000 * 60 * 5,
|
|
12
|
+
// Try cache first, then network
|
|
13
|
+
networkMode: 'offlineFirst',
|
|
14
|
+
// Retry with exponential backoff
|
|
15
|
+
retry: 3,
|
|
16
|
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
17
|
+
},
|
|
18
|
+
mutations: {
|
|
19
|
+
networkMode: 'offlineFirst',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Persist to AsyncStorage for true offline support
|
|
25
|
+
export const persister = createAsyncStoragePersister({
|
|
26
|
+
storage: AsyncStorage,
|
|
27
|
+
key: 'REACT_QUERY_OFFLINE_CACHE',
|
|
28
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result type for explicit error handling without try-catch everywhere.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* async function fetchUser(id: string): Promise<Result<User, string>> {
|
|
7
|
+
* try {
|
|
8
|
+
* const user = await api.getUser(id);
|
|
9
|
+
* return ok(user);
|
|
10
|
+
* } catch (e) {
|
|
11
|
+
* return err(e instanceof Error ? e.message : 'Unknown error');
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* // Usage
|
|
16
|
+
* const result = await fetchUser('123');
|
|
17
|
+
* if (!result.ok) {
|
|
18
|
+
* showToast(result.error);
|
|
19
|
+
* return;
|
|
20
|
+
* }
|
|
21
|
+
* const user = result.value;
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
|
|
25
|
+
|
|
26
|
+
export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
|
|
27
|
+
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Unwrap a Result, throwing if it's an error.
|
|
31
|
+
* Only use this when you're certain the result is ok.
|
|
32
|
+
*/
|
|
33
|
+
export function unwrap<T, E>(result: Result<T, E>): T {
|
|
34
|
+
if (!result.ok) {
|
|
35
|
+
throw result.error;
|
|
36
|
+
}
|
|
37
|
+
return result.value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Unwrap a Result with a default value for errors.
|
|
42
|
+
*/
|
|
43
|
+
export function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
|
|
44
|
+
return result.ok ? result.value : defaultValue;
|
|
45
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// APP STORE
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
interface AppState {
|
|
10
|
+
fontSize: 'small' | 'medium' | 'large';
|
|
11
|
+
theme: 'light' | 'dark' | 'system';
|
|
12
|
+
setFontSize: (size: AppState['fontSize']) => void;
|
|
13
|
+
setTheme: (theme: AppState['theme']) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useAppStore = create<AppState>()(
|
|
17
|
+
persist(
|
|
18
|
+
(set) => ({
|
|
19
|
+
fontSize: 'medium',
|
|
20
|
+
theme: 'system',
|
|
21
|
+
setFontSize: (fontSize) => set({ fontSize }),
|
|
22
|
+
setTheme: (theme) => set({ theme }),
|
|
23
|
+
}),
|
|
24
|
+
{
|
|
25
|
+
name: 'app-storage',
|
|
26
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// SEARCH STORE
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
interface SearchState {
|
|
36
|
+
query: string;
|
|
37
|
+
recentSearches: string[];
|
|
38
|
+
setQuery: (query: string) => void;
|
|
39
|
+
addRecentSearch: (search: string) => void;
|
|
40
|
+
clearRecentSearches: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const useSearchStore = create<SearchState>()(
|
|
44
|
+
persist(
|
|
45
|
+
(set) => ({
|
|
46
|
+
query: '',
|
|
47
|
+
recentSearches: [],
|
|
48
|
+
setQuery: (query) => set({ query }),
|
|
49
|
+
addRecentSearch: (search) =>
|
|
50
|
+
set((state) => ({
|
|
51
|
+
recentSearches: [search, ...state.recentSearches.filter((s) => s !== search)].slice(
|
|
52
|
+
0,
|
|
53
|
+
10
|
|
54
|
+
),
|
|
55
|
+
})),
|
|
56
|
+
clearRecentSearches: () => set({ recentSearches: [] }),
|
|
57
|
+
}),
|
|
58
|
+
{
|
|
59
|
+
name: 'search-storage',
|
|
60
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Environment Variables
|
|
2
|
+
# Copy this file to .env.local and fill in the values
|
|
3
|
+
|
|
4
|
+
# Server-side only (not exposed to client)
|
|
5
|
+
NODE_ENV=development
|
|
6
|
+
# DATABASE_URL=postgresql://user:password@localhost:5432/db
|
|
7
|
+
# API_SECRET=your-secret-key-at-least-32-characters
|
|
8
|
+
|
|
9
|
+
# Client-side (prefixed with VITE_)
|
|
10
|
+
VITE_APP_URL=http://localhost:3000
|
|
11
|
+
# VITE_PUBLIC_API_URL=https://api.example.com
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{name}}/web",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"start": "node .output/server/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@tanstack/react-router": "^1.154.14",
|
|
12
|
+
"@tanstack/react-start": "^1.154.14",
|
|
13
|
+
"@tanstack/react-query": "^5.60.0",
|
|
14
|
+
"@t3-oss/env-core": "^0.12.0",
|
|
15
|
+
"react": "^19.0.0",
|
|
16
|
+
"react-dom": "^19.0.0",
|
|
17
|
+
"zod": "^3.24.0",
|
|
18
|
+
"lucide-react": "^0.460.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/react": "^19.0.0",
|
|
22
|
+
"@types/react-dom": "^19.0.0",
|
|
23
|
+
"@vitejs/plugin-react": "^4.4.1",
|
|
24
|
+
"vite": "^6.3.0",
|
|
25
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
26
|
+
"tailwindcss": "^4.0.0",
|
|
27
|
+
"@tailwindcss/vite": "^4.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createEnv } from '@t3-oss/env-core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const env = createEnv({
|
|
5
|
+
/**
|
|
6
|
+
* Server-side environment variables (not available on client).
|
|
7
|
+
* Will throw if accessed on the client.
|
|
8
|
+
*/
|
|
9
|
+
server: {
|
|
10
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
11
|
+
// DATABASE_URL: z.string().url(),
|
|
12
|
+
// API_SECRET: z.string().min(32),
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Client-side environment variables (available everywhere).
|
|
17
|
+
* Must be prefixed with VITE_ in .env files.
|
|
18
|
+
*/
|
|
19
|
+
clientPrefix: 'VITE_',
|
|
20
|
+
client: {
|
|
21
|
+
VITE_APP_URL: z.string().url().default('http://localhost:3000'),
|
|
22
|
+
// VITE_PUBLIC_API_URL: z.string().url(),
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shared variables (available on both client and server).
|
|
27
|
+
*/
|
|
28
|
+
shared: {
|
|
29
|
+
// NODE_ENV is commonly shared
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Runtime environment - manually specify all variables.
|
|
34
|
+
* Required for bundlers that don't automatically inline process.env.
|
|
35
|
+
*/
|
|
36
|
+
runtimeEnv: {
|
|
37
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
38
|
+
VITE_APP_URL: process.env.VITE_APP_URL,
|
|
39
|
+
// Add all variables here
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Treat empty strings as undefined.
|
|
44
|
+
* Useful for optional variables.
|
|
45
|
+
*/
|
|
46
|
+
emptyStringAsUndefined: true,
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Skip validation in certain environments.
|
|
50
|
+
*/
|
|
51
|
+
skipValidation: process.env.SKIP_ENV_VALIDATION === 'true',
|
|
52
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { queryOptions } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// EXAMPLE QUERY OPTIONS - Replace with your actual queries
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export const exampleQueryOptions = queryOptions({
|
|
8
|
+
queryKey: ['example'],
|
|
9
|
+
queryFn: async () => {
|
|
10
|
+
// Replace with your actual API call
|
|
11
|
+
return { message: 'Hello from TanStack Query!' };
|
|
12
|
+
},
|
|
13
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Query options factory pattern for dynamic queries
|
|
18
|
+
*/
|
|
19
|
+
export const itemQueryOptions = (id: string) =>
|
|
20
|
+
queryOptions({
|
|
21
|
+
queryKey: ['items', id],
|
|
22
|
+
queryFn: async () => {
|
|
23
|
+
const response = await fetch(`/api/items/${id}`);
|
|
24
|
+
if (!response.ok) throw new Error('Failed to fetch item');
|
|
25
|
+
return response.json();
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
export const queryClient = new QueryClient({
|
|
4
|
+
defaultOptions: {
|
|
5
|
+
queries: {
|
|
6
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
7
|
+
retry: 3,
|
|
8
|
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createRouter } from '@tanstack/react-router';
|
|
2
|
+
import { routeTree } from './routeTree.gen';
|
|
3
|
+
|
|
4
|
+
export function getRouter() {
|
|
5
|
+
const router = createRouter({
|
|
6
|
+
routeTree,
|
|
7
|
+
defaultPreload: 'intent',
|
|
8
|
+
scrollRestoration: true,
|
|
9
|
+
});
|
|
10
|
+
return router;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare module '@tanstack/react-router' {
|
|
14
|
+
interface Register {
|
|
15
|
+
router: ReturnType<typeof getRouter>;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
|
3
|
+
|
|
4
|
+
import { queryClient } from '../lib/query-client';
|
|
5
|
+
import '../styles.css';
|
|
6
|
+
|
|
7
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
8
|
+
|
|
9
|
+
export interface RouterContext {
|
|
10
|
+
queryClient: QueryClient;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
14
|
+
component: RootLayout,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function RootLayout() {
|
|
18
|
+
return (
|
|
19
|
+
<QueryClientProvider client={queryClient}>
|
|
20
|
+
<html lang="en">
|
|
21
|
+
<head>
|
|
22
|
+
<meta charSet="UTF-8" />
|
|
23
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
24
|
+
<title>{{name}}</title>
|
|
25
|
+
</head>
|
|
26
|
+
<body className="min-h-screen bg-background text-foreground antialiased">
|
|
27
|
+
<Outlet />
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
</QueryClientProvider>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
|
|
3
|
+
export const Route = createFileRoute('/')({
|
|
4
|
+
component: Home,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
function Home() {
|
|
8
|
+
return (
|
|
9
|
+
<main className="flex min-h-screen flex-col items-center justify-center p-8">
|
|
10
|
+
<h1 className="text-4xl font-bold tracking-tight">Welcome to {{name}}</h1>
|
|
11
|
+
<p className="mt-4 text-muted-foreground">
|
|
12
|
+
Edit <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-sm">src/routes/index.tsx</code> to get started.
|
|
13
|
+
</p>
|
|
14
|
+
</main>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--color-background: oklch(1 0 0);
|
|
5
|
+
--color-foreground: oklch(0.1 0 0);
|
|
6
|
+
--color-primary: oklch(0.5 0.2 250);
|
|
7
|
+
--color-primary-foreground: oklch(1 0 0);
|
|
8
|
+
--color-muted: oklch(0.95 0 0);
|
|
9
|
+
--color-muted-foreground: oklch(0.4 0 0);
|
|
10
|
+
--color-border: oklch(0.9 0 0);
|
|
11
|
+
--font-sans: 'Inter', system-ui, sans-serif;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
@theme {
|
|
16
|
+
--color-background: oklch(0.1 0 0);
|
|
17
|
+
--color-foreground: oklch(0.95 0 0);
|
|
18
|
+
--color-muted: oklch(0.2 0 0);
|
|
19
|
+
--color-muted-foreground: oklch(0.6 0 0);
|
|
20
|
+
--color-border: oklch(0.25 0 0);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
font-family: var(--font-sans);
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
|
2
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
3
|
+
import viteReact from '@vitejs/plugin-react';
|
|
4
|
+
import { defineConfig } from 'vite';
|
|
5
|
+
import tsConfigPaths from 'vite-tsconfig-paths';
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
server: {
|
|
9
|
+
port: 3000,
|
|
10
|
+
},
|
|
11
|
+
plugins: [
|
|
12
|
+
tailwindcss(),
|
|
13
|
+
tsConfigPaths({
|
|
14
|
+
projects: ['./tsconfig.json'],
|
|
15
|
+
}),
|
|
16
|
+
tanstackStart({
|
|
17
|
+
srcDirectory: 'src',
|
|
18
|
+
}),
|
|
19
|
+
viteReact(),
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://claude.ai/schemas/claude-code-settings.json",
|
|
3
|
+
"permissions": {
|
|
4
|
+
"allow": [
|
|
5
|
+
"Bash(bun *)",
|
|
6
|
+
"Bash(turbo *)",
|
|
7
|
+
"Bash(git status)",
|
|
8
|
+
"Bash(git diff*)",
|
|
9
|
+
"Bash(git log*)",
|
|
10
|
+
"Bash(git add*)",
|
|
11
|
+
"Bash(git commit*)",
|
|
12
|
+
"Bash(git branch*)",
|
|
13
|
+
"Bash(git checkout*)",
|
|
14
|
+
"Bash(git switch*)",
|
|
15
|
+
"Bash(npx expo*)",
|
|
16
|
+
"Bash(eas *)"
|
|
17
|
+
],
|
|
18
|
+
"deny": [
|
|
19
|
+
"Bash(rm -rf /)",
|
|
20
|
+
"Bash(git push --force)",
|
|
21
|
+
"Bash(git reset --hard)"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"instructions": [
|
|
25
|
+
"Always use Biome for linting: `bun check` not eslint",
|
|
26
|
+
"Use @/ import alias for src/ paths",
|
|
27
|
+
"Follow existing patterns in the codebase",
|
|
28
|
+
"Write tests before implementation (TDD)",
|
|
29
|
+
"Use Zustand selectors, never destructure entire store",
|
|
30
|
+
"Validate all API responses with Zod schemas",
|
|
31
|
+
"Keep commits small and focused"
|
|
32
|
+
]
|
|
33
|
+
}
|