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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/create.js +141 -0
  4. package/dist/index.js +45 -0
  5. package/dist/utils.js +29 -0
  6. package/package.json +61 -0
  7. package/template/.env.example +17 -0
  8. package/template/KNOWN_ISSUES.md +69 -0
  9. package/template/LICENSE +21 -0
  10. package/template/README.md +241 -0
  11. package/template/gitignore +42 -0
  12. package/template/next-env.d.ts +6 -0
  13. package/template/next.config.js +21 -0
  14. package/template/package.json +39 -0
  15. package/template/postcss.config.js +6 -0
  16. package/template/public/logo.svg +4 -0
  17. package/template/public/robots.txt +4 -0
  18. package/template/public/sitemap.xml +4 -0
  19. package/template/src/app/dashboard/layout.tsx +298 -0
  20. package/template/src/app/dashboard/page.tsx +209 -0
  21. package/template/src/app/dashboard/projects/page.tsx +638 -0
  22. package/template/src/app/dashboard/settings/page.tsx +749 -0
  23. package/template/src/app/dashboard/tasks/page.tsx +301 -0
  24. package/template/src/app/dashboard/team/page.tsx +295 -0
  25. package/template/src/app/globals.css +177 -0
  26. package/template/src/app/icon.svg +4 -0
  27. package/template/src/app/layout.tsx +33 -0
  28. package/template/src/app/login/page.tsx +98 -0
  29. package/template/src/app/not-found.tsx +20 -0
  30. package/template/src/app/page.tsx +23 -0
  31. package/template/src/components/dashboard/DashboardStats.tsx +137 -0
  32. package/template/src/components/dashboard/RecentActivity.tsx +63 -0
  33. package/template/src/components/landing/CTA.tsx +42 -0
  34. package/template/src/components/landing/Features.tsx +116 -0
  35. package/template/src/components/landing/Hero.tsx +146 -0
  36. package/template/src/components/landing/HowItWorks.tsx +80 -0
  37. package/template/src/components/landing/Pricing.tsx +124 -0
  38. package/template/src/components/landing/Testimonials.tsx +78 -0
  39. package/template/src/components/providers.tsx +11 -0
  40. package/template/src/components/shared/Footer.tsx +71 -0
  41. package/template/src/components/shared/Navbar.tsx +87 -0
  42. package/template/src/lib/constants.ts +35 -0
  43. package/template/src/lib/database.ts +7 -0
  44. package/template/src/lib/hooks.ts +331 -0
  45. package/template/src/lib/utils.ts +68 -0
  46. package/template/src/lib/varity.ts +1 -0
  47. package/template/src/services/dashboardService.ts +589 -0
  48. package/template/src/types/index.ts +52 -0
  49. package/template/tailwind.config.js +27 -0
  50. package/template/tsconfig.json +23 -0
  51. package/template/varity.config.json +14 -0
@@ -0,0 +1,241 @@
1
+ # TaskFlow — SaaS Starter Template
2
+
3
+ [![Built with Varity](https://img.shields.io/badge/built%20with-Varity-7C3AED)](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,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -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,4 @@
1
+ User-agent: *
2
+ Allow: /
3
+ Disallow: /dashboard/
4
+ Disallow: /login/
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
+ <url><loc>https://example.com/</loc></url>
4
+ </urlset>
@@ -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
+ }