create-varity-app 2.0.0-beta.1 → 2.0.0-beta.11
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/README.md +21 -20
- package/dist/create.js +5 -6
- package/package.json +1 -1
- package/template/.turbo/turbo-build.log +6 -0
- package/template/KNOWN_ISSUES.md +25 -35
- package/template/README.md +7 -8
- package/template/src/app/dashboard/layout.tsx +4 -3
- package/template/src/app/dashboard/page.tsx +5 -7
- package/template/src/app/dashboard/projects/page.tsx +4 -4
- package/template/src/app/dashboard/tasks/page.tsx +4 -4
- package/template/src/app/dashboard/team/page.tsx +4 -4
- package/template/src/app/layout.tsx +10 -7
- package/template/src/app/login/page.tsx +8 -8
- package/template/src/app/not-found.tsx +0 -2
- package/template/src/app/page.tsx +1 -2
- package/template/src/components/dashboard/RecentActivity.tsx +1 -2
- package/template/src/components/landing/Pricing.tsx +10 -9
- package/template/src/components/shared/Footer.tsx +1 -1
- package/template/src/components/shared/Navbar.tsx +0 -2
- package/template/src/lib/constants.ts +1 -1
- package/template/src/lib/hooks.ts +16 -4
- package/template/src/services/dashboardService.ts +0 -589
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/create-varity-app)
|
|
4
4
|
[](https://github.com/varity-labs/varity-sdk/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
Create production-ready apps with auth
|
|
6
|
+
Create production-ready apps with auth and database built in. Payments coming soon.
|
|
7
7
|
|
|
8
8
|
## Quick Start
|
|
9
9
|
|
|
@@ -23,12 +23,12 @@ yarn create varity-app my-app
|
|
|
23
23
|
## What You Get
|
|
24
24
|
|
|
25
25
|
- **Next.js 15** app with TypeScript and Tailwind CSS
|
|
26
|
-
- **Authentication**
|
|
27
|
-
- **Database**
|
|
28
|
-
- **Dashboard**
|
|
29
|
-
- **Landing page**
|
|
30
|
-
- **Settings**
|
|
31
|
-
- **
|
|
26
|
+
- **Authentication** -- email, social login, and more (zero config)
|
|
27
|
+
- **Database** -- ready-to-use collections with typed queries
|
|
28
|
+
- **Dashboard** -- professional layout with sidebar navigation
|
|
29
|
+
- **Landing page** -- hero, features, pricing, how it works, testimonials, CTA
|
|
30
|
+
- **Settings** -- General, Security, Billing, and Account tabs
|
|
31
|
+
- **8 pages** -- landing, login, dashboard, projects, tasks, team, settings, 404
|
|
32
32
|
|
|
33
33
|
## Project Structure
|
|
34
34
|
|
|
@@ -36,15 +36,16 @@ yarn create varity-app my-app
|
|
|
36
36
|
my-app/
|
|
37
37
|
├── src/
|
|
38
38
|
│ └── app/
|
|
39
|
-
│ ├──
|
|
40
|
-
│ ├──
|
|
41
|
-
│ │ ├──
|
|
39
|
+
│ ├── login/ # Login page
|
|
40
|
+
│ ├── dashboard/ # Protected dashboard pages
|
|
41
|
+
│ │ ├── page.tsx # Overview with KPIs
|
|
42
42
|
│ │ ├── projects/ # Project management
|
|
43
43
|
│ │ ├── tasks/ # Task tracking
|
|
44
44
|
│ │ ├── team/ # Team members
|
|
45
|
-
│ │ └── settings/ # App settings (
|
|
45
|
+
│ │ └── settings/ # App settings (4 tabs)
|
|
46
|
+
│ ├── not-found.tsx # 404 page
|
|
46
47
|
│ └── page.tsx # Landing page
|
|
47
|
-
├── tailwind.config.
|
|
48
|
+
├── tailwind.config.js
|
|
48
49
|
├── next.config.js
|
|
49
50
|
└── package.json
|
|
50
51
|
```
|
|
@@ -60,26 +61,26 @@ varitykit app deploy
|
|
|
60
61
|
|
|
61
62
|
Your app will be live in under 60 seconds.
|
|
62
63
|
|
|
63
|
-
Or deploy from your AI editor with the [Varity MCP server](
|
|
64
|
+
Or deploy from your AI editor with the [Varity MCP server](https://www.npmjs.com/package/@varity-labs/mcp) -- just ask "deploy this project".
|
|
64
65
|
|
|
65
66
|
## Related Packages
|
|
66
67
|
|
|
67
|
-
- **[@varity-labs/sdk](
|
|
68
|
-
- **[@varity-labs/ui-kit](
|
|
69
|
-
- **[@varity-labs/mcp](
|
|
68
|
+
- **[@varity-labs/sdk](https://www.npmjs.com/package/@varity-labs/sdk)** -- Core SDK (database, credentials)
|
|
69
|
+
- **[@varity-labs/ui-kit](https://www.npmjs.com/package/@varity-labs/ui-kit)** -- React UI components
|
|
70
|
+
- **[@varity-labs/mcp](https://www.npmjs.com/package/@varity-labs/mcp)** -- MCP server for AI editors (Cursor, Claude Code, VS Code)
|
|
70
71
|
|
|
71
72
|
## Learn More
|
|
72
73
|
|
|
73
74
|
- [Documentation](https://docs.varity.so)
|
|
74
75
|
- [GitHub](https://github.com/varity-labs/varity-sdk)
|
|
75
|
-
- [Discord](https://discord.gg/
|
|
76
|
+
- [Discord](https://discord.gg/7vWsdwa2Bg)
|
|
76
77
|
|
|
77
78
|
---
|
|
78
79
|
|
|
79
|
-
**Part of the [Varity SDK](https://github.com/varity-labs/varity-sdk)**
|
|
80
|
+
**Part of the [Varity SDK](https://github.com/varity-labs/varity-sdk)** -- Build, deploy, and monetize apps 70% cheaper than AWS.
|
|
80
81
|
|
|
81
|
-
[Documentation](https://docs.varity.so)
|
|
82
|
+
[Documentation](https://docs.varity.so) | [GitHub](https://github.com/varity-labs/varity-sdk) | [Discord](https://discord.gg/7vWsdwa2Bg)
|
|
82
83
|
|
|
83
84
|
## License
|
|
84
85
|
|
|
85
|
-
MIT
|
|
86
|
+
MIT -- [Varity Labs](https://www.varity.so)
|
package/dist/create.js
CHANGED
|
@@ -7,11 +7,10 @@ import ora from "ora";
|
|
|
7
7
|
import { getInstallCommand } from "./utils.js";
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = path.dirname(__filename);
|
|
10
|
-
const VARITY_PACKAGE_VERSION = "^2.0.0-alpha.1";
|
|
11
10
|
const WORKSPACE_DEPS = {
|
|
12
|
-
"@varity-labs/sdk":
|
|
13
|
-
"@varity-labs/types":
|
|
14
|
-
"@varity-labs/ui-kit":
|
|
11
|
+
"@varity-labs/sdk": "2.0.0-beta.6",
|
|
12
|
+
"@varity-labs/types": "2.0.0-beta.4",
|
|
13
|
+
"@varity-labs/ui-kit": "2.0.0-beta.7",
|
|
15
14
|
};
|
|
16
15
|
const EXCLUDED_FILES = new Set([
|
|
17
16
|
".env.local",
|
|
@@ -33,7 +32,7 @@ function rewritePackageJson(content, projectName) {
|
|
|
33
32
|
for (const [name, version] of Object.entries(deps)) {
|
|
34
33
|
if (typeof version === "string" &&
|
|
35
34
|
version.startsWith("workspace:")) {
|
|
36
|
-
deps[name] = WORKSPACE_DEPS[name] ||
|
|
35
|
+
deps[name] = WORKSPACE_DEPS[name] || "2.0.0-beta.4";
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
}
|
|
@@ -136,6 +135,6 @@ export async function createApp(projectName, packageManager) {
|
|
|
136
135
|
console.log(chalk.cyan(" varitykit app deploy"));
|
|
137
136
|
console.log();
|
|
138
137
|
console.log(` Docs: ${chalk.underline("https://docs.varity.so")}`);
|
|
139
|
-
console.log(` Help: ${chalk.underline("https://discord.gg/
|
|
138
|
+
console.log(` Help: ${chalk.underline("https://discord.gg/7vWsdwa2Bg")}`);
|
|
140
139
|
console.log();
|
|
141
140
|
}
|
package/package.json
CHANGED
package/template/KNOWN_ISSUES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Known Issues — SaaS Starter Template
|
|
2
2
|
|
|
3
|
-
> **Last Updated:**
|
|
3
|
+
> **Last Updated:** March 8, 2026
|
|
4
4
|
> **Template:** `saas-starter` (TaskFlow)
|
|
5
5
|
> **Status:** All features functional, builds with 0 errors (8 routes + _not-found)
|
|
6
6
|
|
|
@@ -10,60 +10,50 @@
|
|
|
10
10
|
|
|
11
11
|
| Feature | Status | Notes |
|
|
12
12
|
|---------|--------|-------|
|
|
13
|
-
| Landing Page | Working | 6 sections, scroll animations, social proof
|
|
14
|
-
| Login | Working |
|
|
15
|
-
| Dashboard | Working | KPI cards,
|
|
16
|
-
| Projects CRUD | Working |
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
| Command Palette | Working | Cmd+K / Ctrl+K, searches pages and actions |
|
|
21
|
-
| Toast Notifications | Working | Success/error/info with progress bar, exit animation, max 3 stack |
|
|
22
|
-
| Protected Routes | Working | Automatic redirect for unauthenticated users |
|
|
23
|
-
| Color Themes | Working | 4 built-in presets (Blue, Purple, Green, Orange) via CSS variables |
|
|
13
|
+
| Landing Page | Working | 6 sections, scroll animations, social proof |
|
|
14
|
+
| Auth (Login) | Working | Email/Google via Privy, zero-config dev credentials |
|
|
15
|
+
| Dashboard | Working | KPI cards, checklist, activity feed |
|
|
16
|
+
| Projects/Tasks/Team CRUD | Working | Full create, read, update, delete with validation |
|
|
17
|
+
| Settings | Working | 4 tabs with backend persistence via DB Proxy |
|
|
18
|
+
| Command Palette | Working | Cmd+K / Ctrl+K |
|
|
19
|
+
| Color Themes | Working | 4 presets (Blue, Purple, Green, Orange) |
|
|
24
20
|
| Static Export | Working | `output: 'export'` for IPFS deployment |
|
|
25
|
-
| Mobile Nav | Working |
|
|
26
|
-
| CSV Export | Working | One-click export for tasks and projects |
|
|
27
|
-
| SEO | Working | OpenGraph, Twitter cards, robots.txt, sitemap template |
|
|
21
|
+
| Mobile Nav | Working | Responsive sidebar with hamburger menu |
|
|
28
22
|
|
|
29
23
|
## Known Issues
|
|
30
24
|
|
|
31
|
-
### 1.
|
|
32
|
-
|
|
25
|
+
### 1. Auth Uses Privy Directly (Abstraction Coming Post-Beta)
|
|
26
|
+
The template uses `usePrivy()` and `NEXT_PUBLIC_PRIVY_APP_ID`. The planned `useAuth()` hook and `NEXT_PUBLIC_VARITY_AUTH_ID` env var are post-beta tasks. No action required -- current auth works correctly.
|
|
33
27
|
|
|
34
|
-
### 2.
|
|
35
|
-
|
|
28
|
+
### 2. Payments Section is Placeholder
|
|
29
|
+
Settings > Billing shows mock UI. Credit card payment integration (on/off ramp) is coming soon. Wire your own billing provider (Stripe, etc.) if needed now.
|
|
36
30
|
|
|
37
|
-
### 3.
|
|
38
|
-
|
|
31
|
+
### 3. No Server-Side Rendering
|
|
32
|
+
All pages are statically exported. No SSR, API routes, or middleware. Do not use dynamic routes (`[id]` patterns) -- use client-side state for detail views instead.
|
|
39
33
|
|
|
40
|
-
### 4.
|
|
41
|
-
|
|
34
|
+
### 4. Navigation Flash
|
|
35
|
+
Brief "Initializing Dashboard" screen when navigating between pages. Caused by `PrivyReadyGate` re-checking auth state. Resolves in under 1 second.
|
|
42
36
|
|
|
43
|
-
### 5.
|
|
44
|
-
|
|
37
|
+
### 5. Team Email Invites Are Local Only
|
|
38
|
+
No SMTP integration. Team members are added to the database but no invitation email is sent. Integrate your own email service if needed.
|
|
45
39
|
|
|
46
|
-
### 6.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
### 7. Team Email Invites
|
|
50
|
-
No SMTP integration — team members are added to the database only. No invitation email is sent. Developers should integrate their own email service.
|
|
40
|
+
### 6. Sessions and Password Are Auth-Provider Managed
|
|
41
|
+
Settings > Security shows mock session data. Password changes and profile photos are managed by the auth provider (Privy), not the template.
|
|
51
42
|
|
|
52
43
|
## Environment
|
|
53
44
|
|
|
54
45
|
### Development (Zero Config)
|
|
55
46
|
```bash
|
|
56
|
-
npm install
|
|
57
|
-
npm run dev
|
|
47
|
+
npm install && npm run dev
|
|
58
48
|
```
|
|
59
|
-
No `.env` file, API keys, or accounts needed.
|
|
49
|
+
No `.env` file, API keys, or accounts needed.
|
|
60
50
|
|
|
61
51
|
### Production
|
|
62
52
|
```bash
|
|
63
53
|
varitykit app deploy
|
|
64
54
|
```
|
|
65
|
-
The CLI provisions a private database, injects production credentials, and deploys automatically.
|
|
66
55
|
|
|
67
56
|
## Reporting Issues
|
|
68
57
|
|
|
69
|
-
|
|
58
|
+
- GitHub: https://github.com/varity-labs/varity-sdk/issues
|
|
59
|
+
- Discord: https://discord.gg/7vWsdwa2Bg
|
package/template/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# TaskFlow — SaaS Starter Template
|
|
2
2
|
|
|
3
|
-
[](https://varity.so)
|
|
3
|
+
[](https://www.varity.so)
|
|
4
4
|
|
|
5
|
-
A full-featured project management app built with [Varity](https://varity.so). Everything works immediately — no configuration, no API keys, no setup.
|
|
5
|
+
A full-featured project management app built with [Varity](https://www.varity.so). Everything works immediately — no configuration, no API keys, no setup.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -60,7 +60,7 @@ Switch your entire app's color scheme by editing `src/app/globals.css`:
|
|
|
60
60
|
This template works immediately with **zero setup**:
|
|
61
61
|
|
|
62
62
|
### Instant Auth
|
|
63
|
-
- ✅ Email login (
|
|
63
|
+
- ✅ Email login (built-in)
|
|
64
64
|
- ✅ Google/Apple social login
|
|
65
65
|
- ✅ Dev credentials built-in
|
|
66
66
|
- ❌ No env vars needed
|
|
@@ -75,9 +75,9 @@ This template works immediately with **zero setup**:
|
|
|
75
75
|
```bash
|
|
76
76
|
npm run deploy
|
|
77
77
|
```
|
|
78
|
-
- ✅ Deploys to
|
|
78
|
+
- ✅ Deploys to production
|
|
79
79
|
- ✅ Auto-fetches credentials
|
|
80
|
-
- ❌ No
|
|
80
|
+
- ❌ No extra accounts needed
|
|
81
81
|
|
|
82
82
|
---
|
|
83
83
|
|
|
@@ -103,7 +103,7 @@ This template uses `workspace:^` protocol for Varity packages:
|
|
|
103
103
|
- ✅ `output: 'export'` in next.config.js
|
|
104
104
|
- ✅ All pages pre-rendered to static HTML
|
|
105
105
|
- ✅ No server-side dependencies
|
|
106
|
-
- ✅
|
|
106
|
+
- ✅ CDN deployable
|
|
107
107
|
|
|
108
108
|
### Type Safety
|
|
109
109
|
- ✅ TypeScript strict mode enabled
|
|
@@ -215,8 +215,7 @@ The database collection is created automatically on first use — no migrations
|
|
|
215
215
|
|
|
216
216
|
| Variable | Required | Notes |
|
|
217
217
|
|----------|----------|-------|
|
|
218
|
-
| `
|
|
219
|
-
| `NEXT_PUBLIC_THIRDWEB_CLIENT_ID` | No | Infrastructure (auto-configured) |
|
|
218
|
+
| `NEXT_PUBLIC_VARITY_AUTH_ID` | No | Auth provider (auto-configured) |
|
|
220
219
|
| `NEXT_PUBLIC_VARITY_APP_TOKEN` | No | Database token (auto-configured) |
|
|
221
220
|
| `NEXT_PUBLIC_VARITY_APP_ID` | No | App ID (auto-configured) |
|
|
222
221
|
|
|
@@ -113,8 +113,8 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
113
113
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
114
114
|
const privy = usePrivyHook ? usePrivyHook() : { user: null, logout: async () => {} };
|
|
115
115
|
const { user, logout } = privy;
|
|
116
|
-
const pathname = usePathname();
|
|
117
116
|
const router = useRouter();
|
|
117
|
+
const pathname = usePathname();
|
|
118
118
|
const isMobile = useIsMobile();
|
|
119
119
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
120
120
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
@@ -154,7 +154,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
154
154
|
|
|
155
155
|
const handleLogout = async () => {
|
|
156
156
|
await logout();
|
|
157
|
-
|
|
157
|
+
window.location.href = '/';
|
|
158
158
|
};
|
|
159
159
|
|
|
160
160
|
// Fallback layout when DashboardLayout from ui-kit isn't available
|
|
@@ -257,6 +257,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
257
257
|
name: userName,
|
|
258
258
|
address: userEmail,
|
|
259
259
|
}}
|
|
260
|
+
onNavigate={(path: string) => router.push(path)}
|
|
260
261
|
onLogout={handleLogout}
|
|
261
262
|
onNavigateToProfile={() => router.push('/dashboard/settings')}
|
|
262
263
|
onNavigateToSettings={() => router.push('/dashboard/settings')}
|
|
@@ -288,7 +289,7 @@ export default function DashboardRootLayout({
|
|
|
288
289
|
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
|
|
289
290
|
thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
|
|
290
291
|
loginMethods={['email', 'google']}
|
|
291
|
-
appearance={{ theme: 'light', accentColor: '#2563EB'
|
|
292
|
+
appearance={{ theme: 'light', accentColor: '#2563EB' }}
|
|
292
293
|
>
|
|
293
294
|
<PrivyProtectedRoute fallback={<RedirectToLogin />}>
|
|
294
295
|
<DashboardShell>{children}</DashboardShell>
|
|
@@ -9,7 +9,6 @@ import { FolderKanban, ListTodo, Users, ArrowRight } from 'lucide-react';
|
|
|
9
9
|
|
|
10
10
|
function QuickActions() {
|
|
11
11
|
const router = useRouter();
|
|
12
|
-
|
|
13
12
|
const actions = [
|
|
14
13
|
{
|
|
15
14
|
label: 'New Project',
|
|
@@ -138,7 +137,6 @@ function GettingStarted({
|
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
export default function DashboardPage() {
|
|
141
|
-
const router = useRouter();
|
|
142
140
|
const { name } = useCurrentUser();
|
|
143
141
|
const { data: projects, loading: projectsLoading, error: projectsError, refresh: refreshProjects } = useProjects();
|
|
144
142
|
const { data: tasks, loading: tasksLoading, error: tasksError, refresh: refreshTasks } = useTasks();
|
|
@@ -163,14 +161,14 @@ export default function DashboardPage() {
|
|
|
163
161
|
</p>
|
|
164
162
|
</div>
|
|
165
163
|
|
|
166
|
-
{error && (
|
|
167
|
-
<div className="flex items-center justify-between rounded-lg border border-
|
|
168
|
-
<p className="text-sm text-
|
|
164
|
+
{error && !loading && (
|
|
165
|
+
<div className="flex items-center justify-between rounded-lg border border-amber-200 bg-amber-50 px-4 py-3">
|
|
166
|
+
<p className="text-sm text-amber-700">Syncing your data...</p>
|
|
169
167
|
<button
|
|
170
168
|
onClick={() => { refreshProjects(); refreshTasks(); refreshTeam(); }}
|
|
171
|
-
className="text-sm font-medium text-
|
|
169
|
+
className="text-sm font-medium text-amber-700 hover:text-amber-800 underline"
|
|
172
170
|
>
|
|
173
|
-
|
|
171
|
+
Refresh
|
|
174
172
|
</button>
|
|
175
173
|
</div>
|
|
176
174
|
)}
|
|
@@ -606,10 +606,10 @@ export default function ProjectsPage() {
|
|
|
606
606
|
</div>
|
|
607
607
|
</Dialog>
|
|
608
608
|
|
|
609
|
-
{error && (
|
|
610
|
-
<div className="flex items-center justify-between rounded-lg border border-
|
|
611
|
-
<p className="text-sm text-
|
|
612
|
-
<button onClick={refresh} className="text-sm font-medium text-
|
|
609
|
+
{error && !loading && (
|
|
610
|
+
<div className="flex items-center justify-between rounded-lg border border-amber-200 bg-amber-50 px-4 py-3">
|
|
611
|
+
<p className="text-sm text-amber-700">Syncing your projects...</p>
|
|
612
|
+
<button onClick={refresh} className="text-sm font-medium text-amber-700 hover:text-amber-800 underline">Refresh</button>
|
|
613
613
|
</div>
|
|
614
614
|
)}
|
|
615
615
|
|
|
@@ -258,10 +258,10 @@ export default function TasksPage() {
|
|
|
258
258
|
)}
|
|
259
259
|
</div>
|
|
260
260
|
|
|
261
|
-
{error && (
|
|
262
|
-
<div className="flex items-center justify-between rounded-lg border border-
|
|
263
|
-
<p className="text-sm text-
|
|
264
|
-
<button onClick={refresh} className="text-sm font-medium text-
|
|
261
|
+
{error && !loading && (
|
|
262
|
+
<div className="flex items-center justify-between rounded-lg border border-amber-200 bg-amber-50 px-4 py-3">
|
|
263
|
+
<p className="text-sm text-amber-700">Syncing your tasks...</p>
|
|
264
|
+
<button onClick={refresh} className="text-sm font-medium text-amber-700 hover:text-amber-800 underline">Refresh</button>
|
|
265
265
|
</div>
|
|
266
266
|
)}
|
|
267
267
|
|
|
@@ -246,10 +246,10 @@ export default function TeamPage() {
|
|
|
246
246
|
loading={removeSubmitting}
|
|
247
247
|
/>
|
|
248
248
|
|
|
249
|
-
{error && (
|
|
250
|
-
<div className="flex items-center justify-between rounded-lg border border-
|
|
251
|
-
<p className="text-sm text-
|
|
252
|
-
<button onClick={refresh} className="text-sm font-medium text-
|
|
249
|
+
{error && !loading && (
|
|
250
|
+
<div className="flex items-center justify-between rounded-lg border border-amber-200 bg-amber-50 px-4 py-3">
|
|
251
|
+
<p className="text-sm text-amber-700">Syncing your team data...</p>
|
|
252
|
+
<button onClick={refresh} className="text-sm font-medium text-amber-700 hover:text-amber-800 underline">Refresh</button>
|
|
253
253
|
</div>
|
|
254
254
|
)}
|
|
255
255
|
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import { Providers } from '@/components/providers';
|
|
3
|
+
import { APP_NAME } from '@/lib/constants';
|
|
3
4
|
import './globals.css';
|
|
4
5
|
|
|
6
|
+
const description = 'Built with Varity — auth, database, and deployment included.';
|
|
7
|
+
|
|
5
8
|
export const metadata: Metadata = {
|
|
6
|
-
title:
|
|
7
|
-
description
|
|
8
|
-
metadataBase: new URL('https://
|
|
9
|
+
title: APP_NAME,
|
|
10
|
+
description,
|
|
11
|
+
metadataBase: new URL('https://varity.app'),
|
|
9
12
|
openGraph: {
|
|
10
|
-
title:
|
|
11
|
-
description
|
|
13
|
+
title: APP_NAME,
|
|
14
|
+
description,
|
|
12
15
|
type: 'website',
|
|
13
16
|
},
|
|
14
17
|
twitter: {
|
|
15
18
|
card: 'summary',
|
|
16
|
-
title:
|
|
17
|
-
description
|
|
19
|
+
title: APP_NAME,
|
|
20
|
+
description,
|
|
18
21
|
},
|
|
19
22
|
};
|
|
20
23
|
|
|
@@ -15,6 +15,12 @@ try {
|
|
|
15
15
|
usePrivyHook = uiKit.usePrivy;
|
|
16
16
|
} catch {}
|
|
17
17
|
|
|
18
|
+
function loginButtonLabel(privy: { ready: boolean; authenticated: boolean }): string {
|
|
19
|
+
if (!privy.ready) return 'Loading...';
|
|
20
|
+
if (privy.authenticated) return 'Already Signed In';
|
|
21
|
+
return 'Sign In with Email or Social';
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
function LoginContent() {
|
|
19
25
|
const router = useRouter();
|
|
20
26
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
@@ -55,11 +61,7 @@ function LoginContent() {
|
|
|
55
61
|
disabled={!privy.ready || privy.authenticated}
|
|
56
62
|
className="w-full px-6 py-3 bg-primary-600 hover:bg-primary-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors shadow-sm"
|
|
57
63
|
>
|
|
58
|
-
{
|
|
59
|
-
? 'Loading...'
|
|
60
|
-
: privy.authenticated
|
|
61
|
-
? 'Already Signed In'
|
|
62
|
-
: 'Sign In with Email or Social'}
|
|
64
|
+
{loginButtonLabel(privy)}
|
|
63
65
|
</button>
|
|
64
66
|
) : (
|
|
65
67
|
<div className="text-center space-y-4">
|
|
@@ -79,20 +81,18 @@ function LoginContent() {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export default function LoginPage() {
|
|
82
|
-
// Always wrap in PrivyStack - it uses dev credentials automatically when no appId is provided
|
|
83
84
|
if (PrivyStackComponent) {
|
|
84
85
|
return (
|
|
85
86
|
<PrivyStackComponent
|
|
86
87
|
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
|
|
87
88
|
thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
|
|
88
89
|
loginMethods={['email', 'google']}
|
|
89
|
-
appearance={{ theme: 'light', accentColor: '#2563EB'
|
|
90
|
+
appearance={{ theme: 'light', accentColor: '#2563EB' }}
|
|
90
91
|
>
|
|
91
92
|
<LoginContent />
|
|
92
93
|
</PrivyStackComponent>
|
|
93
94
|
);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
// Fallback if ui-kit package isn't installed
|
|
97
97
|
return <LoginContent />;
|
|
98
98
|
}
|
|
@@ -2,7 +2,6 @@ import { Navbar } from '@/components/shared/Navbar';
|
|
|
2
2
|
import { Hero } from '@/components/landing/Hero';
|
|
3
3
|
import { Features } from '@/components/landing/Features';
|
|
4
4
|
import { HowItWorks } from '@/components/landing/HowItWorks';
|
|
5
|
-
import { Testimonials } from '@/components/landing/Testimonials';
|
|
6
5
|
import { Pricing } from '@/components/landing/Pricing';
|
|
7
6
|
import { CTA } from '@/components/landing/CTA';
|
|
8
7
|
import { Footer } from '@/components/shared/Footer';
|
|
@@ -14,7 +13,7 @@ export default function HomePage() {
|
|
|
14
13
|
<Hero />
|
|
15
14
|
<Features />
|
|
16
15
|
<HowItWorks />
|
|
17
|
-
|
|
16
|
+
|
|
18
17
|
<Pricing />
|
|
19
18
|
<CTA />
|
|
20
19
|
<Footer />
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { DataTable } from '@varity-labs/ui-kit';
|
|
5
|
-
import { TaskStatusBadge, PriorityBadge } from '@varity-labs/ui-kit';
|
|
4
|
+
import { DataTable, TaskStatusBadge, PriorityBadge } from '@varity-labs/ui-kit';
|
|
6
5
|
import { formatRelativeDate } from '@/lib/utils';
|
|
7
6
|
import { ArrowRight } from 'lucide-react';
|
|
8
7
|
import type { Task } from '@/types';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
1
2
|
import { Check } from 'lucide-react';
|
|
2
3
|
|
|
3
4
|
const plans = [
|
|
@@ -52,6 +53,12 @@ const plans = [
|
|
|
52
53
|
},
|
|
53
54
|
];
|
|
54
55
|
|
|
56
|
+
function buttonStyle(plan: (typeof plans)[number]): string {
|
|
57
|
+
if (plan.popular) return 'bg-primary-600 text-white hover:bg-primary-700';
|
|
58
|
+
if (plan.price === 0) return 'bg-gray-100 text-gray-900 hover:bg-gray-200';
|
|
59
|
+
return 'bg-gray-900 text-white hover:bg-gray-800';
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
export function Pricing() {
|
|
56
63
|
return (
|
|
57
64
|
<section id="pricing" className="py-24 bg-white">
|
|
@@ -102,18 +109,12 @@ export function Pricing() {
|
|
|
102
109
|
</ul>
|
|
103
110
|
|
|
104
111
|
<div className="mt-8">
|
|
105
|
-
<
|
|
112
|
+
<Link
|
|
106
113
|
href="/login"
|
|
107
|
-
className={`block w-full rounded-lg py-3 text-center font-medium transition-colors ${
|
|
108
|
-
plan.popular
|
|
109
|
-
? 'bg-primary-600 text-white hover:bg-primary-700'
|
|
110
|
-
: plan.price === 0
|
|
111
|
-
? 'bg-gray-100 text-gray-900 hover:bg-gray-200'
|
|
112
|
-
: 'bg-gray-900 text-white hover:bg-gray-800'
|
|
113
|
-
}`}
|
|
114
|
+
className={`block w-full rounded-lg py-3 text-center font-medium transition-colors ${buttonStyle(plan)}`}
|
|
114
115
|
>
|
|
115
116
|
{plan.cta}
|
|
116
|
-
</
|
|
117
|
+
</Link>
|
|
117
118
|
</div>
|
|
118
119
|
</div>
|
|
119
120
|
))}
|
|
@@ -31,7 +31,7 @@ export function Footer() {
|
|
|
31
31
|
<div className="mt-8 border-t border-gray-100 pt-6 text-center text-sm text-gray-400">
|
|
32
32
|
© {year} {APP_NAME}. Built with{' '}
|
|
33
33
|
<a
|
|
34
|
-
href="https://varity.so"
|
|
34
|
+
href="https://www.varity.so"
|
|
35
35
|
target="_blank"
|
|
36
36
|
rel="noopener noreferrer"
|
|
37
37
|
className="inline-flex items-center gap-1 text-primary-500 hover:text-primary-600 transition-colors"
|
|
@@ -46,7 +46,6 @@ export function Navbar() {
|
|
|
46
46
|
>
|
|
47
47
|
Get Started
|
|
48
48
|
</Link>
|
|
49
|
-
{/* Mobile menu toggle */}
|
|
50
49
|
<button
|
|
51
50
|
onClick={() => setMobileOpen(!mobileOpen)}
|
|
52
51
|
className="rounded-lg p-2 text-gray-600 hover:bg-gray-100 sm:hidden"
|
|
@@ -58,7 +57,6 @@ export function Navbar() {
|
|
|
58
57
|
</div>
|
|
59
58
|
</div>
|
|
60
59
|
|
|
61
|
-
{/* Mobile dropdown */}
|
|
62
60
|
{mobileOpen && (
|
|
63
61
|
<div className="border-t border-gray-100 bg-white px-4 pb-4 pt-2 sm:hidden">
|
|
64
62
|
<div className="space-y-1">
|
|
@@ -10,6 +10,18 @@ try {
|
|
|
10
10
|
usePrivyHook = uiKit.usePrivy;
|
|
11
11
|
} catch {}
|
|
12
12
|
|
|
13
|
+
async function fetchWithRetry<T>(fn: () => Promise<T>, retries = 3, delay = 1500): Promise<T> {
|
|
14
|
+
for (let i = 0; i < retries; i++) {
|
|
15
|
+
try {
|
|
16
|
+
return await fn();
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (i === retries - 1) throw err;
|
|
19
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
throw new Error('Unexpected');
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
export function useCurrentUser() {
|
|
14
26
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
15
27
|
const privy = usePrivyHook ? usePrivyHook() : { user: null, authenticated: false, logout: async () => {} };
|
|
@@ -54,7 +66,7 @@ export function useProjects(): UseCollectionReturn<Project> {
|
|
|
54
66
|
try {
|
|
55
67
|
setLoading(true);
|
|
56
68
|
setError(null);
|
|
57
|
-
const result = await projects().get();
|
|
69
|
+
const result = await fetchWithRetry(() => projects().get());
|
|
58
70
|
setData(result as Project[]);
|
|
59
71
|
} catch (err) {
|
|
60
72
|
setError(err instanceof Error ? err.message : 'Failed to load projects');
|
|
@@ -126,7 +138,7 @@ export function useTasks(projectId?: string): UseCollectionReturn<Task> {
|
|
|
126
138
|
try {
|
|
127
139
|
setLoading(true);
|
|
128
140
|
setError(null);
|
|
129
|
-
const result = await tasks().get();
|
|
141
|
+
const result = await fetchWithRetry(() => tasks().get());
|
|
130
142
|
setAllTasks(result as Task[]);
|
|
131
143
|
} catch (err) {
|
|
132
144
|
setError(err instanceof Error ? err.message : 'Failed to load tasks');
|
|
@@ -202,7 +214,7 @@ export function useTeam(): UseCollectionReturn<TeamMember> {
|
|
|
202
214
|
try {
|
|
203
215
|
setLoading(true);
|
|
204
216
|
setError(null);
|
|
205
|
-
const result = await teamMembers().get();
|
|
217
|
+
const result = await fetchWithRetry(() => teamMembers().get());
|
|
206
218
|
setData(result as TeamMember[]);
|
|
207
219
|
} catch (err) {
|
|
208
220
|
setError(err instanceof Error ? err.message : 'Failed to load team');
|
|
@@ -289,7 +301,7 @@ export function useUserSettings() {
|
|
|
289
301
|
try {
|
|
290
302
|
setLoading(true);
|
|
291
303
|
setError(null);
|
|
292
|
-
const all = await userSettings().get();
|
|
304
|
+
const all = await fetchWithRetry(() => userSettings().get());
|
|
293
305
|
const mine = (all as UserSettings[]).find((s) => s.user_id === userId);
|
|
294
306
|
if (mine) {
|
|
295
307
|
setSettings(mine);
|
|
@@ -1,589 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dashboard Service
|
|
3
|
-
*
|
|
4
|
-
* Centralized service for dashboard-related API calls.
|
|
5
|
-
* Provides type-safe interfaces and error handling.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { Project, Task, TeamMember } from '@/types';
|
|
9
|
-
|
|
10
|
-
// ============================================================================
|
|
11
|
-
// Type Definitions
|
|
12
|
-
// ============================================================================
|
|
13
|
-
|
|
14
|
-
export interface KPIMetric {
|
|
15
|
-
title: string;
|
|
16
|
-
value: string | number;
|
|
17
|
-
change?: {
|
|
18
|
-
value: number;
|
|
19
|
-
period: string;
|
|
20
|
-
};
|
|
21
|
-
trend?: 'up' | 'down' | 'neutral';
|
|
22
|
-
sparklineData?: number[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface KPIResponse {
|
|
26
|
-
kpis: KPIMetric[];
|
|
27
|
-
has_data: boolean;
|
|
28
|
-
last_updated: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface Activity {
|
|
32
|
-
id: string;
|
|
33
|
-
type: 'project_created' | 'task_completed' | 'member_added' | 'comment_added';
|
|
34
|
-
title: string;
|
|
35
|
-
description: string;
|
|
36
|
-
timestamp: string;
|
|
37
|
-
user?: {
|
|
38
|
-
name: string;
|
|
39
|
-
avatar?: string;
|
|
40
|
-
};
|
|
41
|
-
metadata?: Record<string, any>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ActivityResponse {
|
|
45
|
-
activities: Activity[];
|
|
46
|
-
total_count: number;
|
|
47
|
-
has_more: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface ProjectResponse {
|
|
51
|
-
projects: Project[];
|
|
52
|
-
total_count: number;
|
|
53
|
-
active_count: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface TaskResponse {
|
|
57
|
-
tasks: Task[];
|
|
58
|
-
total_count: number;
|
|
59
|
-
completed_count: number;
|
|
60
|
-
by_status: {
|
|
61
|
-
todo: number;
|
|
62
|
-
in_progress: number;
|
|
63
|
-
done: number;
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface TeamMemberResponse {
|
|
68
|
-
members: TeamMember[];
|
|
69
|
-
total_count: number;
|
|
70
|
-
roles: {
|
|
71
|
-
owner: number;
|
|
72
|
-
admin: number;
|
|
73
|
-
member: number;
|
|
74
|
-
viewer: number;
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface DashboardOverviewResponse {
|
|
79
|
-
kpis: KPIResponse;
|
|
80
|
-
recent_activity: Activity[];
|
|
81
|
-
projects_summary: {
|
|
82
|
-
active: number;
|
|
83
|
-
total: number;
|
|
84
|
-
recent: Project[];
|
|
85
|
-
};
|
|
86
|
-
tasks_summary: {
|
|
87
|
-
open: number;
|
|
88
|
-
completed: number;
|
|
89
|
-
completion_rate: number;
|
|
90
|
-
};
|
|
91
|
-
team_summary: {
|
|
92
|
-
total: number;
|
|
93
|
-
active: number;
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ============================================================================
|
|
98
|
-
// Error Handling
|
|
99
|
-
// ============================================================================
|
|
100
|
-
|
|
101
|
-
export class DashboardServiceError extends Error {
|
|
102
|
-
constructor(
|
|
103
|
-
message: string,
|
|
104
|
-
public statusCode?: number,
|
|
105
|
-
public originalError?: Error
|
|
106
|
-
) {
|
|
107
|
-
super(message);
|
|
108
|
-
this.name = 'DashboardServiceError';
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function handleResponse<T>(response: Response): Promise<T> {
|
|
113
|
-
if (!response.ok) {
|
|
114
|
-
const errorText = await response.text().catch(() => 'Unknown error');
|
|
115
|
-
throw new DashboardServiceError(
|
|
116
|
-
`API request failed: ${response.statusText}`,
|
|
117
|
-
response.status,
|
|
118
|
-
new Error(errorText)
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
return await response.json();
|
|
124
|
-
} catch (error) {
|
|
125
|
-
throw new DashboardServiceError(
|
|
126
|
-
'Failed to parse API response',
|
|
127
|
-
response.status,
|
|
128
|
-
error instanceof Error ? error : undefined
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ============================================================================
|
|
134
|
-
// API Client
|
|
135
|
-
// ============================================================================
|
|
136
|
-
|
|
137
|
-
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api';
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Fetch dashboard KPIs
|
|
141
|
-
*/
|
|
142
|
-
export async function getKPIs(userId: string): Promise<KPIResponse> {
|
|
143
|
-
try {
|
|
144
|
-
const response = await fetch(
|
|
145
|
-
`${API_BASE_URL}/dashboard/kpis?userId=${encodeURIComponent(userId)}`,
|
|
146
|
-
{
|
|
147
|
-
method: 'GET',
|
|
148
|
-
headers: {
|
|
149
|
-
'Content-Type': 'application/json',
|
|
150
|
-
},
|
|
151
|
-
cache: 'no-store', // Always fetch fresh data
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
return await handleResponse<KPIResponse>(response);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
if (error instanceof DashboardServiceError) {
|
|
158
|
-
throw error;
|
|
159
|
-
}
|
|
160
|
-
throw new DashboardServiceError(
|
|
161
|
-
'Failed to fetch KPIs',
|
|
162
|
-
undefined,
|
|
163
|
-
error instanceof Error ? error : undefined
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Fetch recent activity
|
|
170
|
-
*/
|
|
171
|
-
export async function getRecentActivity(
|
|
172
|
-
userId: string,
|
|
173
|
-
limit: number = 10
|
|
174
|
-
): Promise<Activity[]> {
|
|
175
|
-
try {
|
|
176
|
-
const response = await fetch(
|
|
177
|
-
`${API_BASE_URL}/dashboard/activity?userId=${encodeURIComponent(userId)}&limit=${limit}`,
|
|
178
|
-
{
|
|
179
|
-
method: 'GET',
|
|
180
|
-
headers: {
|
|
181
|
-
'Content-Type': 'application/json',
|
|
182
|
-
},
|
|
183
|
-
cache: 'no-store',
|
|
184
|
-
}
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const data = await handleResponse<ActivityResponse>(response);
|
|
188
|
-
return data.activities;
|
|
189
|
-
} catch (error) {
|
|
190
|
-
if (error instanceof DashboardServiceError) {
|
|
191
|
-
throw error;
|
|
192
|
-
}
|
|
193
|
-
throw new DashboardServiceError(
|
|
194
|
-
'Failed to fetch recent activity',
|
|
195
|
-
undefined,
|
|
196
|
-
error instanceof Error ? error : undefined
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Fetch all projects for a user
|
|
203
|
-
*/
|
|
204
|
-
export async function getProjects(userId: string): Promise<Project[]> {
|
|
205
|
-
try {
|
|
206
|
-
const response = await fetch(
|
|
207
|
-
`${API_BASE_URL}/projects?userId=${encodeURIComponent(userId)}`,
|
|
208
|
-
{
|
|
209
|
-
method: 'GET',
|
|
210
|
-
headers: {
|
|
211
|
-
'Content-Type': 'application/json',
|
|
212
|
-
},
|
|
213
|
-
cache: 'no-store',
|
|
214
|
-
}
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
const data = await handleResponse<ProjectResponse>(response);
|
|
218
|
-
return data.projects;
|
|
219
|
-
} catch (error) {
|
|
220
|
-
if (error instanceof DashboardServiceError) {
|
|
221
|
-
throw error;
|
|
222
|
-
}
|
|
223
|
-
throw new DashboardServiceError(
|
|
224
|
-
'Failed to fetch projects',
|
|
225
|
-
undefined,
|
|
226
|
-
error instanceof Error ? error : undefined
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Fetch tasks, optionally filtered by project
|
|
233
|
-
*/
|
|
234
|
-
export async function getTasks(
|
|
235
|
-
userId: string,
|
|
236
|
-
projectId?: string
|
|
237
|
-
): Promise<Task[]> {
|
|
238
|
-
try {
|
|
239
|
-
const url = projectId
|
|
240
|
-
? `${API_BASE_URL}/tasks?userId=${encodeURIComponent(userId)}&projectId=${encodeURIComponent(projectId)}`
|
|
241
|
-
: `${API_BASE_URL}/tasks?userId=${encodeURIComponent(userId)}`;
|
|
242
|
-
|
|
243
|
-
const response = await fetch(url, {
|
|
244
|
-
method: 'GET',
|
|
245
|
-
headers: {
|
|
246
|
-
'Content-Type': 'application/json',
|
|
247
|
-
},
|
|
248
|
-
cache: 'no-store',
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const data = await handleResponse<TaskResponse>(response);
|
|
252
|
-
return data.tasks;
|
|
253
|
-
} catch (error) {
|
|
254
|
-
if (error instanceof DashboardServiceError) {
|
|
255
|
-
throw error;
|
|
256
|
-
}
|
|
257
|
-
throw new DashboardServiceError(
|
|
258
|
-
'Failed to fetch tasks',
|
|
259
|
-
undefined,
|
|
260
|
-
error instanceof Error ? error : undefined
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Fetch team members
|
|
267
|
-
*/
|
|
268
|
-
export async function getTeamMembers(userId: string): Promise<TeamMember[]> {
|
|
269
|
-
try {
|
|
270
|
-
const response = await fetch(
|
|
271
|
-
`${API_BASE_URL}/team?userId=${encodeURIComponent(userId)}`,
|
|
272
|
-
{
|
|
273
|
-
method: 'GET',
|
|
274
|
-
headers: {
|
|
275
|
-
'Content-Type': 'application/json',
|
|
276
|
-
},
|
|
277
|
-
cache: 'no-store',
|
|
278
|
-
}
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
const data = await handleResponse<TeamMemberResponse>(response);
|
|
282
|
-
return data.members;
|
|
283
|
-
} catch (error) {
|
|
284
|
-
if (error instanceof DashboardServiceError) {
|
|
285
|
-
throw error;
|
|
286
|
-
}
|
|
287
|
-
throw new DashboardServiceError(
|
|
288
|
-
'Failed to fetch team members',
|
|
289
|
-
undefined,
|
|
290
|
-
error instanceof Error ? error : undefined
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Fetch complete dashboard overview
|
|
297
|
-
*/
|
|
298
|
-
export async function getDashboardOverview(
|
|
299
|
-
userId: string
|
|
300
|
-
): Promise<DashboardOverviewResponse> {
|
|
301
|
-
try {
|
|
302
|
-
const response = await fetch(
|
|
303
|
-
`${API_BASE_URL}/dashboard/overview?userId=${encodeURIComponent(userId)}`,
|
|
304
|
-
{
|
|
305
|
-
method: 'GET',
|
|
306
|
-
headers: {
|
|
307
|
-
'Content-Type': 'application/json',
|
|
308
|
-
},
|
|
309
|
-
cache: 'no-store',
|
|
310
|
-
}
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
return await handleResponse<DashboardOverviewResponse>(response);
|
|
314
|
-
} catch (error) {
|
|
315
|
-
if (error instanceof DashboardServiceError) {
|
|
316
|
-
throw error;
|
|
317
|
-
}
|
|
318
|
-
throw new DashboardServiceError(
|
|
319
|
-
'Failed to fetch dashboard overview',
|
|
320
|
-
undefined,
|
|
321
|
-
error instanceof Error ? error : undefined
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Create a new project
|
|
328
|
-
*/
|
|
329
|
-
export async function createProject(
|
|
330
|
-
userId: string,
|
|
331
|
-
data: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>
|
|
332
|
-
): Promise<Project> {
|
|
333
|
-
try {
|
|
334
|
-
const response = await fetch(`${API_BASE_URL}/projects`, {
|
|
335
|
-
method: 'POST',
|
|
336
|
-
headers: {
|
|
337
|
-
'Content-Type': 'application/json',
|
|
338
|
-
},
|
|
339
|
-
body: JSON.stringify({ userId, ...data }),
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
return await handleResponse<Project>(response);
|
|
343
|
-
} catch (error) {
|
|
344
|
-
if (error instanceof DashboardServiceError) {
|
|
345
|
-
throw error;
|
|
346
|
-
}
|
|
347
|
-
throw new DashboardServiceError(
|
|
348
|
-
'Failed to create project',
|
|
349
|
-
undefined,
|
|
350
|
-
error instanceof Error ? error : undefined
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Update an existing project
|
|
357
|
-
*/
|
|
358
|
-
export async function updateProject(
|
|
359
|
-
projectId: string,
|
|
360
|
-
data: Partial<Project>
|
|
361
|
-
): Promise<Project> {
|
|
362
|
-
try {
|
|
363
|
-
const response = await fetch(`${API_BASE_URL}/projects/${projectId}`, {
|
|
364
|
-
method: 'PATCH',
|
|
365
|
-
headers: {
|
|
366
|
-
'Content-Type': 'application/json',
|
|
367
|
-
},
|
|
368
|
-
body: JSON.stringify(data),
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
return await handleResponse<Project>(response);
|
|
372
|
-
} catch (error) {
|
|
373
|
-
if (error instanceof DashboardServiceError) {
|
|
374
|
-
throw error;
|
|
375
|
-
}
|
|
376
|
-
throw new DashboardServiceError(
|
|
377
|
-
'Failed to update project',
|
|
378
|
-
undefined,
|
|
379
|
-
error instanceof Error ? error : undefined
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Delete a project
|
|
386
|
-
*/
|
|
387
|
-
export async function deleteProject(projectId: string): Promise<void> {
|
|
388
|
-
try {
|
|
389
|
-
const response = await fetch(`${API_BASE_URL}/projects/${projectId}`, {
|
|
390
|
-
method: 'DELETE',
|
|
391
|
-
headers: {
|
|
392
|
-
'Content-Type': 'application/json',
|
|
393
|
-
},
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
if (!response.ok) {
|
|
397
|
-
throw new DashboardServiceError(
|
|
398
|
-
`Failed to delete project: ${response.statusText}`,
|
|
399
|
-
response.status
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
} catch (error) {
|
|
403
|
-
if (error instanceof DashboardServiceError) {
|
|
404
|
-
throw error;
|
|
405
|
-
}
|
|
406
|
-
throw new DashboardServiceError(
|
|
407
|
-
'Failed to delete project',
|
|
408
|
-
undefined,
|
|
409
|
-
error instanceof Error ? error : undefined
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Create a new task
|
|
416
|
-
*/
|
|
417
|
-
export async function createTask(
|
|
418
|
-
userId: string,
|
|
419
|
-
data: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>
|
|
420
|
-
): Promise<Task> {
|
|
421
|
-
try {
|
|
422
|
-
const response = await fetch(`${API_BASE_URL}/tasks`, {
|
|
423
|
-
method: 'POST',
|
|
424
|
-
headers: {
|
|
425
|
-
'Content-Type': 'application/json',
|
|
426
|
-
},
|
|
427
|
-
body: JSON.stringify({ userId, ...data }),
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
return await handleResponse<Task>(response);
|
|
431
|
-
} catch (error) {
|
|
432
|
-
if (error instanceof DashboardServiceError) {
|
|
433
|
-
throw error;
|
|
434
|
-
}
|
|
435
|
-
throw new DashboardServiceError(
|
|
436
|
-
'Failed to create task',
|
|
437
|
-
undefined,
|
|
438
|
-
error instanceof Error ? error : undefined
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Update an existing task
|
|
445
|
-
*/
|
|
446
|
-
export async function updateTask(
|
|
447
|
-
taskId: string,
|
|
448
|
-
data: Partial<Task>
|
|
449
|
-
): Promise<Task> {
|
|
450
|
-
try {
|
|
451
|
-
const response = await fetch(`${API_BASE_URL}/tasks/${taskId}`, {
|
|
452
|
-
method: 'PATCH',
|
|
453
|
-
headers: {
|
|
454
|
-
'Content-Type': 'application/json',
|
|
455
|
-
},
|
|
456
|
-
body: JSON.stringify(data),
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
return await handleResponse<Task>(response);
|
|
460
|
-
} catch (error) {
|
|
461
|
-
if (error instanceof DashboardServiceError) {
|
|
462
|
-
throw error;
|
|
463
|
-
}
|
|
464
|
-
throw new DashboardServiceError(
|
|
465
|
-
'Failed to update task',
|
|
466
|
-
undefined,
|
|
467
|
-
error instanceof Error ? error : undefined
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Delete a task
|
|
474
|
-
*/
|
|
475
|
-
export async function deleteTask(taskId: string): Promise<void> {
|
|
476
|
-
try {
|
|
477
|
-
const response = await fetch(`${API_BASE_URL}/tasks/${taskId}`, {
|
|
478
|
-
method: 'DELETE',
|
|
479
|
-
headers: {
|
|
480
|
-
'Content-Type': 'application/json',
|
|
481
|
-
},
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
if (!response.ok) {
|
|
485
|
-
throw new DashboardServiceError(
|
|
486
|
-
`Failed to delete task: ${response.statusText}`,
|
|
487
|
-
response.status
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
} catch (error) {
|
|
491
|
-
if (error instanceof DashboardServiceError) {
|
|
492
|
-
throw error;
|
|
493
|
-
}
|
|
494
|
-
throw new DashboardServiceError(
|
|
495
|
-
'Failed to delete task',
|
|
496
|
-
undefined,
|
|
497
|
-
error instanceof Error ? error : undefined
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Invite a team member
|
|
504
|
-
*/
|
|
505
|
-
export async function inviteTeamMember(
|
|
506
|
-
userId: string,
|
|
507
|
-
email: string,
|
|
508
|
-
role: TeamMember['role']
|
|
509
|
-
): Promise<TeamMember> {
|
|
510
|
-
try {
|
|
511
|
-
const response = await fetch(`${API_BASE_URL}/team/invite`, {
|
|
512
|
-
method: 'POST',
|
|
513
|
-
headers: {
|
|
514
|
-
'Content-Type': 'application/json',
|
|
515
|
-
},
|
|
516
|
-
body: JSON.stringify({ userId, email, role }),
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
return await handleResponse<TeamMember>(response);
|
|
520
|
-
} catch (error) {
|
|
521
|
-
if (error instanceof DashboardServiceError) {
|
|
522
|
-
throw error;
|
|
523
|
-
}
|
|
524
|
-
throw new DashboardServiceError(
|
|
525
|
-
'Failed to invite team member',
|
|
526
|
-
undefined,
|
|
527
|
-
error instanceof Error ? error : undefined
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Remove a team member
|
|
534
|
-
*/
|
|
535
|
-
export async function removeTeamMember(memberId: string): Promise<void> {
|
|
536
|
-
try {
|
|
537
|
-
const response = await fetch(`${API_BASE_URL}/team/${memberId}`, {
|
|
538
|
-
method: 'DELETE',
|
|
539
|
-
headers: {
|
|
540
|
-
'Content-Type': 'application/json',
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
if (!response.ok) {
|
|
545
|
-
throw new DashboardServiceError(
|
|
546
|
-
`Failed to remove team member: ${response.statusText}`,
|
|
547
|
-
response.status
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
} catch (error) {
|
|
551
|
-
if (error instanceof DashboardServiceError) {
|
|
552
|
-
throw error;
|
|
553
|
-
}
|
|
554
|
-
throw new DashboardServiceError(
|
|
555
|
-
'Failed to remove team member',
|
|
556
|
-
undefined,
|
|
557
|
-
error instanceof Error ? error : undefined
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Update team member role
|
|
564
|
-
*/
|
|
565
|
-
export async function updateTeamMemberRole(
|
|
566
|
-
memberId: string,
|
|
567
|
-
role: TeamMember['role']
|
|
568
|
-
): Promise<TeamMember> {
|
|
569
|
-
try {
|
|
570
|
-
const response = await fetch(`${API_BASE_URL}/team/${memberId}`, {
|
|
571
|
-
method: 'PATCH',
|
|
572
|
-
headers: {
|
|
573
|
-
'Content-Type': 'application/json',
|
|
574
|
-
},
|
|
575
|
-
body: JSON.stringify({ role }),
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
return await handleResponse<TeamMember>(response);
|
|
579
|
-
} catch (error) {
|
|
580
|
-
if (error instanceof DashboardServiceError) {
|
|
581
|
-
throw error;
|
|
582
|
-
}
|
|
583
|
-
throw new DashboardServiceError(
|
|
584
|
-
'Failed to update team member role',
|
|
585
|
-
undefined,
|
|
586
|
-
error instanceof Error ? error : undefined
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
}
|