create-varity-app 2.0.0-beta.1
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 +85 -0
- package/dist/create.js +141 -0
- package/dist/index.js +45 -0
- package/dist/utils.js +29 -0
- package/package.json +61 -0
- package/template/.env.example +17 -0
- package/template/KNOWN_ISSUES.md +69 -0
- package/template/LICENSE +21 -0
- package/template/README.md +241 -0
- package/template/gitignore +42 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.js +21 -0
- package/template/package.json +39 -0
- package/template/postcss.config.js +6 -0
- package/template/public/logo.svg +4 -0
- package/template/public/robots.txt +4 -0
- package/template/public/sitemap.xml +4 -0
- package/template/src/app/dashboard/layout.tsx +298 -0
- package/template/src/app/dashboard/page.tsx +209 -0
- package/template/src/app/dashboard/projects/page.tsx +638 -0
- package/template/src/app/dashboard/settings/page.tsx +749 -0
- package/template/src/app/dashboard/tasks/page.tsx +301 -0
- package/template/src/app/dashboard/team/page.tsx +295 -0
- package/template/src/app/globals.css +177 -0
- package/template/src/app/icon.svg +4 -0
- package/template/src/app/layout.tsx +33 -0
- package/template/src/app/login/page.tsx +98 -0
- package/template/src/app/not-found.tsx +20 -0
- package/template/src/app/page.tsx +23 -0
- package/template/src/components/dashboard/DashboardStats.tsx +137 -0
- package/template/src/components/dashboard/RecentActivity.tsx +63 -0
- package/template/src/components/landing/CTA.tsx +42 -0
- package/template/src/components/landing/Features.tsx +116 -0
- package/template/src/components/landing/Hero.tsx +146 -0
- package/template/src/components/landing/HowItWorks.tsx +80 -0
- package/template/src/components/landing/Pricing.tsx +124 -0
- package/template/src/components/landing/Testimonials.tsx +78 -0
- package/template/src/components/providers.tsx +11 -0
- package/template/src/components/shared/Footer.tsx +71 -0
- package/template/src/components/shared/Navbar.tsx +87 -0
- package/template/src/lib/constants.ts +35 -0
- package/template/src/lib/database.ts +7 -0
- package/template/src/lib/hooks.ts +331 -0
- package/template/src/lib/utils.ts +68 -0
- package/template/src/lib/varity.ts +1 -0
- package/template/src/services/dashboardService.ts +589 -0
- package/template/src/types/index.ts +52 -0
- package/template/tailwind.config.js +27 -0
- package/template/tsconfig.json +23 -0
- package/template/varity.config.json +14 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# TaskFlow — SaaS Starter Template
|
|
2
|
+
|
|
3
|
+
[](https://varity.so)
|
|
4
|
+
|
|
5
|
+
A full-featured project management app built with [Varity](https://varity.so). Everything works immediately — no configuration, no API keys, no setup.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That's it. Open [http://localhost:3000](http://localhost:3000) and your app is fully functional:
|
|
15
|
+
|
|
16
|
+
- **Auth** works instantly (email + Google login)
|
|
17
|
+
- **Database** works instantly (create, read, update, delete)
|
|
18
|
+
- **Dashboard** is fully interactive with real data persistence
|
|
19
|
+
|
|
20
|
+
No `.env` file needed. No accounts to create. No credentials to configure.
|
|
21
|
+
|
|
22
|
+
## Make This Your Own (5 Minutes)
|
|
23
|
+
|
|
24
|
+
Transform this into your own branded SaaS app:
|
|
25
|
+
|
|
26
|
+
1. **App name** — Edit `APP_NAME` in `src/lib/constants.ts`
|
|
27
|
+
2. **Logo** — Replace `public/logo.svg` with your logo
|
|
28
|
+
3. **Colors** — Open `src/app/globals.css` and uncomment a color preset (Purple, Green, or Orange) or set your own
|
|
29
|
+
4. **Meta title** — Update the `title` and `description` in `src/app/layout.tsx`
|
|
30
|
+
5. **Navigation** — Edit `NAVIGATION_ITEMS` in `src/lib/constants.ts` to rename or add sidebar links
|
|
31
|
+
6. **Landing page** — Edit the sections in `src/components/landing/` (Hero, Features, Pricing, etc.)
|
|
32
|
+
|
|
33
|
+
## Built-in Color Themes
|
|
34
|
+
|
|
35
|
+
Switch your entire app's color scheme by editing `src/app/globals.css`:
|
|
36
|
+
|
|
37
|
+
| Theme | How |
|
|
38
|
+
|-------|-----|
|
|
39
|
+
| **Blue** (default) | Active by default |
|
|
40
|
+
| **Purple** | Uncomment the Purple `:root` block, comment out Blue |
|
|
41
|
+
| **Green** | Uncomment the Green `:root` block, comment out Blue |
|
|
42
|
+
| **Orange** | Uncomment the Orange `:root` block, comment out Blue |
|
|
43
|
+
| **Custom** | Set your own `--color-primary-*` values using any [Tailwind palette](https://tailwindcss.com/docs/customizing-colors) |
|
|
44
|
+
|
|
45
|
+
## What's Included
|
|
46
|
+
|
|
47
|
+
- **Zero-Config Auth** — Email and social login works out of the box
|
|
48
|
+
- **Zero-Config Database** — Data persistence with isolated dev environment
|
|
49
|
+
- **Dashboard** — KPI cards, data tables, status badges, getting started guide
|
|
50
|
+
- **Full CRUD** — Create, read, update, delete for projects, tasks, and team members
|
|
51
|
+
- **Command Palette** — Cmd+K search across all data
|
|
52
|
+
- **Protected Routes** — Automatic redirect for unauthenticated users
|
|
53
|
+
- **Landing Page** — Professional marketing page with hero, features, pricing, testimonials
|
|
54
|
+
- **Mobile Responsive** — Hamburger menu, responsive layouts, touch-friendly
|
|
55
|
+
- **TypeScript** — Full type safety throughout
|
|
56
|
+
- **Tailwind CSS** — Utility-first styling with CSS variable theming
|
|
57
|
+
|
|
58
|
+
## ✅ Zero Configuration Required
|
|
59
|
+
|
|
60
|
+
This template works immediately with **zero setup**:
|
|
61
|
+
|
|
62
|
+
### Instant Auth
|
|
63
|
+
- ✅ Email login (Privy)
|
|
64
|
+
- ✅ Google/Apple social login
|
|
65
|
+
- ✅ Dev credentials built-in
|
|
66
|
+
- ❌ No env vars needed
|
|
67
|
+
|
|
68
|
+
### Instant Database
|
|
69
|
+
- ✅ Create, read, update, delete data
|
|
70
|
+
- ✅ Dev token built-in
|
|
71
|
+
- ✅ Production-ready proxy
|
|
72
|
+
- ❌ No credentials needed
|
|
73
|
+
|
|
74
|
+
### Instant Deploy
|
|
75
|
+
```bash
|
|
76
|
+
npm run deploy
|
|
77
|
+
```
|
|
78
|
+
- ✅ Deploys to IPFS
|
|
79
|
+
- ✅ Auto-fetches credentials
|
|
80
|
+
- ❌ No thirdweb account needed
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🏗️ Architecture
|
|
85
|
+
|
|
86
|
+
### Workspace Dependencies
|
|
87
|
+
This template uses `workspace:^` protocol for Varity packages:
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"dependencies": {
|
|
91
|
+
"@varity-labs/sdk": "workspace:^",
|
|
92
|
+
"@varity-labs/ui-kit": "workspace:^",
|
|
93
|
+
"@varity-labs/types": "workspace:^"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Why?** Ensures you always use the latest local package versions during development.
|
|
99
|
+
|
|
100
|
+
**Publishing:** When published to npm, `workspace:^` converts to `^2.0.0-alpha.1` automatically.
|
|
101
|
+
|
|
102
|
+
### Static Export Ready
|
|
103
|
+
- ✅ `output: 'export'` in next.config.js
|
|
104
|
+
- ✅ All pages pre-rendered to static HTML
|
|
105
|
+
- ✅ No server-side dependencies
|
|
106
|
+
- ✅ IPFS/CDN deployable
|
|
107
|
+
|
|
108
|
+
### Type Safety
|
|
109
|
+
- ✅ TypeScript strict mode enabled
|
|
110
|
+
- ✅ All errors surface during build
|
|
111
|
+
- ✅ No `ignoreBuildErrors` flag
|
|
112
|
+
|
|
113
|
+
## Project Structure
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
src/
|
|
117
|
+
app/ # Pages (Next.js App Router)
|
|
118
|
+
dashboard/ # Protected dashboard pages
|
|
119
|
+
projects/ # Project management (list + detail views)
|
|
120
|
+
tasks/ # Task management (status filters, CSV export)
|
|
121
|
+
team/ # Team management (invite, roles)
|
|
122
|
+
settings/ # User settings (4-tab layout with backend persistence)
|
|
123
|
+
login/ # Login page
|
|
124
|
+
components/ # Reusable components
|
|
125
|
+
dashboard/ # Dashboard-specific components
|
|
126
|
+
landing/ # Landing page sections (Hero, Features, Pricing, etc.)
|
|
127
|
+
shared/ # Shared components (Navbar, Footer)
|
|
128
|
+
providers/ # App providers (auth, database, toast)
|
|
129
|
+
lib/ # Core utilities
|
|
130
|
+
varity.ts # SDK initialization
|
|
131
|
+
database.ts # Typed database collections
|
|
132
|
+
hooks.ts # Data hooks (useProjects, useTasks, useTeam)
|
|
133
|
+
constants.ts # App name, navigation, option lists
|
|
134
|
+
utils.ts # Helpers (CSV export, formatting)
|
|
135
|
+
types/ # TypeScript type definitions
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Add a New Page
|
|
139
|
+
|
|
140
|
+
Example: adding a `/dashboard/reports` page.
|
|
141
|
+
|
|
142
|
+
**1. Create the page file:**
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// src/app/dashboard/reports/page.tsx
|
|
146
|
+
'use client';
|
|
147
|
+
|
|
148
|
+
import { useProjects } from '@/lib/hooks';
|
|
149
|
+
|
|
150
|
+
export default function ReportsPage() {
|
|
151
|
+
const { data: projects, loading } = useProjects();
|
|
152
|
+
|
|
153
|
+
if (loading) return <div className="p-6">Loading...</div>;
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div className="space-y-6">
|
|
157
|
+
<h1 className="text-2xl font-bold text-gray-900">Reports</h1>
|
|
158
|
+
<p className="text-gray-600">{projects.length} projects total</p>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**2. Add navigation item** in `src/lib/constants.ts`:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
{ label: 'Reports', icon: 'chart', path: '/dashboard/reports' },
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Done. The page is automatically protected by auth and appears in the sidebar.
|
|
171
|
+
|
|
172
|
+
## Add a New Data Collection
|
|
173
|
+
|
|
174
|
+
Example: adding an `invoices` collection.
|
|
175
|
+
|
|
176
|
+
**1. Define the type** in `src/types/index.ts`:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
export interface Invoice {
|
|
180
|
+
id: string;
|
|
181
|
+
projectId: string;
|
|
182
|
+
amount: number;
|
|
183
|
+
status: 'draft' | 'sent' | 'paid';
|
|
184
|
+
createdAt: string;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**2. Create the collection** in `src/lib/database.ts`:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
export const invoices = () => db.collection<Invoice>('invoices');
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**3. Create a hook** in `src/lib/hooks.ts` (copy the `useProjects` pattern):
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
export function useInvoices(): UseCollectionReturn<Invoice> {
|
|
198
|
+
// ... same pattern as useProjects, using invoices() instead of projects()
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**4. Use it in any page:**
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
const { data, loading, create, update, remove } = useInvoices();
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The database collection is created automatically on first use — no migrations needed.
|
|
209
|
+
|
|
210
|
+
## Environment Variables
|
|
211
|
+
|
|
212
|
+
**For development:** Leave everything blank. Shared development credentials are used automatically.
|
|
213
|
+
|
|
214
|
+
**For production:** Run `varitykit app deploy` — it injects all credentials into your build automatically. You never need to manually set API keys.
|
|
215
|
+
|
|
216
|
+
| Variable | Required | Notes |
|
|
217
|
+
|----------|----------|-------|
|
|
218
|
+
| `NEXT_PUBLIC_PRIVY_APP_ID` | No | Auth provider (auto-configured) |
|
|
219
|
+
| `NEXT_PUBLIC_THIRDWEB_CLIENT_ID` | No | Infrastructure (auto-configured) |
|
|
220
|
+
| `NEXT_PUBLIC_VARITY_APP_TOKEN` | No | Database token (auto-configured) |
|
|
221
|
+
| `NEXT_PUBLIC_VARITY_APP_ID` | No | App ID (auto-configured) |
|
|
222
|
+
|
|
223
|
+
## Deployment
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Deploy to production with a live URL
|
|
227
|
+
varitykit app deploy
|
|
228
|
+
|
|
229
|
+
# Deploy and submit to the Varity App Store
|
|
230
|
+
varitykit app deploy --submit-to-store
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The CLI builds your app, provisions a private database, injects production credentials, and deploys — all in one command.
|
|
234
|
+
|
|
235
|
+
**Deploy from your AI editor:** Set up the [Varity MCP server](https://docs.varity.so/mcp) (`npx @varity-labs/mcp`) and ask your AI to "deploy this project".
|
|
236
|
+
|
|
237
|
+
## Learn More
|
|
238
|
+
|
|
239
|
+
- [Varity Documentation](https://docs.varity.so)
|
|
240
|
+
- [UI Kit Components](https://docs.varity.so/ui-kit)
|
|
241
|
+
- [SDK Reference](https://docs.varity.so/sdk)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
.next/
|
|
6
|
+
out/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
|
|
10
|
+
# TypeScript
|
|
11
|
+
*.tsbuildinfo
|
|
12
|
+
next-env.d.ts
|
|
13
|
+
|
|
14
|
+
# Environment variables (secrets)
|
|
15
|
+
.env.local
|
|
16
|
+
.env.development.local
|
|
17
|
+
.env.test.local
|
|
18
|
+
.env.production.local
|
|
19
|
+
|
|
20
|
+
# Debug logs
|
|
21
|
+
npm-debug.log*
|
|
22
|
+
yarn-debug.log*
|
|
23
|
+
yarn-error.log*
|
|
24
|
+
pnpm-debug.log*
|
|
25
|
+
|
|
26
|
+
# OS files
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
|
29
|
+
|
|
30
|
+
# IDE
|
|
31
|
+
.vscode/
|
|
32
|
+
.idea/
|
|
33
|
+
*.swp
|
|
34
|
+
*.swo
|
|
35
|
+
|
|
36
|
+
# Test results
|
|
37
|
+
test-results/
|
|
38
|
+
playwright-report/
|
|
39
|
+
blob-report/
|
|
40
|
+
|
|
41
|
+
# Vercel (if ejected)
|
|
42
|
+
.vercel
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/// <reference types="next" />
|
|
2
|
+
/// <reference types="next/image-types/global" />
|
|
3
|
+
/// <reference path="./.next/types/routes.d.ts" />
|
|
4
|
+
|
|
5
|
+
// NOTE: This file should not be edited
|
|
6
|
+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** @type {import('next').NextConfig} */
|
|
2
|
+
const nextConfig = {
|
|
3
|
+
output: 'export',
|
|
4
|
+
images: { unoptimized: true },
|
|
5
|
+
trailingSlash: true,
|
|
6
|
+
productionBrowserSourceMaps: false,
|
|
7
|
+
webpack: (config, { isServer, dev }) => {
|
|
8
|
+
// Suppress MetaMask SDK warning for @react-native-async-storage
|
|
9
|
+
config.resolve.fallback = {
|
|
10
|
+
...config.resolve.fallback,
|
|
11
|
+
'@react-native-async-storage/async-storage': false,
|
|
12
|
+
};
|
|
13
|
+
// Force production devtool to avoid 35MB eval-source-map chunks
|
|
14
|
+
if (!dev && !isServer) {
|
|
15
|
+
config.devtool = false;
|
|
16
|
+
}
|
|
17
|
+
return config;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
module.exports = nextConfig;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-saas-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"lint": "next lint",
|
|
11
|
+
"type-check": "tsc --noEmit",
|
|
12
|
+
"test:e2e": "playwright test",
|
|
13
|
+
"test:e2e:ui": "playwright test --ui",
|
|
14
|
+
"test:e2e:headed": "playwright test --headed",
|
|
15
|
+
"test:e2e:debug": "playwright test --debug",
|
|
16
|
+
"prepare": "husky install",
|
|
17
|
+
"deploy": "varitykit app deploy"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@varity-labs/sdk": "workspace:^",
|
|
21
|
+
"@varity-labs/types": "workspace:^",
|
|
22
|
+
"@varity-labs/ui-kit": "workspace:^",
|
|
23
|
+
"lucide-react": "^0.400.0",
|
|
24
|
+
"next": "^15.0.0",
|
|
25
|
+
"react": "^18.3.0",
|
|
26
|
+
"react-dom": "^18.3.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@playwright/test": "^1.58.2",
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"@types/react": "^18.3.0",
|
|
32
|
+
"@types/react-dom": "^18.3.0",
|
|
33
|
+
"autoprefixer": "^10.0.0",
|
|
34
|
+
"husky": "^9.1.7",
|
|
35
|
+
"postcss": "^8.0.0",
|
|
36
|
+
"tailwindcss": "^3.4.0",
|
|
37
|
+
"typescript": "^5.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="16" cy="16" r="14" fill="#4f46e5"/>
|
|
3
|
+
<path d="M10 16L14.5 20.5L22 11.5" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useRouter, usePathname } from 'next/navigation';
|
|
5
|
+
import { APP_NAME, NAVIGATION_ITEMS } from '@/lib/constants';
|
|
6
|
+
import { useProjects, useTasks, useTeam } from '@/lib/hooks';
|
|
7
|
+
import { CommandPalette } from '@varity-labs/ui-kit';
|
|
8
|
+
import { Menu, X } from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
// Conditionally import Privy/UI-Kit components
|
|
11
|
+
let DashboardLayout: any = null;
|
|
12
|
+
let PrivyProtectedRoute: any = null;
|
|
13
|
+
let PrivyStackComponent: any = null;
|
|
14
|
+
let usePrivyHook: any = null;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const uiKit = require('@varity-labs/ui-kit');
|
|
18
|
+
DashboardLayout = uiKit.DashboardLayout;
|
|
19
|
+
PrivyProtectedRoute = uiKit.PrivyProtectedRoute;
|
|
20
|
+
PrivyStackComponent = uiKit.PrivyStack;
|
|
21
|
+
usePrivyHook = uiKit.usePrivy;
|
|
22
|
+
} catch {}
|
|
23
|
+
|
|
24
|
+
function RedirectToLogin() {
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
router.push('/login');
|
|
28
|
+
}, [router]);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function useIsMobile() {
|
|
33
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
function check() {
|
|
37
|
+
setIsMobile(window.innerWidth < 768);
|
|
38
|
+
}
|
|
39
|
+
check();
|
|
40
|
+
window.addEventListener('resize', check);
|
|
41
|
+
return () => window.removeEventListener('resize', check);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
return isMobile;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function MobileNav({
|
|
48
|
+
open,
|
|
49
|
+
onToggle,
|
|
50
|
+
onClose,
|
|
51
|
+
navItems,
|
|
52
|
+
userEmail,
|
|
53
|
+
onLogout,
|
|
54
|
+
onNavigate,
|
|
55
|
+
}: {
|
|
56
|
+
open: boolean;
|
|
57
|
+
onToggle: () => void;
|
|
58
|
+
onClose: () => void;
|
|
59
|
+
navItems: { path: string; label: string; active: boolean }[];
|
|
60
|
+
userEmail: string;
|
|
61
|
+
onLogout: () => void;
|
|
62
|
+
onNavigate: (path: string) => void;
|
|
63
|
+
}) {
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<button
|
|
67
|
+
onClick={onToggle}
|
|
68
|
+
className="fixed top-4 left-4 z-[60] rounded-lg bg-white p-2 shadow-md border border-gray-200"
|
|
69
|
+
aria-label={open ? 'Close menu' : 'Open menu'}
|
|
70
|
+
>
|
|
71
|
+
{open ? <X className="h-5 w-5 text-gray-700" /> : <Menu className="h-5 w-5 text-gray-700" />}
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
{open && (
|
|
75
|
+
<>
|
|
76
|
+
<div className="fixed inset-0 z-[55] bg-black/50" onClick={onClose} />
|
|
77
|
+
<div className="fixed inset-y-0 left-0 z-[56] w-64 bg-white shadow-xl overflow-y-auto">
|
|
78
|
+
<div className="p-4 pt-16">
|
|
79
|
+
<div className="mb-6">
|
|
80
|
+
<h2 className="text-lg font-semibold text-gray-900">{APP_NAME}</h2>
|
|
81
|
+
<p className="text-sm text-gray-500">{userEmail || 'Not signed in'}</p>
|
|
82
|
+
</div>
|
|
83
|
+
<nav className="space-y-1">
|
|
84
|
+
{navItems.map((item) => (
|
|
85
|
+
<button
|
|
86
|
+
key={item.path}
|
|
87
|
+
onClick={() => { onNavigate(item.path); onClose(); }}
|
|
88
|
+
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
|
|
89
|
+
item.active ? 'bg-primary-50 text-primary-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
|
|
90
|
+
}`}
|
|
91
|
+
>
|
|
92
|
+
{item.label}
|
|
93
|
+
</button>
|
|
94
|
+
))}
|
|
95
|
+
</nav>
|
|
96
|
+
<div className="mt-8 border-t border-gray-200 pt-4">
|
|
97
|
+
<button
|
|
98
|
+
onClick={onLogout}
|
|
99
|
+
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-red-600 hover:bg-red-50 transition-colors"
|
|
100
|
+
>
|
|
101
|
+
Sign Out
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
113
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
114
|
+
const privy = usePrivyHook ? usePrivyHook() : { user: null, logout: async () => {} };
|
|
115
|
+
const { user, logout } = privy;
|
|
116
|
+
const pathname = usePathname();
|
|
117
|
+
const router = useRouter();
|
|
118
|
+
const isMobile = useIsMobile();
|
|
119
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
120
|
+
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
121
|
+
|
|
122
|
+
// Data for command palette search
|
|
123
|
+
const { data: projects } = useProjects();
|
|
124
|
+
const { data: tasks } = useTasks();
|
|
125
|
+
const { data: team } = useTeam();
|
|
126
|
+
|
|
127
|
+
// Cmd+K / Ctrl+K keyboard shortcut
|
|
128
|
+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
129
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
setCommandPaletteOpen((prev) => !prev);
|
|
132
|
+
}
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
137
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
138
|
+
}, [handleKeyDown]);
|
|
139
|
+
|
|
140
|
+
// Close mobile menu on navigation
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setMobileMenuOpen(false);
|
|
143
|
+
}, [pathname]);
|
|
144
|
+
|
|
145
|
+
const navWithActive = NAVIGATION_ITEMS.map((item) => ({
|
|
146
|
+
...item,
|
|
147
|
+
active: item.path === '/dashboard'
|
|
148
|
+
? pathname === '/dashboard'
|
|
149
|
+
: pathname === item.path || pathname.startsWith(item.path + '/'),
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
const userName = user?.email?.address?.split('@')[0] || 'User';
|
|
153
|
+
const userEmail = user?.email?.address || '';
|
|
154
|
+
|
|
155
|
+
const handleLogout = async () => {
|
|
156
|
+
await logout();
|
|
157
|
+
router.push('/');
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Fallback layout when DashboardLayout from ui-kit isn't available
|
|
161
|
+
if (!DashboardLayout) {
|
|
162
|
+
return (
|
|
163
|
+
<>
|
|
164
|
+
<CommandPalette
|
|
165
|
+
open={commandPaletteOpen}
|
|
166
|
+
onClose={() => setCommandPaletteOpen(false)}
|
|
167
|
+
onNavigate={(path: string) => router.push(path)}
|
|
168
|
+
projects={projects}
|
|
169
|
+
tasks={tasks}
|
|
170
|
+
team={team}
|
|
171
|
+
/>
|
|
172
|
+
<div className="flex min-h-screen">
|
|
173
|
+
{/* Simple sidebar */}
|
|
174
|
+
{!isMobile && (
|
|
175
|
+
<div className="w-64 shrink-0 border-r border-gray-200 bg-white">
|
|
176
|
+
<div className="p-4">
|
|
177
|
+
<h2 className="text-lg font-semibold text-gray-900">{APP_NAME}</h2>
|
|
178
|
+
<p className="text-sm text-gray-500">{userEmail || 'Not signed in'}</p>
|
|
179
|
+
</div>
|
|
180
|
+
<nav className="px-3 space-y-1">
|
|
181
|
+
{navWithActive.map((item) => (
|
|
182
|
+
<button
|
|
183
|
+
key={item.path}
|
|
184
|
+
onClick={() => router.push(item.path)}
|
|
185
|
+
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
|
|
186
|
+
item.active
|
|
187
|
+
? 'bg-primary-50 text-primary-700'
|
|
188
|
+
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
|
|
189
|
+
}`}
|
|
190
|
+
>
|
|
191
|
+
{item.label}
|
|
192
|
+
</button>
|
|
193
|
+
))}
|
|
194
|
+
</nav>
|
|
195
|
+
<div className="mt-8 px-3 border-t border-gray-200 pt-4">
|
|
196
|
+
<button
|
|
197
|
+
onClick={handleLogout}
|
|
198
|
+
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-red-600 hover:bg-red-50 transition-colors"
|
|
199
|
+
>
|
|
200
|
+
Sign Out
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{isMobile && (
|
|
207
|
+
<MobileNav
|
|
208
|
+
open={mobileMenuOpen}
|
|
209
|
+
onToggle={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
210
|
+
onClose={() => setMobileMenuOpen(false)}
|
|
211
|
+
navItems={navWithActive}
|
|
212
|
+
userEmail={userEmail}
|
|
213
|
+
onLogout={handleLogout}
|
|
214
|
+
onNavigate={(path) => router.push(path)}
|
|
215
|
+
/>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
<main className={`flex-1 bg-gray-50 p-6 ${isMobile ? 'pt-16' : ''}`}>
|
|
219
|
+
{children}
|
|
220
|
+
</main>
|
|
221
|
+
</div>
|
|
222
|
+
</>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<>
|
|
228
|
+
<CommandPalette
|
|
229
|
+
open={commandPaletteOpen}
|
|
230
|
+
onClose={() => setCommandPaletteOpen(false)}
|
|
231
|
+
onNavigate={(path: string) => router.push(path)}
|
|
232
|
+
projects={projects}
|
|
233
|
+
tasks={tasks}
|
|
234
|
+
team={team}
|
|
235
|
+
/>
|
|
236
|
+
|
|
237
|
+
{isMobile && (
|
|
238
|
+
<MobileNav
|
|
239
|
+
open={mobileMenuOpen}
|
|
240
|
+
onToggle={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
241
|
+
onClose={() => setMobileMenuOpen(false)}
|
|
242
|
+
navItems={navWithActive}
|
|
243
|
+
userEmail={userEmail}
|
|
244
|
+
onLogout={handleLogout}
|
|
245
|
+
onNavigate={(path) => router.push(path)}
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{/* Desktop layout */}
|
|
250
|
+
<div className={isMobile ? '[&>div>div:first-child]:hidden' : ''}>
|
|
251
|
+
<DashboardLayout
|
|
252
|
+
companyName={APP_NAME}
|
|
253
|
+
logoUrl="/logo.svg"
|
|
254
|
+
navigationItems={navWithActive}
|
|
255
|
+
showSidebar={!isMobile}
|
|
256
|
+
user={{
|
|
257
|
+
name: userName,
|
|
258
|
+
address: userEmail,
|
|
259
|
+
}}
|
|
260
|
+
onLogout={handleLogout}
|
|
261
|
+
onNavigateToProfile={() => router.push('/dashboard/settings')}
|
|
262
|
+
onNavigateToSettings={() => router.push('/dashboard/settings')}
|
|
263
|
+
onSearchClick={() => setCommandPaletteOpen(true)}
|
|
264
|
+
searchPlaceholder="Search projects, tasks, team..."
|
|
265
|
+
>
|
|
266
|
+
<div className={isMobile ? 'pt-14' : ''}>
|
|
267
|
+
{children}
|
|
268
|
+
</div>
|
|
269
|
+
</DashboardLayout>
|
|
270
|
+
</div>
|
|
271
|
+
</>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export default function DashboardRootLayout({
|
|
276
|
+
children,
|
|
277
|
+
}: {
|
|
278
|
+
children: React.ReactNode;
|
|
279
|
+
}) {
|
|
280
|
+
// Always wrap in PrivyStack + PrivyProtectedRoute - uses dev credentials automatically when env vars are empty
|
|
281
|
+
if (!PrivyProtectedRoute || !PrivyStackComponent) {
|
|
282
|
+
// Fallback if ui-kit package isn't installed
|
|
283
|
+
return <DashboardShell>{children}</DashboardShell>;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<PrivyStackComponent
|
|
288
|
+
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
|
|
289
|
+
thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
|
|
290
|
+
loginMethods={['email', 'google']}
|
|
291
|
+
appearance={{ theme: 'light', accentColor: '#2563EB', logo: '/logo.svg' }}
|
|
292
|
+
>
|
|
293
|
+
<PrivyProtectedRoute fallback={<RedirectToLogin />}>
|
|
294
|
+
<DashboardShell>{children}</DashboardShell>
|
|
295
|
+
</PrivyProtectedRoute>
|
|
296
|
+
</PrivyStackComponent>
|
|
297
|
+
);
|
|
298
|
+
}
|