create-varity-app 2.0.0-beta.1 → 2.0.0-beta.3
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/dist/create.js +1 -1
- package/package.json +1 -1
- package/template/.env.example +3 -3
- package/template/.turbo/turbo-build.log +0 -0
- package/template/KNOWN_ISSUES.md +10 -10
- package/template/README.md +41 -60
- package/template/next.config.js +1 -1
- package/template/package.json +1 -1
- package/template/src/app/dashboard/layout.tsx +26 -23
- package/template/src/app/dashboard/settings/page.tsx +11 -11
- package/template/src/app/layout.tsx +1 -1
- package/template/src/app/login/page.tsx +25 -22
- package/template/src/components/providers.tsx +1 -1
- package/template/src/lib/hooks.ts +7 -7
- package/template/varity.config.json +1 -1
- package/template/src/services/dashboardService.ts +0 -589
package/dist/create.js
CHANGED
|
@@ -7,7 +7,7 @@ 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-
|
|
10
|
+
const VARITY_PACKAGE_VERSION = "^2.0.0-beta.2";
|
|
11
11
|
const WORKSPACE_DEPS = {
|
|
12
12
|
"@varity-labs/sdk": VARITY_PACKAGE_VERSION,
|
|
13
13
|
"@varity-labs/types": VARITY_PACKAGE_VERSION,
|
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
# For production: `varitykit app deploy` injects all credentials automatically.
|
|
7
7
|
# You never need to manually set these values.
|
|
8
8
|
|
|
9
|
-
# Auth (
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# Auth configuration (managed by Varity)
|
|
10
|
+
# Leave blank for dev mode, auto-configured on deploy
|
|
11
|
+
NEXT_PUBLIC_VARITY_AUTH_ID=
|
|
12
12
|
|
|
13
13
|
# Database (optional — dev database used automatically when blank)
|
|
14
14
|
NEXT_PUBLIC_VARITY_APP_TOKEN=
|
|
File without changes
|
package/template/KNOWN_ISSUES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Known Issues
|
|
1
|
+
# Known Issues -- SaaS Starter Template
|
|
2
2
|
|
|
3
|
-
> **Last Updated:**
|
|
3
|
+
> **Last Updated:** March 4, 2026
|
|
4
4
|
> **Template:** `saas-starter` (TaskFlow)
|
|
5
5
|
> **Status:** All features functional, builds with 0 errors (8 routes + _not-found)
|
|
6
6
|
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
| Feature | Status | Notes |
|
|
12
12
|
|---------|--------|-------|
|
|
13
13
|
| Landing Page | Working | 6 sections, scroll animations, social proof, testimonials, dashboard mockup |
|
|
14
|
-
| Login | Working |
|
|
14
|
+
| Login | Working | Email/Google auth, zero-config with dev credentials, auto-redirect |
|
|
15
15
|
| Dashboard | Working | KPI cards, getting started checklist, recent activity feed |
|
|
16
16
|
| Projects CRUD | Working | Master-detail, nested tasks, CSV export, validation, optimistic updates |
|
|
17
17
|
| Tasks CRUD | Working | Status cycling (click to advance), filtering, CSV export, cross-references projects |
|
|
18
18
|
| Team CRUD | Working | Invite members, role management, email validation, role badges |
|
|
19
|
-
| Settings | Working | 4 tabs (General, Security, Billing, Account), backend persistence
|
|
19
|
+
| Settings | Working | 4 tabs (General, Security, Billing, Account), backend persistence, skeleton loading |
|
|
20
20
|
| Command Palette | Working | Cmd+K / Ctrl+K, searches pages and actions |
|
|
21
21
|
| Toast Notifications | Working | Success/error/info with progress bar, exit animation, max 3 stack |
|
|
22
22
|
| Protected Routes | Working | Automatic redirect for unauthenticated users |
|
|
23
23
|
| Color Themes | Working | 4 built-in presets (Blue, Purple, Green, Orange) via CSS variables |
|
|
24
|
-
| Static Export | Working | `output: 'export'` for
|
|
24
|
+
| Static Export | Working | `output: 'export'` for static hosting deployment |
|
|
25
25
|
| Mobile Nav | Working | Hamburger menu with responsive sidebar |
|
|
26
26
|
| CSV Export | Working | One-click export for tasks and projects |
|
|
27
27
|
| SEO | Working | OpenGraph, Twitter cards, robots.txt, sitemap template |
|
|
@@ -29,25 +29,25 @@
|
|
|
29
29
|
## Known Issues
|
|
30
30
|
|
|
31
31
|
### 1. Navigation Flash (UI-Kit Limitation)
|
|
32
|
-
Brief "Initializing Dashboard" screen when navigating between dashboard pages. Caused by
|
|
32
|
+
Brief "Initializing Dashboard" screen when navigating between dashboard pages. Caused by the auth guard in UI-Kit re-checking auth state on each route change. Resolves in <1 second. This is a UI-Kit issue, not template-side.
|
|
33
33
|
|
|
34
34
|
### 2. DashboardLayout Mobile Support
|
|
35
|
-
The `DashboardLayout` from `@varity-labs/ui-kit` does not include mobile navigation. The template provides its own responsive sidebar in `src/app/dashboard/layout.tsx` as a workaround. UI-Kit mobile improvements are planned
|
|
35
|
+
The `DashboardLayout` from `@varity-labs/ui-kit` does not include mobile navigation. The template provides its own responsive sidebar in `src/app/dashboard/layout.tsx` as a workaround. UI-Kit mobile improvements are planned.
|
|
36
36
|
|
|
37
37
|
### 3. Billing Section is Mock
|
|
38
38
|
The Settings > Billing tab shows a mock UI (plan name, usage bars, payment method). Developers should wire their own billing provider (Stripe, etc.).
|
|
39
39
|
|
|
40
40
|
### 4. Sessions Are Mock
|
|
41
|
-
The Settings > Security tab shows active sessions with a "Revoke" button. Session data is client-side mock
|
|
41
|
+
The Settings > Security tab shows active sessions with a "Revoke" button. Session data is client-side mock -- real session management is handled by the auth provider.
|
|
42
42
|
|
|
43
43
|
### 5. Password & Profile Managed by Auth Provider
|
|
44
|
-
"Change password" and profile photo are managed by
|
|
44
|
+
"Change password" and profile photo are managed by the authentication provider. The Settings page shows informational dialogs explaining this.
|
|
45
45
|
|
|
46
46
|
### 6. No Server-Side Rendering
|
|
47
47
|
All pages are statically exported (`output: 'export'`). No server-side rendering, API routes, or middleware. Data fetching happens client-side via the SDK.
|
|
48
48
|
|
|
49
49
|
### 7. Team Email Invites
|
|
50
|
-
No SMTP integration
|
|
50
|
+
No SMTP integration -- team members are added to the database only. No invitation email is sent. Developers should integrate their own email service.
|
|
51
51
|
|
|
52
52
|
## Environment
|
|
53
53
|
|
package/template/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# TaskFlow
|
|
1
|
+
# TaskFlow -- SaaS Starter Template
|
|
2
2
|
|
|
3
3
|
[](https://varity.so)
|
|
4
4
|
|
|
5
|
-
A full-featured project management app built with [Varity](https://varity.so). Everything works immediately
|
|
5
|
+
A full-featured project management app built with [Varity](https://varity.so). Everything works immediately -- no configuration, no API keys, no setup.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -23,12 +23,12 @@ No `.env` file needed. No accounts to create. No credentials to configure.
|
|
|
23
23
|
|
|
24
24
|
Transform this into your own branded SaaS app:
|
|
25
25
|
|
|
26
|
-
1. **App name**
|
|
27
|
-
2. **Logo**
|
|
28
|
-
3. **Colors**
|
|
29
|
-
4. **Meta title**
|
|
30
|
-
5. **Navigation**
|
|
31
|
-
6. **Landing page**
|
|
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
32
|
|
|
33
33
|
## Built-in Color Themes
|
|
34
34
|
|
|
@@ -44,71 +44,53 @@ Switch your entire app's color scheme by editing `src/app/globals.css`:
|
|
|
44
44
|
|
|
45
45
|
## What's Included
|
|
46
46
|
|
|
47
|
-
- **Zero-Config Auth**
|
|
48
|
-
- **Zero-Config Database**
|
|
49
|
-
- **Dashboard**
|
|
50
|
-
- **Full CRUD**
|
|
51
|
-
- **Command Palette**
|
|
52
|
-
- **Protected Routes**
|
|
53
|
-
- **Landing Page**
|
|
54
|
-
- **Mobile Responsive**
|
|
55
|
-
- **TypeScript**
|
|
56
|
-
- **Tailwind CSS**
|
|
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
57
|
|
|
58
|
-
##
|
|
58
|
+
## Zero Configuration Required
|
|
59
59
|
|
|
60
60
|
This template works immediately with **zero setup**:
|
|
61
61
|
|
|
62
62
|
### Instant Auth
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
63
|
+
- Email login (email + Google)
|
|
64
|
+
- Social login support
|
|
65
|
+
- Dev credentials built-in
|
|
66
|
+
- No env vars needed
|
|
67
67
|
|
|
68
68
|
### Instant Database
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
69
|
+
- Create, read, update, delete data
|
|
70
|
+
- Dev token built-in
|
|
71
|
+
- Production-ready
|
|
72
|
+
- No credentials needed
|
|
73
73
|
|
|
74
74
|
### Instant Deploy
|
|
75
75
|
```bash
|
|
76
|
-
|
|
76
|
+
varitykit app deploy
|
|
77
77
|
```
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
78
|
+
- Deploys to production
|
|
79
|
+
- Auto-configures credentials
|
|
80
|
+
- No accounts needed
|
|
81
81
|
|
|
82
82
|
---
|
|
83
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
84
|
### Static Export Ready
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
85
|
+
- `output: 'export'` in next.config.js
|
|
86
|
+
- All pages pre-rendered to static HTML
|
|
87
|
+
- No server-side dependencies
|
|
88
|
+
- CDN deployable
|
|
107
89
|
|
|
108
90
|
### Type Safety
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
91
|
+
- TypeScript strict mode enabled
|
|
92
|
+
- All errors surface during build
|
|
93
|
+
- No `ignoreBuildErrors` flag
|
|
112
94
|
|
|
113
95
|
## Project Structure
|
|
114
96
|
|
|
@@ -205,18 +187,17 @@ export function useInvoices(): UseCollectionReturn<Invoice> {
|
|
|
205
187
|
const { data, loading, create, update, remove } = useInvoices();
|
|
206
188
|
```
|
|
207
189
|
|
|
208
|
-
The database collection is created automatically on first use
|
|
190
|
+
The database collection is created automatically on first use -- no migrations needed.
|
|
209
191
|
|
|
210
192
|
## Environment Variables
|
|
211
193
|
|
|
212
194
|
**For development:** Leave everything blank. Shared development credentials are used automatically.
|
|
213
195
|
|
|
214
|
-
**For production:** Run `varitykit app deploy`
|
|
196
|
+
**For production:** Run `varitykit app deploy` -- it injects all credentials into your build automatically. You never need to manually set API keys.
|
|
215
197
|
|
|
216
198
|
| Variable | Required | Notes |
|
|
217
199
|
|----------|----------|-------|
|
|
218
200
|
| `NEXT_PUBLIC_PRIVY_APP_ID` | No | Auth provider (auto-configured) |
|
|
219
|
-
| `NEXT_PUBLIC_THIRDWEB_CLIENT_ID` | No | Infrastructure (auto-configured) |
|
|
220
201
|
| `NEXT_PUBLIC_VARITY_APP_TOKEN` | No | Database token (auto-configured) |
|
|
221
202
|
| `NEXT_PUBLIC_VARITY_APP_ID` | No | App ID (auto-configured) |
|
|
222
203
|
|
|
@@ -230,7 +211,7 @@ varitykit app deploy
|
|
|
230
211
|
varitykit app deploy --submit-to-store
|
|
231
212
|
```
|
|
232
213
|
|
|
233
|
-
The CLI builds your app, provisions a private database, injects production credentials, and deploys
|
|
214
|
+
The CLI builds your app, provisions a private database, injects production credentials, and deploys -- all in one command.
|
|
234
215
|
|
|
235
216
|
**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
217
|
|
package/template/next.config.js
CHANGED
|
@@ -5,7 +5,7 @@ const nextConfig = {
|
|
|
5
5
|
trailingSlash: true,
|
|
6
6
|
productionBrowserSourceMaps: false,
|
|
7
7
|
webpack: (config, { isServer, dev }) => {
|
|
8
|
-
// Suppress
|
|
8
|
+
// Suppress async-storage warning from auth provider dependencies
|
|
9
9
|
config.resolve.fallback = {
|
|
10
10
|
...config.resolve.fallback,
|
|
11
11
|
'@react-native-async-storage/async-storage': false,
|
package/template/package.json
CHANGED
|
@@ -7,18 +7,20 @@ import { useProjects, useTasks, useTeam } from '@/lib/hooks';
|
|
|
7
7
|
import { CommandPalette } from '@varity-labs/ui-kit';
|
|
8
8
|
import { Menu, X } from 'lucide-react';
|
|
9
9
|
|
|
10
|
-
// Conditionally import
|
|
10
|
+
// Conditionally import UI-Kit components
|
|
11
11
|
let DashboardLayout: any = null;
|
|
12
|
-
let
|
|
13
|
-
let
|
|
14
|
-
let
|
|
12
|
+
let ProtectedRoute: any = null;
|
|
13
|
+
let AuthStackComponent: any = null;
|
|
14
|
+
let ZeroDevProviderComponent: any = null;
|
|
15
|
+
let useAuthHook: any = null;
|
|
15
16
|
|
|
16
17
|
try {
|
|
17
18
|
const uiKit = require('@varity-labs/ui-kit');
|
|
18
19
|
DashboardLayout = uiKit.DashboardLayout;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
ProtectedRoute = uiKit.PrivyProtectedRoute;
|
|
21
|
+
AuthStackComponent = uiKit.PrivyStack;
|
|
22
|
+
ZeroDevProviderComponent = uiKit.ZeroDevProvider;
|
|
23
|
+
useAuthHook = uiKit.useAuth;
|
|
22
24
|
} catch {}
|
|
23
25
|
|
|
24
26
|
function RedirectToLogin() {
|
|
@@ -111,8 +113,8 @@ function MobileNav({
|
|
|
111
113
|
|
|
112
114
|
function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
113
115
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
114
|
-
const
|
|
115
|
-
const { user, logout } =
|
|
116
|
+
const auth = useAuthHook ? useAuthHook() : { user: null, logout: async () => {} };
|
|
117
|
+
const { user, logout } = auth;
|
|
116
118
|
const pathname = usePathname();
|
|
117
119
|
const router = useRouter();
|
|
118
120
|
const isMobile = useIsMobile();
|
|
@@ -152,7 +154,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
152
154
|
const userName = user?.email?.address?.split('@')[0] || 'User';
|
|
153
155
|
const userEmail = user?.email?.address || '';
|
|
154
156
|
|
|
155
|
-
const
|
|
157
|
+
const handleSignOut = async () => {
|
|
156
158
|
await logout();
|
|
157
159
|
router.push('/');
|
|
158
160
|
};
|
|
@@ -194,7 +196,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
194
196
|
</nav>
|
|
195
197
|
<div className="mt-8 px-3 border-t border-gray-200 pt-4">
|
|
196
198
|
<button
|
|
197
|
-
onClick={
|
|
199
|
+
onClick={handleSignOut}
|
|
198
200
|
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
201
|
>
|
|
200
202
|
Sign Out
|
|
@@ -210,7 +212,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
210
212
|
onClose={() => setMobileMenuOpen(false)}
|
|
211
213
|
navItems={navWithActive}
|
|
212
214
|
userEmail={userEmail}
|
|
213
|
-
onLogout={
|
|
215
|
+
onLogout={handleSignOut}
|
|
214
216
|
onNavigate={(path) => router.push(path)}
|
|
215
217
|
/>
|
|
216
218
|
)}
|
|
@@ -241,7 +243,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
241
243
|
onClose={() => setMobileMenuOpen(false)}
|
|
242
244
|
navItems={navWithActive}
|
|
243
245
|
userEmail={userEmail}
|
|
244
|
-
onLogout={
|
|
246
|
+
onLogout={handleSignOut}
|
|
245
247
|
onNavigate={(path) => router.push(path)}
|
|
246
248
|
/>
|
|
247
249
|
)}
|
|
@@ -257,7 +259,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
257
259
|
name: userName,
|
|
258
260
|
address: userEmail,
|
|
259
261
|
}}
|
|
260
|
-
onLogout={
|
|
262
|
+
onLogout={handleSignOut}
|
|
261
263
|
onNavigateToProfile={() => router.push('/dashboard/settings')}
|
|
262
264
|
onNavigateToSettings={() => router.push('/dashboard/settings')}
|
|
263
265
|
onSearchClick={() => setCommandPaletteOpen(true)}
|
|
@@ -277,22 +279,23 @@ export default function DashboardRootLayout({
|
|
|
277
279
|
}: {
|
|
278
280
|
children: React.ReactNode;
|
|
279
281
|
}) {
|
|
280
|
-
//
|
|
281
|
-
if (!
|
|
282
|
+
// Auth provider (managed by Varity) with gas sponsorship
|
|
283
|
+
if (!ProtectedRoute || !AuthStackComponent || !ZeroDevProviderComponent) {
|
|
282
284
|
// Fallback if ui-kit package isn't installed
|
|
283
285
|
return <DashboardShell>{children}</DashboardShell>;
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
return (
|
|
287
|
-
<
|
|
288
|
-
appId={process.env.
|
|
289
|
-
thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
|
|
289
|
+
<AuthStackComponent
|
|
290
|
+
appId={process.env.NEXT_PUBLIC_VARITY_AUTH_ID}
|
|
290
291
|
loginMethods={['email', 'google']}
|
|
291
292
|
appearance={{ theme: 'light', accentColor: '#2563EB', logo: '/logo.svg' }}
|
|
292
293
|
>
|
|
293
|
-
<
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
<ZeroDevProviderComponent>
|
|
295
|
+
<ProtectedRoute fallback={<RedirectToLogin />}>
|
|
296
|
+
<DashboardShell>{children}</DashboardShell>
|
|
297
|
+
</ProtectedRoute>
|
|
298
|
+
</ZeroDevProviderComponent>
|
|
299
|
+
</AuthStackComponent>
|
|
297
300
|
);
|
|
298
301
|
}
|
|
@@ -37,15 +37,15 @@ import {
|
|
|
37
37
|
Key,
|
|
38
38
|
} from 'lucide-react';
|
|
39
39
|
|
|
40
|
-
const
|
|
40
|
+
const authAppId = process.env.NEXT_PUBLIC_VARITY_AUTH_ID;
|
|
41
41
|
|
|
42
|
-
let
|
|
43
|
-
let
|
|
42
|
+
let UserProfileComponent: any = null;
|
|
43
|
+
let useAuthHook: any = null;
|
|
44
44
|
|
|
45
45
|
try {
|
|
46
46
|
const uiKit = require('@varity-labs/ui-kit');
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
UserProfileComponent = uiKit.PrivyUserProfile;
|
|
48
|
+
useAuthHook = uiKit.useAuth;
|
|
49
49
|
} catch {}
|
|
50
50
|
|
|
51
51
|
const TABS = [
|
|
@@ -79,7 +79,7 @@ export default function SettingsPage() {
|
|
|
79
79
|
const { settings, loading, update: updateSettings } = useUserSettings();
|
|
80
80
|
const toast = useToast();
|
|
81
81
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() + env var, stable across renders
|
|
82
|
-
const
|
|
82
|
+
const auth = authAppId && useAuthHook ? useAuthHook() : { logout: async () => {} };
|
|
83
83
|
|
|
84
84
|
const [activeTab, setActiveTab] = useState<TabId>('general');
|
|
85
85
|
|
|
@@ -556,14 +556,14 @@ export default function SettingsPage() {
|
|
|
556
556
|
)}
|
|
557
557
|
</div>
|
|
558
558
|
|
|
559
|
-
{/*
|
|
560
|
-
{
|
|
559
|
+
{/* Advanced Account Settings */}
|
|
560
|
+
{authAppId && UserProfileComponent && (
|
|
561
561
|
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
562
562
|
<h2 className="mb-4 text-lg font-semibold text-gray-900 flex items-center gap-2">
|
|
563
563
|
<Key className="h-5 w-5" />
|
|
564
|
-
Account Settings
|
|
564
|
+
Advanced Account Settings
|
|
565
565
|
</h2>
|
|
566
|
-
<
|
|
566
|
+
<UserProfileComponent showLogoutButton={false} />
|
|
567
567
|
</div>
|
|
568
568
|
)}
|
|
569
569
|
|
|
@@ -577,7 +577,7 @@ export default function SettingsPage() {
|
|
|
577
577
|
These actions are permanent and cannot be undone.
|
|
578
578
|
</p>
|
|
579
579
|
<div className="flex flex-wrap gap-3">
|
|
580
|
-
<Button variant="danger" onClick={() =>
|
|
580
|
+
<Button variant="danger" onClick={() => auth.logout()}>
|
|
581
581
|
<LogOut className="h-4 w-4" />
|
|
582
582
|
Sign Out
|
|
583
583
|
</Button>
|
|
@@ -5,7 +5,7 @@ import './globals.css';
|
|
|
5
5
|
export const metadata: Metadata = {
|
|
6
6
|
title: 'TaskFlow - Project Management',
|
|
7
7
|
description: 'Manage projects, track tasks, and collaborate with your team.',
|
|
8
|
-
metadataBase: new URL('https://
|
|
8
|
+
metadataBase: new URL('https://varity.so'),
|
|
9
9
|
openGraph: {
|
|
10
10
|
title: 'TaskFlow - Project Management',
|
|
11
11
|
description: 'Manage projects, track tasks, and collaborate with your team.',
|
|
@@ -6,29 +6,31 @@ import Link from 'next/link';
|
|
|
6
6
|
import { CheckCircle } from 'lucide-react';
|
|
7
7
|
import { APP_NAME } from '@/lib/constants';
|
|
8
8
|
|
|
9
|
-
let
|
|
10
|
-
let
|
|
9
|
+
let AuthStackComponent: any = null;
|
|
10
|
+
let ZeroDevProviderComponent: any = null;
|
|
11
|
+
let useAuthHook: (() => { authenticated: boolean; ready: boolean; login: () => void }) | null = null;
|
|
11
12
|
|
|
12
13
|
try {
|
|
13
14
|
const uiKit = require('@varity-labs/ui-kit');
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
AuthStackComponent = uiKit.PrivyStack;
|
|
16
|
+
ZeroDevProviderComponent = uiKit.ZeroDevProvider;
|
|
17
|
+
useAuthHook = uiKit.useAuth;
|
|
16
18
|
} catch {}
|
|
17
19
|
|
|
18
20
|
function LoginContent() {
|
|
19
21
|
const router = useRouter();
|
|
20
22
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
21
|
-
const
|
|
23
|
+
const auth = useAuthHook ? useAuthHook() : null;
|
|
22
24
|
|
|
23
25
|
useEffect(() => {
|
|
24
|
-
if (
|
|
26
|
+
if (auth?.authenticated) {
|
|
25
27
|
router.push('/dashboard');
|
|
26
28
|
}
|
|
27
|
-
}, [
|
|
29
|
+
}, [auth?.authenticated, router]);
|
|
28
30
|
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
|
|
31
|
+
const handleSignIn = () => {
|
|
32
|
+
if (auth?.login) {
|
|
33
|
+
auth.login();
|
|
32
34
|
}
|
|
33
35
|
};
|
|
34
36
|
|
|
@@ -49,15 +51,15 @@ function LoginContent() {
|
|
|
49
51
|
</div>
|
|
50
52
|
|
|
51
53
|
<div className="rounded-xl border border-gray-200 bg-white p-8 shadow-sm">
|
|
52
|
-
{
|
|
54
|
+
{auth ? (
|
|
53
55
|
<button
|
|
54
|
-
onClick={
|
|
55
|
-
disabled={!
|
|
56
|
+
onClick={handleSignIn}
|
|
57
|
+
disabled={!auth.ready || auth.authenticated}
|
|
56
58
|
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
59
|
>
|
|
58
|
-
{!
|
|
60
|
+
{!auth.ready
|
|
59
61
|
? 'Loading...'
|
|
60
|
-
:
|
|
62
|
+
: auth.authenticated
|
|
61
63
|
? 'Already Signed In'
|
|
62
64
|
: 'Sign In with Email or Social'}
|
|
63
65
|
</button>
|
|
@@ -79,17 +81,18 @@ function LoginContent() {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export default function LoginPage() {
|
|
82
|
-
//
|
|
83
|
-
if (
|
|
84
|
+
// Auth provider (managed by Varity) with gas sponsorship
|
|
85
|
+
if (AuthStackComponent && ZeroDevProviderComponent) {
|
|
84
86
|
return (
|
|
85
|
-
<
|
|
86
|
-
appId={process.env.
|
|
87
|
-
thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
|
|
87
|
+
<AuthStackComponent
|
|
88
|
+
appId={process.env.NEXT_PUBLIC_VARITY_AUTH_ID}
|
|
88
89
|
loginMethods={['email', 'google']}
|
|
89
90
|
appearance={{ theme: 'light', accentColor: '#2563EB', logo: '/logo.svg' }}
|
|
90
91
|
>
|
|
91
|
-
<
|
|
92
|
-
|
|
92
|
+
<ZeroDevProviderComponent>
|
|
93
|
+
<LoginContent />
|
|
94
|
+
</ZeroDevProviderComponent>
|
|
95
|
+
</AuthStackComponent>
|
|
93
96
|
);
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -5,7 +5,7 @@ import { ToastProvider } from '@varity-labs/ui-kit';
|
|
|
5
5
|
|
|
6
6
|
export function Providers({ children }: { children: ReactNode }) {
|
|
7
7
|
// Only ToastProvider at the global level.
|
|
8
|
-
//
|
|
8
|
+
// Auth provider is added in dashboard/layout.tsx and login/page.tsx —
|
|
9
9
|
// the landing page loads instantly without any auth dependency.
|
|
10
10
|
return <ToastProvider>{children}</ToastProvider>;
|
|
11
11
|
}
|
|
@@ -4,18 +4,18 @@ import { useState, useEffect, useCallback } from 'react';
|
|
|
4
4
|
import { projects, tasks, teamMembers, userSettings } from './database';
|
|
5
5
|
import type { Project, Task, TeamMember, UserSettings } from '../types';
|
|
6
6
|
|
|
7
|
-
let
|
|
7
|
+
let useAuthHook: any = null;
|
|
8
8
|
try {
|
|
9
9
|
const uiKit = require('@varity-labs/ui-kit');
|
|
10
|
-
|
|
10
|
+
useAuthHook = uiKit.useAuth;
|
|
11
11
|
} catch {}
|
|
12
12
|
|
|
13
13
|
export function useCurrentUser() {
|
|
14
14
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
15
|
-
const
|
|
15
|
+
const auth = useAuthHook ? useAuthHook() : { user: null, authenticated: false, logout: async () => {} };
|
|
16
16
|
|
|
17
|
-
// Extract email from any
|
|
18
|
-
const user =
|
|
17
|
+
// Extract email from any auth method (email, Google, GitHub, etc.)
|
|
18
|
+
const user = auth.user;
|
|
19
19
|
const email =
|
|
20
20
|
user?.email?.address ||
|
|
21
21
|
user?.google?.email ||
|
|
@@ -30,8 +30,8 @@ export function useCurrentUser() {
|
|
|
30
30
|
id: user?.id || 'dev-user-id',
|
|
31
31
|
email,
|
|
32
32
|
name,
|
|
33
|
-
authenticated:
|
|
34
|
-
logout:
|
|
33
|
+
authenticated: auth.authenticated,
|
|
34
|
+
logout: auth.logout,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -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
|
-
}
|