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,139 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
{{name}} is a Bun + Turborepo monorepo scaffolded with [create-edhor-stack](https://github.com/your-username/create-edhor-stack).
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Root (monorepo)
|
|
13
|
+
bun dev # Start all apps in development
|
|
14
|
+
bun build # Build all apps and packages
|
|
15
|
+
bun lint # Lint with Biome
|
|
16
|
+
bun check # Lint + format with auto-fix
|
|
17
|
+
bun clean # Clean all build artifacts
|
|
18
|
+
|
|
19
|
+
# Filter to specific app
|
|
20
|
+
bun dev --filter=web # Start web app only
|
|
21
|
+
bun dev --filter=mobile # Start mobile app only
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
### Monorepo Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
{{name}}/
|
|
30
|
+
├── apps/
|
|
31
|
+
│ ├── web/ # TanStack Start web application
|
|
32
|
+
│ │ ├── src/
|
|
33
|
+
│ │ │ ├── routes/ # File-based routing
|
|
34
|
+
│ │ │ └── lib/ # Utilities, query client
|
|
35
|
+
│ │ └── package.json
|
|
36
|
+
│ └── mobile/ # Expo React Native application
|
|
37
|
+
│ ├── app/ # Expo Router screens
|
|
38
|
+
│ ├── src/
|
|
39
|
+
│ │ ├── api/ # API client, schemas, queries
|
|
40
|
+
│ │ ├── lib/ # Store, utils, query client
|
|
41
|
+
│ │ └── hooks/ # Custom React hooks
|
|
42
|
+
│ └── package.json
|
|
43
|
+
├── packages/
|
|
44
|
+
│ └── ui/ # Shared UI components (shadcn/ui)
|
|
45
|
+
├── turbo.json # Turborepo configuration
|
|
46
|
+
├── biome.json # Linting & formatting rules
|
|
47
|
+
└── package.json # Root workspace configuration
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Tech Stack
|
|
51
|
+
|
|
52
|
+
| Layer | Technology |
|
|
53
|
+
|-------|------------|
|
|
54
|
+
| Package Manager | Bun |
|
|
55
|
+
| Monorepo | Turborepo |
|
|
56
|
+
| Web Framework | TanStack Start |
|
|
57
|
+
| Mobile Framework | Expo SDK 54 + Expo Router |
|
|
58
|
+
| State (Server) | TanStack Query |
|
|
59
|
+
| State (Client) | Zustand (mobile) |
|
|
60
|
+
| Validation | Zod |
|
|
61
|
+
| Styling | Tailwind CSS v4, shadcn/ui |
|
|
62
|
+
| Icons | Lucide |
|
|
63
|
+
| Linting | Biome |
|
|
64
|
+
|
|
65
|
+
### Key Patterns
|
|
66
|
+
|
|
67
|
+
**State Management (Mobile)**:
|
|
68
|
+
- Multiple specialized Zustand stores with `persist()` middleware
|
|
69
|
+
- Use selectors to prevent re-renders: `useAppStore(state => state.theme)`
|
|
70
|
+
- Never destructure the whole store
|
|
71
|
+
|
|
72
|
+
**API Layer**:
|
|
73
|
+
- `fetchValidated()` wraps fetch with Zod schema validation
|
|
74
|
+
- Query options factories for React Query consistency
|
|
75
|
+
- Offline-first configuration with 7-day cache
|
|
76
|
+
|
|
77
|
+
**Routing**:
|
|
78
|
+
- Web: TanStack Router file-based routing in `src/routes/`
|
|
79
|
+
- Mobile: Expo Router file-based routing in `app/`
|
|
80
|
+
|
|
81
|
+
### Import Alias
|
|
82
|
+
|
|
83
|
+
All imports use `@/` prefix mapping to `src/`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { useAppStore } from '@/lib/store';
|
|
87
|
+
import { fetchValidated } from '@/api/client';
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Code Style
|
|
91
|
+
|
|
92
|
+
Enforced by Biome (`biome.json`):
|
|
93
|
+
- 2-space indentation, 100-char line width
|
|
94
|
+
- Single quotes, ES5 trailing commas, semicolons
|
|
95
|
+
- `useImportType` for type-only imports
|
|
96
|
+
- No unused imports (error), no unused variables (warn)
|
|
97
|
+
|
|
98
|
+
Section comments format:
|
|
99
|
+
```typescript
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// SECTION NAME
|
|
102
|
+
// ============================================================================
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Development Guidelines
|
|
106
|
+
|
|
107
|
+
### Adding a New Route (Web)
|
|
108
|
+
|
|
109
|
+
1. Create file in `apps/web/src/routes/` (e.g., `dashboard.tsx`)
|
|
110
|
+
2. Export Route with `createFileRoute`
|
|
111
|
+
3. Use route loaders for data fetching
|
|
112
|
+
|
|
113
|
+
### Adding a New Screen (Mobile)
|
|
114
|
+
|
|
115
|
+
1. Create file in `apps/mobile/app/` (e.g., `settings.tsx`)
|
|
116
|
+
2. Use `useLocalSearchParams` for dynamic routes
|
|
117
|
+
3. Connect to Zustand stores via selectors
|
|
118
|
+
|
|
119
|
+
### Adding API Endpoints
|
|
120
|
+
|
|
121
|
+
1. Define Zod schema in `src/api/schemas.ts`
|
|
122
|
+
2. Create query options in `src/api/queries.ts`
|
|
123
|
+
3. Use `fetchValidated()` for type-safe fetching
|
|
124
|
+
|
|
125
|
+
## AI Agent Instructions
|
|
126
|
+
|
|
127
|
+
See [AGENTS.md](./AGENTS.md) for multi-agent workflow documentation.
|
|
128
|
+
|
|
129
|
+
### Preferred Approaches
|
|
130
|
+
|
|
131
|
+
- **TDD**: Write failing tests first, then implement
|
|
132
|
+
- **Small commits**: Commit after each logical change
|
|
133
|
+
- **DRY/YAGNI**: Don't over-engineer, build what's needed
|
|
134
|
+
|
|
135
|
+
### Before Making Changes
|
|
136
|
+
|
|
137
|
+
1. Read relevant files to understand context
|
|
138
|
+
2. Check existing patterns in similar files
|
|
139
|
+
3. Run `bun check` before committing
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
# Build stage
|
|
4
|
+
FROM oven/bun:1 AS builder
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
# Copy package files
|
|
8
|
+
COPY package.json bun.lock* ./
|
|
9
|
+
COPY apps/web/package.json ./apps/web/
|
|
10
|
+
COPY packages/*/package.json ./packages/
|
|
11
|
+
|
|
12
|
+
# Install dependencies
|
|
13
|
+
RUN bun install --frozen-lockfile
|
|
14
|
+
|
|
15
|
+
# Copy source
|
|
16
|
+
COPY . .
|
|
17
|
+
|
|
18
|
+
# Build the web app
|
|
19
|
+
RUN bun run build --filter=web
|
|
20
|
+
|
|
21
|
+
# Production stage
|
|
22
|
+
FROM oven/bun:1-slim AS runner
|
|
23
|
+
WORKDIR /app
|
|
24
|
+
|
|
25
|
+
ENV NODE_ENV=production
|
|
26
|
+
|
|
27
|
+
# Copy built application
|
|
28
|
+
COPY --from=builder /app/apps/web/.output ./.output
|
|
29
|
+
|
|
30
|
+
# Run the server
|
|
31
|
+
EXPOSE 3000
|
|
32
|
+
CMD ["bun", "run", ".output/server/index.mjs"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": true,
|
|
10
|
+
"includes": ["apps/**/*.ts", "apps/**/*.tsx", "packages/**/*.ts", "packages/**/*.tsx"]
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "space",
|
|
15
|
+
"indentWidth": 2,
|
|
16
|
+
"lineWidth": 100
|
|
17
|
+
},
|
|
18
|
+
"assist": {
|
|
19
|
+
"actions": {
|
|
20
|
+
"source": {
|
|
21
|
+
"organizeImports": "on"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"linter": {
|
|
26
|
+
"enabled": true,
|
|
27
|
+
"rules": {
|
|
28
|
+
"recommended": true,
|
|
29
|
+
"correctness": {
|
|
30
|
+
"useExhaustiveDependencies": "warn",
|
|
31
|
+
"noUnusedImports": "error",
|
|
32
|
+
"noUnusedVariables": "warn"
|
|
33
|
+
},
|
|
34
|
+
"style": {
|
|
35
|
+
"noNonNullAssertion": "off",
|
|
36
|
+
"useConst": "error",
|
|
37
|
+
"useImportType": "error"
|
|
38
|
+
},
|
|
39
|
+
"suspicious": {
|
|
40
|
+
"noExplicitAny": "warn",
|
|
41
|
+
"noConsole": "off"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"javascript": {
|
|
46
|
+
"formatter": {
|
|
47
|
+
"quoteStyle": "double",
|
|
48
|
+
"trailingCommas": "es5",
|
|
49
|
+
"semicolons": "always"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
app = "{{name}}"
|
|
2
|
+
primary_region = "iad"
|
|
3
|
+
|
|
4
|
+
[build]
|
|
5
|
+
|
|
6
|
+
[http_service]
|
|
7
|
+
internal_port = 3000
|
|
8
|
+
force_https = true
|
|
9
|
+
auto_stop_machines = "stop"
|
|
10
|
+
auto_start_machines = true
|
|
11
|
+
min_machines_running = 0
|
|
12
|
+
processes = ["app"]
|
|
13
|
+
|
|
14
|
+
[env]
|
|
15
|
+
NODE_ENV = "production"
|
|
16
|
+
|
|
17
|
+
[[vm]]
|
|
18
|
+
memory = "512mb"
|
|
19
|
+
cpu_kind = "shared"
|
|
20
|
+
cpus = 1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build outputs
|
|
5
|
+
dist/
|
|
6
|
+
.output/
|
|
7
|
+
.vinxi/
|
|
8
|
+
.expo/
|
|
9
|
+
build/
|
|
10
|
+
|
|
11
|
+
# Turbo
|
|
12
|
+
.turbo/
|
|
13
|
+
|
|
14
|
+
# Environment
|
|
15
|
+
.env
|
|
16
|
+
.env.local
|
|
17
|
+
.env.*.local
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
*.swp
|
|
23
|
+
*.swo
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
Thumbs.db
|
|
28
|
+
|
|
29
|
+
# Logs
|
|
30
|
+
*.log
|
|
31
|
+
npm-debug.log*
|
|
32
|
+
|
|
33
|
+
# Testing
|
|
34
|
+
coverage/
|
|
35
|
+
playwright-report/
|
|
36
|
+
test-results/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"workspaces": ["apps/*", "packages/*"],
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "turbo dev",
|
|
7
|
+
"build": "turbo build",
|
|
8
|
+
"lint": "biome lint .",
|
|
9
|
+
"format": "biome format --write .",
|
|
10
|
+
"check": "biome check --write .",
|
|
11
|
+
"clean": "turbo clean && rm -rf node_modules",
|
|
12
|
+
"prepare": "husky"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@biomejs/biome": "2.3.11",
|
|
16
|
+
"husky": "^9.1.7",
|
|
17
|
+
"lint-staged": "^16.1.0",
|
|
18
|
+
"turbo": "^2.5.4",
|
|
19
|
+
"typescript": "^5.8.3"
|
|
20
|
+
},
|
|
21
|
+
"packageManager": "bun@1.3.5"
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://turbo.build/schema.json",
|
|
3
|
+
"tasks": {
|
|
4
|
+
"build": {
|
|
5
|
+
"dependsOn": ["^build"],
|
|
6
|
+
"outputs": ["dist/**", ".output/**", ".vinxi/**"]
|
|
7
|
+
},
|
|
8
|
+
"dev": {
|
|
9
|
+
"cache": false,
|
|
10
|
+
"persistent": true
|
|
11
|
+
},
|
|
12
|
+
"lint": {
|
|
13
|
+
"dependsOn": ["^lint"]
|
|
14
|
+
},
|
|
15
|
+
"typecheck": {
|
|
16
|
+
"dependsOn": ["^build"]
|
|
17
|
+
},
|
|
18
|
+
"clean": {
|
|
19
|
+
"cache": false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{name}}/shared",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.ts",
|
|
7
|
+
"./schemas": "./src/schemas.ts",
|
|
8
|
+
"./types": "./src/types.ts",
|
|
9
|
+
"./utils": "./src/utils.ts"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"zod": "^3.24.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.8.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// COMMON SCHEMAS
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export const IdSchema = z.string().uuid();
|
|
8
|
+
|
|
9
|
+
export const PaginationSchema = z.object({
|
|
10
|
+
page: z.number().int().positive().default(1),
|
|
11
|
+
limit: z.number().int().positive().max(100).default(20),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const TimestampsSchema = z.object({
|
|
15
|
+
createdAt: z.string().datetime(),
|
|
16
|
+
updatedAt: z.string().datetime(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// USER SCHEMAS
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export const UserSchema = z.object({
|
|
24
|
+
id: IdSchema,
|
|
25
|
+
email: z.string().email(),
|
|
26
|
+
name: z.string().min(1),
|
|
27
|
+
image: z.string().url().optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const CreateUserSchema = UserSchema.omit({ id: true });
|
|
31
|
+
export const UpdateUserSchema = UserSchema.partial().omit({ id: true });
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// API RESPONSE SCHEMAS
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
export const ApiErrorSchema = z.object({
|
|
38
|
+
code: z.string(),
|
|
39
|
+
message: z.string(),
|
|
40
|
+
details: z.record(z.unknown()).optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const PaginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
|
|
44
|
+
z.object({
|
|
45
|
+
items: z.array(itemSchema),
|
|
46
|
+
total: z.number().int().nonnegative(),
|
|
47
|
+
page: z.number().int().positive(),
|
|
48
|
+
limit: z.number().int().positive(),
|
|
49
|
+
hasMore: z.boolean(),
|
|
50
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type {
|
|
3
|
+
UserSchema,
|
|
4
|
+
CreateUserSchema,
|
|
5
|
+
UpdateUserSchema,
|
|
6
|
+
PaginationSchema,
|
|
7
|
+
ApiErrorSchema,
|
|
8
|
+
} from './schemas';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// INFERRED TYPES FROM SCHEMAS
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export type User = z.infer<typeof UserSchema>;
|
|
15
|
+
export type CreateUser = z.infer<typeof CreateUserSchema>;
|
|
16
|
+
export type UpdateUser = z.infer<typeof UpdateUserSchema>;
|
|
17
|
+
export type Pagination = z.infer<typeof PaginationSchema>;
|
|
18
|
+
export type ApiError = z.infer<typeof ApiErrorSchema>;
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// UTILITY TYPES
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
export type PaginatedResponse<T> = {
|
|
25
|
+
items: T[];
|
|
26
|
+
total: number;
|
|
27
|
+
page: number;
|
|
28
|
+
limit: number;
|
|
29
|
+
hasMore: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type Result<T, E = ApiError> =
|
|
33
|
+
| { ok: true; data: T }
|
|
34
|
+
| { ok: false; error: E };
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// API TYPES
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
41
|
+
|
|
42
|
+
export type ApiEndpoint<TInput, TOutput> = {
|
|
43
|
+
method: HttpMethod;
|
|
44
|
+
path: string;
|
|
45
|
+
input: TInput;
|
|
46
|
+
output: TOutput;
|
|
47
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Result, ApiError } from './types';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// RESULT HELPERS
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export const ok = <T>(data: T): Result<T, never> => ({ ok: true, data });
|
|
8
|
+
export const err = <E = ApiError>(error: E): Result<never, E> => ({ ok: false, error });
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// STRING UTILS
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export function slugify(text: string): string {
|
|
15
|
+
return text
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/[^\w\s-]/g, '')
|
|
19
|
+
.replace(/[\s_-]+/g, '-')
|
|
20
|
+
.replace(/^-+|-+$/g, '');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function truncate(text: string, maxLength: number, suffix = '...'): string {
|
|
24
|
+
if (text.length <= maxLength) return text;
|
|
25
|
+
return text.slice(0, maxLength - suffix.length) + suffix;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function capitalize(text: string): string {
|
|
29
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// DATE UTILS
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export function formatDate(date: Date | string, locale = 'en-US'): string {
|
|
37
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
38
|
+
return d.toLocaleDateString(locale, {
|
|
39
|
+
year: 'numeric',
|
|
40
|
+
month: 'long',
|
|
41
|
+
day: 'numeric',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatRelativeTime(date: Date | string): string {
|
|
46
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
47
|
+
const now = new Date();
|
|
48
|
+
const diffMs = now.getTime() - d.getTime();
|
|
49
|
+
const diffSecs = Math.floor(diffMs / 1000);
|
|
50
|
+
const diffMins = Math.floor(diffSecs / 60);
|
|
51
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
52
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
53
|
+
|
|
54
|
+
if (diffSecs < 60) return 'just now';
|
|
55
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
56
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
57
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
58
|
+
return formatDate(d);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// OBJECT UTILS
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
export function omit<T extends object, K extends keyof T>(
|
|
66
|
+
obj: T,
|
|
67
|
+
keys: K[]
|
|
68
|
+
): Omit<T, K> {
|
|
69
|
+
const result = { ...obj };
|
|
70
|
+
for (const key of keys) {
|
|
71
|
+
delete result[key];
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function pick<T extends object, K extends keyof T>(
|
|
77
|
+
obj: T,
|
|
78
|
+
keys: K[]
|
|
79
|
+
): Pick<T, K> {
|
|
80
|
+
const result = {} as Pick<T, K>;
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
if (key in obj) {
|
|
83
|
+
result[key] = obj[key];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"composite": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{name}}/stripe",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.ts",
|
|
7
|
+
"./client": "./src/client.ts",
|
|
8
|
+
"./webhooks": "./src/webhooks.ts",
|
|
9
|
+
"./schemas": "./src/schemas.ts"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"stripe": "^17.0.0",
|
|
13
|
+
"zod": "^3.24.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.8.0"
|
|
17
|
+
}
|
|
18
|
+
}
|