@getjack/jack 0.1.19 → 0.1.22
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/package.json +5 -2
- package/src/commands/down.ts +11 -1
- package/src/commands/init.ts +19 -6
- package/src/commands/new.ts +56 -4
- package/src/commands/publish.ts +1 -1
- package/src/lib/agents.ts +3 -1
- package/src/lib/auth/ensure-auth.test.ts +3 -3
- package/src/lib/control-plane.ts +15 -1
- package/src/lib/deploy-upload.ts +26 -1
- package/src/lib/hooks.ts +232 -1
- package/src/lib/managed-deploy.ts +13 -6
- package/src/lib/managed-down.ts +66 -45
- package/src/lib/progress.ts +76 -5
- package/src/lib/project-list.ts +6 -1
- package/src/lib/project-operations.ts +21 -31
- package/src/lib/project-resolver.ts +1 -1
- package/src/lib/zip-packager.ts +36 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/types.ts +16 -0
- package/templates/CLAUDE.md +172 -5
- package/templates/miniapp/.jack.json +1 -3
- package/templates/saas/.jack.json +154 -0
- package/templates/saas/AGENTS.md +333 -0
- package/templates/saas/bun.lock +925 -0
- package/templates/saas/components.json +21 -0
- package/templates/saas/index.html +12 -0
- package/templates/saas/package.json +75 -0
- package/templates/saas/public/icon.png +0 -0
- package/templates/saas/public/og.png +0 -0
- package/templates/saas/schema.sql +73 -0
- package/templates/saas/src/auth.ts +77 -0
- package/templates/saas/src/client/App.tsx +63 -0
- package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
- package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
- package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
- package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
- package/templates/saas/src/client/components/ui/alert.tsx +60 -0
- package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
- package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
- package/templates/saas/src/client/components/ui/badge.tsx +39 -0
- package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
- package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
- package/templates/saas/src/client/components/ui/button.tsx +60 -0
- package/templates/saas/src/client/components/ui/card.tsx +75 -0
- package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
- package/templates/saas/src/client/components/ui/chart.tsx +326 -0
- package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
- package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
- package/templates/saas/src/client/components/ui/command.tsx +159 -0
- package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
- package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
- package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
- package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
- package/templates/saas/src/client/components/ui/empty.tsx +94 -0
- package/templates/saas/src/client/components/ui/field.tsx +232 -0
- package/templates/saas/src/client/components/ui/form.tsx +152 -0
- package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
- package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
- package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
- package/templates/saas/src/client/components/ui/input.tsx +21 -0
- package/templates/saas/src/client/components/ui/item.tsx +172 -0
- package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
- package/templates/saas/src/client/components/ui/label.tsx +21 -0
- package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
- package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
- package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
- package/templates/saas/src/client/components/ui/popover.tsx +42 -0
- package/templates/saas/src/client/components/ui/progress.tsx +26 -0
- package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
- package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
- package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
- package/templates/saas/src/client/components/ui/select.tsx +173 -0
- package/templates/saas/src/client/components/ui/separator.tsx +28 -0
- package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
- package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
- package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
- package/templates/saas/src/client/components/ui/slider.tsx +58 -0
- package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
- package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
- package/templates/saas/src/client/components/ui/switch.tsx +28 -0
- package/templates/saas/src/client/components/ui/table.tsx +90 -0
- package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
- package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
- package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
- package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
- package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
- package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
- package/templates/saas/src/client/hooks/useAuth.ts +14 -0
- package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
- package/templates/saas/src/client/index.css +165 -0
- package/templates/saas/src/client/lib/auth-client.ts +7 -0
- package/templates/saas/src/client/lib/plans.ts +82 -0
- package/templates/saas/src/client/lib/utils.ts +6 -0
- package/templates/saas/src/client/main.tsx +15 -0
- package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
- package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
- package/templates/saas/src/client/pages/HomePage.tsx +285 -0
- package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
- package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
- package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
- package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
- package/templates/saas/src/index.ts +208 -0
- package/templates/saas/tsconfig.json +18 -0
- package/templates/saas/vite.config.ts +14 -0
- package/templates/saas/wrangler.jsonc +20 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/client/index.css",
|
|
9
|
+
"baseColor": "stone",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/lib/utils",
|
|
17
|
+
"ui": "@/components/ui",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>jack-template</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/client/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jack-template",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "vite",
|
|
6
|
+
"build": "tsc && vite build",
|
|
7
|
+
"preview": "vite preview",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"db:migrate": "wrangler d1 execute DB --file=schema.sql --remote",
|
|
10
|
+
"db:migrate:local": "wrangler d1 execute DB --file=schema.sql --local"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@better-auth/stripe": "^1.4.15",
|
|
14
|
+
"clsx": "^2.1.1",
|
|
15
|
+
"@hookform/resolvers": "^5.2.2",
|
|
16
|
+
"@radix-ui/react-accordion": "^1.2.12",
|
|
17
|
+
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
18
|
+
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
|
19
|
+
"@radix-ui/react-avatar": "^1.1.11",
|
|
20
|
+
"@radix-ui/react-checkbox": "^1.3.3",
|
|
21
|
+
"@radix-ui/react-collapsible": "^1.1.12",
|
|
22
|
+
"@radix-ui/react-context-menu": "^2.2.16",
|
|
23
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
24
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
25
|
+
"@radix-ui/react-hover-card": "^1.1.15",
|
|
26
|
+
"@radix-ui/react-label": "^2.1.8",
|
|
27
|
+
"@radix-ui/react-menubar": "^1.1.16",
|
|
28
|
+
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
29
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
30
|
+
"@radix-ui/react-progress": "^1.1.8",
|
|
31
|
+
"@radix-ui/react-radio-group": "^1.3.8",
|
|
32
|
+
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
33
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
34
|
+
"@radix-ui/react-separator": "^1.1.8",
|
|
35
|
+
"@radix-ui/react-slider": "^1.3.6",
|
|
36
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
37
|
+
"@radix-ui/react-switch": "^1.2.6",
|
|
38
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
39
|
+
"@radix-ui/react-toggle": "^1.1.10",
|
|
40
|
+
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
41
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
42
|
+
"better-auth": "^1.4.15",
|
|
43
|
+
"class-variance-authority": "^0.7.1",
|
|
44
|
+
"cmdk": "^1.1.1",
|
|
45
|
+
"embla-carousel-react": "^8.6.0",
|
|
46
|
+
"kysely": "^0.28.0",
|
|
47
|
+
"kysely-d1": "^0.4.0",
|
|
48
|
+
"hono": "^4.6.0",
|
|
49
|
+
"input-otp": "^1.4.2",
|
|
50
|
+
"lucide-react": "^0.562.0",
|
|
51
|
+
"next-themes": "^0.4.6",
|
|
52
|
+
"react": "^18.3.1",
|
|
53
|
+
"react-dom": "^18.3.1",
|
|
54
|
+
"react-hook-form": "^7.71.1",
|
|
55
|
+
"react-resizable-panels": "^4.4.1",
|
|
56
|
+
"recharts": "2.15.4",
|
|
57
|
+
"sonner": "^2.0.7",
|
|
58
|
+
"stripe": "^20.0.0",
|
|
59
|
+
"tailwind-merge": "^3.0.2",
|
|
60
|
+
"tw-animate-css": "^1.3.4",
|
|
61
|
+
"vaul": "^1.1.2",
|
|
62
|
+
"zod": "^4.3.5"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@cloudflare/vite-plugin": "^1.0.0",
|
|
66
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
67
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
68
|
+
"@types/react": "^18.3.0",
|
|
69
|
+
"@types/react-dom": "^18.3.0",
|
|
70
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
71
|
+
"tailwindcss": "^4.0.0",
|
|
72
|
+
"typescript": "^5.6.0",
|
|
73
|
+
"vite": "^6.0.0"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
-- Better Auth core tables (using Better Auth's expected schema)
|
|
2
|
+
-- Column names use camelCase to match Better Auth's defaults
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS user (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
email TEXT UNIQUE NOT NULL,
|
|
7
|
+
emailVerified INTEGER DEFAULT 0,
|
|
8
|
+
name TEXT,
|
|
9
|
+
image TEXT,
|
|
10
|
+
stripeCustomerId TEXT,
|
|
11
|
+
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
updatedAt TEXT DEFAULT CURRENT_TIMESTAMP
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS session (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
userId TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
|
18
|
+
token TEXT UNIQUE NOT NULL,
|
|
19
|
+
expiresAt TEXT NOT NULL,
|
|
20
|
+
ipAddress TEXT,
|
|
21
|
+
userAgent TEXT,
|
|
22
|
+
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
23
|
+
updatedAt TEXT DEFAULT CURRENT_TIMESTAMP
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS account (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
userId TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
|
29
|
+
accountId TEXT NOT NULL,
|
|
30
|
+
providerId TEXT NOT NULL,
|
|
31
|
+
accessToken TEXT,
|
|
32
|
+
refreshToken TEXT,
|
|
33
|
+
accessTokenExpiresAt TEXT,
|
|
34
|
+
refreshTokenExpiresAt TEXT,
|
|
35
|
+
scope TEXT,
|
|
36
|
+
idToken TEXT,
|
|
37
|
+
password TEXT,
|
|
38
|
+
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
39
|
+
updatedAt TEXT DEFAULT CURRENT_TIMESTAMP
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS verification (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
identifier TEXT NOT NULL,
|
|
45
|
+
value TEXT NOT NULL,
|
|
46
|
+
expiresAt TEXT NOT NULL,
|
|
47
|
+
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
48
|
+
updatedAt TEXT DEFAULT CURRENT_TIMESTAMP
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- Stripe plugin subscription table
|
|
52
|
+
CREATE TABLE IF NOT EXISTS subscription (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
plan TEXT NOT NULL,
|
|
55
|
+
referenceId TEXT NOT NULL,
|
|
56
|
+
stripeCustomerId TEXT,
|
|
57
|
+
stripeSubscriptionId TEXT,
|
|
58
|
+
status TEXT DEFAULT 'incomplete',
|
|
59
|
+
periodStart TEXT,
|
|
60
|
+
periodEnd TEXT,
|
|
61
|
+
cancelAtPeriodEnd INTEGER DEFAULT 0,
|
|
62
|
+
cancelAt TEXT,
|
|
63
|
+
canceledAt TEXT,
|
|
64
|
+
endedAt TEXT,
|
|
65
|
+
seats INTEGER DEFAULT 1,
|
|
66
|
+
trialStart TEXT,
|
|
67
|
+
trialEnd TEXT,
|
|
68
|
+
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
69
|
+
updatedAt TEXT DEFAULT CURRENT_TIMESTAMP
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_subscription_reference ON subscription(referenceId);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_subscription_stripe ON subscription(stripeSubscriptionId);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { stripe } from "@better-auth/stripe";
|
|
3
|
+
import Stripe from "stripe";
|
|
4
|
+
import { Kysely } from "kysely";
|
|
5
|
+
import { D1Dialect } from "kysely-d1";
|
|
6
|
+
|
|
7
|
+
// Env type is defined in index.ts and passed from the worker
|
|
8
|
+
type Env = {
|
|
9
|
+
DB: D1Database;
|
|
10
|
+
BETTER_AUTH_SECRET: string;
|
|
11
|
+
STRIPE_SECRET_KEY: string;
|
|
12
|
+
STRIPE_WEBHOOK_SECRET?: string; // Optional until webhook is configured
|
|
13
|
+
STRIPE_PRO_PRICE_ID?: string;
|
|
14
|
+
STRIPE_ENTERPRISE_PRICE_ID?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function createAuth(env: Env) {
|
|
18
|
+
const stripeClient = env.STRIPE_SECRET_KEY ? new Stripe(env.STRIPE_SECRET_KEY) : null;
|
|
19
|
+
|
|
20
|
+
// Build plugins array - Stripe plugin only if we have the API key
|
|
21
|
+
const plugins = [];
|
|
22
|
+
|
|
23
|
+
if (env.STRIPE_SECRET_KEY && stripeClient) {
|
|
24
|
+
// Validate required Stripe configuration
|
|
25
|
+
const missingConfig: string[] = [];
|
|
26
|
+
if (!env.STRIPE_PRO_PRICE_ID) missingConfig.push("STRIPE_PRO_PRICE_ID");
|
|
27
|
+
if (!env.STRIPE_ENTERPRISE_PRICE_ID) missingConfig.push("STRIPE_ENTERPRISE_PRICE_ID");
|
|
28
|
+
if (!env.STRIPE_WEBHOOK_SECRET) missingConfig.push("STRIPE_WEBHOOK_SECRET");
|
|
29
|
+
|
|
30
|
+
if (missingConfig.length > 0) {
|
|
31
|
+
console.error(`[Stripe] Missing required config: ${missingConfig.join(", ")}`);
|
|
32
|
+
console.error("[Stripe] Subscriptions will not work correctly. Set these secrets via: jack secrets set <KEY> <value>");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Only enable Stripe plugin if we have the minimum required config
|
|
36
|
+
if (env.STRIPE_WEBHOOK_SECRET) {
|
|
37
|
+
plugins.push(
|
|
38
|
+
stripe({
|
|
39
|
+
stripeClient,
|
|
40
|
+
stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET,
|
|
41
|
+
createCustomerOnSignUp: true,
|
|
42
|
+
subscription: {
|
|
43
|
+
enabled: true,
|
|
44
|
+
plans: [
|
|
45
|
+
{ name: "pro", priceId: env.STRIPE_PRO_PRICE_ID || "" },
|
|
46
|
+
{ name: "enterprise", priceId: env.STRIPE_ENTERPRISE_PRICE_ID || "" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
console.error("[Stripe] Plugin DISABLED - STRIPE_WEBHOOK_SECRET is required for reliable subscription sync");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Use Kysely with D1 dialect - Better Auth uses Kysely internally for D1
|
|
57
|
+
const db = new Kysely<any>({
|
|
58
|
+
dialect: new D1Dialect({ database: env.DB }),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return betterAuth({
|
|
62
|
+
database: {
|
|
63
|
+
db,
|
|
64
|
+
type: "sqlite",
|
|
65
|
+
},
|
|
66
|
+
emailAndPassword: {
|
|
67
|
+
enabled: true,
|
|
68
|
+
sendResetPassword: async ({ user, url }) => {
|
|
69
|
+
// TODO: Configure email sending (Resend, SendGrid, etc.)
|
|
70
|
+
// For now, log the reset URL for development
|
|
71
|
+
console.log(`[Password Reset] User: ${user.email}, URL: ${url}`);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
75
|
+
plugins,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
// Page imports
|
|
4
|
+
import HomePage from "./pages/HomePage";
|
|
5
|
+
import LoginPage from "./pages/LoginPage";
|
|
6
|
+
import SignupPage from "./pages/SignupPage";
|
|
7
|
+
import PricingPage from "./pages/PricingPage";
|
|
8
|
+
import DashboardPage from "./pages/DashboardPage";
|
|
9
|
+
import ForgotPasswordPage from "./pages/ForgotPasswordPage";
|
|
10
|
+
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
|
11
|
+
import { ProtectedRoute } from "./components/ProtectedRoute";
|
|
12
|
+
|
|
13
|
+
type Route = "/" | "/login" | "/signup" | "/pricing" | "/dashboard" | "/forgot-password" | "/reset-password";
|
|
14
|
+
|
|
15
|
+
function getRouteFromHash(): Route {
|
|
16
|
+
const hash = window.location.hash.split("?")[0].slice(1) || "/";
|
|
17
|
+
const validRoutes: Route[] = ["/", "/login", "/signup", "/pricing", "/dashboard", "/forgot-password", "/reset-password"];
|
|
18
|
+
return validRoutes.includes(hash as Route) ? (hash as Route) : "/";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function App() {
|
|
22
|
+
const [route, setRoute] = useState<Route>(getRouteFromHash);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const handleHashChange = () => {
|
|
26
|
+
setRoute(getRouteFromHash());
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
window.addEventListener("hashchange", handleHashChange);
|
|
30
|
+
return () => window.removeEventListener("hashchange", handleHashChange);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const navigate = (newRoute: Route) => {
|
|
34
|
+
window.location.hash = newRoute;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const renderPage = () => {
|
|
38
|
+
switch (route) {
|
|
39
|
+
case "/":
|
|
40
|
+
return <HomePage navigate={navigate} />;
|
|
41
|
+
case "/login":
|
|
42
|
+
return <LoginPage navigate={navigate} />;
|
|
43
|
+
case "/signup":
|
|
44
|
+
return <SignupPage navigate={navigate} />;
|
|
45
|
+
case "/pricing":
|
|
46
|
+
return <PricingPage navigate={navigate} />;
|
|
47
|
+
case "/forgot-password":
|
|
48
|
+
return <ForgotPasswordPage navigate={navigate} />;
|
|
49
|
+
case "/reset-password":
|
|
50
|
+
return <ResetPasswordPage navigate={navigate} />;
|
|
51
|
+
case "/dashboard":
|
|
52
|
+
return (
|
|
53
|
+
<ProtectedRoute navigate={navigate}>
|
|
54
|
+
<DashboardPage navigate={navigate} />
|
|
55
|
+
</ProtectedRoute>
|
|
56
|
+
);
|
|
57
|
+
default:
|
|
58
|
+
return <HomePage navigate={navigate} />;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return <div className="min-h-screen">{renderPage()}</div>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ReactNode, useEffect } from "react";
|
|
2
|
+
import { authClient } from "../lib/auth-client";
|
|
3
|
+
|
|
4
|
+
type Route = "/" | "/login" | "/signup" | "/pricing" | "/dashboard" | "/forgot-password" | "/reset-password";
|
|
5
|
+
|
|
6
|
+
interface ProtectedRouteProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
navigate: (path: Route) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ProtectedRoute({ children, navigate }: ProtectedRouteProps) {
|
|
12
|
+
const { data: session, isPending } = authClient.useSession();
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!isPending && !session) {
|
|
16
|
+
navigate("/login" as Route);
|
|
17
|
+
}
|
|
18
|
+
}, [session, isPending, navigate]);
|
|
19
|
+
|
|
20
|
+
if (isPending) {
|
|
21
|
+
return <div className="flex items-center justify-center min-h-screen">Loading...</div>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!session) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return <>{children}</>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Moon, Sun } from "lucide-react";
|
|
2
|
+
import { useTheme } from "next-themes";
|
|
3
|
+
import { Button } from "./ui/button";
|
|
4
|
+
|
|
5
|
+
export function ThemeToggle() {
|
|
6
|
+
const { theme, setTheme } = useTheme();
|
|
7
|
+
|
|
8
|
+
const toggleTheme = () => {
|
|
9
|
+
if (theme === "dark") {
|
|
10
|
+
setTheme("light");
|
|
11
|
+
} else if (theme === "light") {
|
|
12
|
+
setTheme("dark");
|
|
13
|
+
} else {
|
|
14
|
+
// system theme - toggle to opposite of current appearance
|
|
15
|
+
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
16
|
+
setTheme(isDark ? "light" : "dark");
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Button
|
|
22
|
+
variant="ghost"
|
|
23
|
+
size="icon"
|
|
24
|
+
className="h-9 w-9"
|
|
25
|
+
onClick={toggleTheme}
|
|
26
|
+
>
|
|
27
|
+
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
28
|
+
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
29
|
+
<span className="sr-only">Toggle theme</span>
|
|
30
|
+
</Button>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
3
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
|
8
|
+
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function AccordionItem({
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
|
15
|
+
return (
|
|
16
|
+
<AccordionPrimitive.Item
|
|
17
|
+
data-slot="accordion-item"
|
|
18
|
+
className={cn("border-b last:border-b-0", className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function AccordionTrigger({
|
|
25
|
+
className,
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
|
29
|
+
return (
|
|
30
|
+
<AccordionPrimitive.Header className="flex">
|
|
31
|
+
<AccordionPrimitive.Trigger
|
|
32
|
+
data-slot="accordion-trigger"
|
|
33
|
+
className={cn(
|
|
34
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
|
41
|
+
</AccordionPrimitive.Trigger>
|
|
42
|
+
</AccordionPrimitive.Header>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function AccordionContent({
|
|
47
|
+
className,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
|
51
|
+
return (
|
|
52
|
+
<AccordionPrimitive.Content
|
|
53
|
+
data-slot="accordion-content"
|
|
54
|
+
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
|
58
|
+
</AccordionPrimitive.Content>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
import { buttonVariants } from "@/components/ui/button";
|
|
6
|
+
|
|
7
|
+
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
|
8
|
+
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function AlertDialogTrigger({
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
|
14
|
+
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
|
18
|
+
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function AlertDialogOverlay({
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
|
25
|
+
return (
|
|
26
|
+
<AlertDialogPrimitive.Overlay
|
|
27
|
+
data-slot="alert-dialog-overlay"
|
|
28
|
+
className={cn(
|
|
29
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function AlertDialogContent({
|
|
38
|
+
className,
|
|
39
|
+
...props
|
|
40
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
|
41
|
+
return (
|
|
42
|
+
<AlertDialogPortal>
|
|
43
|
+
<AlertDialogOverlay />
|
|
44
|
+
<AlertDialogPrimitive.Content
|
|
45
|
+
data-slot="alert-dialog-content"
|
|
46
|
+
className={cn(
|
|
47
|
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
</AlertDialogPortal>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
data-slot="alert-dialog-header"
|
|
60
|
+
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
data-slot="alert-dialog-footer"
|
|
70
|
+
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function AlertDialogTitle({
|
|
77
|
+
className,
|
|
78
|
+
...props
|
|
79
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
|
80
|
+
return (
|
|
81
|
+
<AlertDialogPrimitive.Title
|
|
82
|
+
data-slot="alert-dialog-title"
|
|
83
|
+
className={cn("text-lg font-semibold", className)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function AlertDialogDescription({
|
|
90
|
+
className,
|
|
91
|
+
...props
|
|
92
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
|
93
|
+
return (
|
|
94
|
+
<AlertDialogPrimitive.Description
|
|
95
|
+
data-slot="alert-dialog-description"
|
|
96
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function AlertDialogAction({
|
|
103
|
+
className,
|
|
104
|
+
...props
|
|
105
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
|
106
|
+
return <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function AlertDialogCancel({
|
|
110
|
+
className,
|
|
111
|
+
...props
|
|
112
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
|
113
|
+
return (
|
|
114
|
+
<AlertDialogPrimitive.Cancel
|
|
115
|
+
className={cn(buttonVariants({ variant: "outline" }), className)}
|
|
116
|
+
{...props}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export {
|
|
122
|
+
AlertDialog,
|
|
123
|
+
AlertDialogPortal,
|
|
124
|
+
AlertDialogOverlay,
|
|
125
|
+
AlertDialogTrigger,
|
|
126
|
+
AlertDialogContent,
|
|
127
|
+
AlertDialogHeader,
|
|
128
|
+
AlertDialogFooter,
|
|
129
|
+
AlertDialogTitle,
|
|
130
|
+
AlertDialogDescription,
|
|
131
|
+
AlertDialogAction,
|
|
132
|
+
AlertDialogCancel,
|
|
133
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const alertVariants = cva(
|
|
7
|
+
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-card text-card-foreground",
|
|
12
|
+
destructive:
|
|
13
|
+
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: "default",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
function Alert({
|
|
23
|
+
className,
|
|
24
|
+
variant,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
data-slot="alert"
|
|
30
|
+
role="alert"
|
|
31
|
+
className={cn(alertVariants({ variant }), className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
data-slot="alert-title"
|
|
41
|
+
className={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
data-slot="alert-description"
|
|
51
|
+
className={cn(
|
|
52
|
+
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Alert, AlertTitle, AlertDescription };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
|
4
|
+
|
|
5
|
+
function AspectRatio({ ...props }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
|
6
|
+
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { AspectRatio };
|