create-varity-app 2.0.0-beta.10 → 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/package.json +1 -1
- package/template/src/app/dashboard/layout.tsx +15 -14
- package/template/src/app/dashboard/page.tsx +7 -6
- package/template/src/app/layout.tsx +9 -9
- package/template/src/app/login/page.tsx +15 -13
- package/template/src/app/not-found.tsx +3 -6
- package/template/src/app/page.tsx +0 -1
- package/template/src/components/dashboard/RecentActivity.tsx +5 -7
- package/template/src/components/landing/CTA.tsx +4 -7
- package/template/src/components/landing/Hero.tsx +4 -5
- package/template/src/components/landing/Pricing.tsx +11 -14
- package/template/src/components/shared/Footer.tsx +3 -5
- package/template/src/components/shared/Navbar.tsx +13 -17
- package/template/src/lib/constants.ts +1 -1
- package/template/src/lib/utils.ts +0 -21
- package/template/src/services/dashboardService.ts +0 -589
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
-
import { usePathname } from 'next/navigation';
|
|
5
|
-
import { appNavigate } from '@/lib/utils';
|
|
4
|
+
import { useRouter, usePathname } from 'next/navigation';
|
|
6
5
|
import { APP_NAME, NAVIGATION_ITEMS } from '@/lib/constants';
|
|
7
6
|
import { useProjects, useTasks, useTeam } from '@/lib/hooks';
|
|
8
7
|
import { CommandPalette } from '@varity-labs/ui-kit';
|
|
@@ -23,9 +22,10 @@ try {
|
|
|
23
22
|
} catch {}
|
|
24
23
|
|
|
25
24
|
function RedirectToLogin() {
|
|
25
|
+
const router = useRouter();
|
|
26
26
|
useEffect(() => {
|
|
27
|
-
|
|
28
|
-
}, []);
|
|
27
|
+
router.push('/login');
|
|
28
|
+
}, [router]);
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -113,6 +113,7 @@ 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 router = useRouter();
|
|
116
117
|
const pathname = usePathname();
|
|
117
118
|
const isMobile = useIsMobile();
|
|
118
119
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
@@ -153,7 +154,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
153
154
|
|
|
154
155
|
const handleLogout = async () => {
|
|
155
156
|
await logout();
|
|
156
|
-
|
|
157
|
+
window.location.href = '/';
|
|
157
158
|
};
|
|
158
159
|
|
|
159
160
|
// Fallback layout when DashboardLayout from ui-kit isn't available
|
|
@@ -163,7 +164,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
163
164
|
<CommandPalette
|
|
164
165
|
open={commandPaletteOpen}
|
|
165
166
|
onClose={() => setCommandPaletteOpen(false)}
|
|
166
|
-
onNavigate={(path: string) =>
|
|
167
|
+
onNavigate={(path: string) => router.push(path)}
|
|
167
168
|
projects={projects}
|
|
168
169
|
tasks={tasks}
|
|
169
170
|
team={team}
|
|
@@ -180,7 +181,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
180
181
|
{navWithActive.map((item) => (
|
|
181
182
|
<button
|
|
182
183
|
key={item.path}
|
|
183
|
-
onClick={() =>
|
|
184
|
+
onClick={() => router.push(item.path)}
|
|
184
185
|
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
|
|
185
186
|
item.active
|
|
186
187
|
? 'bg-primary-50 text-primary-700'
|
|
@@ -210,7 +211,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
210
211
|
navItems={navWithActive}
|
|
211
212
|
userEmail={userEmail}
|
|
212
213
|
onLogout={handleLogout}
|
|
213
|
-
onNavigate={(path) =>
|
|
214
|
+
onNavigate={(path) => router.push(path)}
|
|
214
215
|
/>
|
|
215
216
|
)}
|
|
216
217
|
|
|
@@ -227,7 +228,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
227
228
|
<CommandPalette
|
|
228
229
|
open={commandPaletteOpen}
|
|
229
230
|
onClose={() => setCommandPaletteOpen(false)}
|
|
230
|
-
onNavigate={(path: string) =>
|
|
231
|
+
onNavigate={(path: string) => router.push(path)}
|
|
231
232
|
projects={projects}
|
|
232
233
|
tasks={tasks}
|
|
233
234
|
team={team}
|
|
@@ -241,7 +242,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
241
242
|
navItems={navWithActive}
|
|
242
243
|
userEmail={userEmail}
|
|
243
244
|
onLogout={handleLogout}
|
|
244
|
-
onNavigate={(path) =>
|
|
245
|
+
onNavigate={(path) => router.push(path)}
|
|
245
246
|
/>
|
|
246
247
|
)}
|
|
247
248
|
|
|
@@ -256,10 +257,10 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
256
257
|
name: userName,
|
|
257
258
|
address: userEmail,
|
|
258
259
|
}}
|
|
259
|
-
onNavigate={(path: string) =>
|
|
260
|
+
onNavigate={(path: string) => router.push(path)}
|
|
260
261
|
onLogout={handleLogout}
|
|
261
|
-
onNavigateToProfile={() =>
|
|
262
|
-
onNavigateToSettings={() =>
|
|
262
|
+
onNavigateToProfile={() => router.push('/dashboard/settings')}
|
|
263
|
+
onNavigateToSettings={() => router.push('/dashboard/settings')}
|
|
263
264
|
onSearchClick={() => setCommandPaletteOpen(true)}
|
|
264
265
|
searchPlaceholder="Search projects, tasks, team..."
|
|
265
266
|
>
|
|
@@ -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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useRouter } from 'next/navigation';
|
|
4
4
|
import { useProjects, useTasks, useTeam, useCurrentUser } from '@/lib/hooks';
|
|
5
5
|
import { DashboardStats } from '@/components/dashboard/DashboardStats';
|
|
6
6
|
import { RecentActivity } from '@/components/dashboard/RecentActivity';
|
|
@@ -8,7 +8,7 @@ import { APP_NAME } from '@/lib/constants';
|
|
|
8
8
|
import { FolderKanban, ListTodo, Users, ArrowRight } from 'lucide-react';
|
|
9
9
|
|
|
10
10
|
function QuickActions() {
|
|
11
|
-
|
|
11
|
+
const router = useRouter();
|
|
12
12
|
const actions = [
|
|
13
13
|
{
|
|
14
14
|
label: 'New Project',
|
|
@@ -48,7 +48,7 @@ function QuickActions() {
|
|
|
48
48
|
{actions.map((action) => (
|
|
49
49
|
<button
|
|
50
50
|
key={action.path}
|
|
51
|
-
onClick={() =>
|
|
51
|
+
onClick={() => router.push(action.path)}
|
|
52
52
|
className="flex items-center gap-3 rounded-xl border border-gray-200 bg-white p-4 text-left shadow-sm hover:shadow-md hover:border-gray-300 transition-all"
|
|
53
53
|
>
|
|
54
54
|
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${action.color}`}>
|
|
@@ -73,7 +73,8 @@ function GettingStarted({
|
|
|
73
73
|
hasTasks: boolean;
|
|
74
74
|
hasTeam: boolean;
|
|
75
75
|
}) {
|
|
76
|
-
|
|
76
|
+
const router = useRouter();
|
|
77
|
+
const completedCount = [hasProjects, hasTasks, hasTeam].filter(Boolean).length;
|
|
77
78
|
|
|
78
79
|
if (completedCount === 3) return null;
|
|
79
80
|
|
|
@@ -111,7 +112,7 @@ function GettingStarted({
|
|
|
111
112
|
{steps.map((step) => (
|
|
112
113
|
<button
|
|
113
114
|
key={step.label}
|
|
114
|
-
onClick={() => !step.done &&
|
|
115
|
+
onClick={() => !step.done && router.push(step.path)}
|
|
115
116
|
disabled={step.done}
|
|
116
117
|
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors ${
|
|
117
118
|
step.done
|
|
@@ -136,7 +137,7 @@ function GettingStarted({
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
export default function DashboardPage() {
|
|
139
|
-
|
|
140
|
+
const { name } = useCurrentUser();
|
|
140
141
|
const { data: projects, loading: projectsLoading, error: projectsError, refresh: refreshProjects } = useProjects();
|
|
141
142
|
const { data: tasks, loading: tasksLoading, error: tasksError, refresh: refreshTasks } = useTasks();
|
|
142
143
|
const { data: team, loading: teamLoading, error: teamError, refresh: refreshTeam } = useTeam();
|
|
@@ -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
|
|
9
|
+
title: APP_NAME,
|
|
10
|
+
description,
|
|
8
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
|
|
|
@@ -25,9 +28,6 @@ export default function RootLayout({
|
|
|
25
28
|
}) {
|
|
26
29
|
return (
|
|
27
30
|
<html lang="en">
|
|
28
|
-
<head>
|
|
29
|
-
<base href="./" />
|
|
30
|
-
</head>
|
|
31
31
|
<body>
|
|
32
32
|
<Providers>{children}</Providers>
|
|
33
33
|
</body>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
5
6
|
import { CheckCircle } from 'lucide-react';
|
|
6
7
|
import { APP_NAME } from '@/lib/constants';
|
|
7
8
|
|
|
@@ -14,15 +15,22 @@ try {
|
|
|
14
15
|
usePrivyHook = uiKit.usePrivy;
|
|
15
16
|
} catch {}
|
|
16
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
|
+
|
|
17
24
|
function LoginContent() {
|
|
25
|
+
const router = useRouter();
|
|
18
26
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
19
27
|
const privy = usePrivyHook ? usePrivyHook() : null;
|
|
20
28
|
|
|
21
29
|
useEffect(() => {
|
|
22
30
|
if (privy?.authenticated) {
|
|
23
|
-
|
|
31
|
+
router.push('/dashboard');
|
|
24
32
|
}
|
|
25
|
-
}, [privy?.authenticated]);
|
|
33
|
+
}, [privy?.authenticated, router]);
|
|
26
34
|
|
|
27
35
|
const handleLogin = () => {
|
|
28
36
|
if (privy?.login) {
|
|
@@ -34,10 +42,10 @@ function LoginContent() {
|
|
|
34
42
|
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4">
|
|
35
43
|
<div className="w-full max-w-md space-y-8">
|
|
36
44
|
<div className="text-center">
|
|
37
|
-
<
|
|
45
|
+
<Link href="/" className="inline-flex items-center gap-2">
|
|
38
46
|
<CheckCircle className="h-8 w-8 text-primary-600" />
|
|
39
47
|
<span className="text-2xl font-bold text-gray-900">{APP_NAME}</span>
|
|
40
|
-
</
|
|
48
|
+
</Link>
|
|
41
49
|
<h2 className="mt-6 text-2xl font-bold text-gray-900">
|
|
42
50
|
Sign in to your account
|
|
43
51
|
</h2>
|
|
@@ -53,11 +61,7 @@ function LoginContent() {
|
|
|
53
61
|
disabled={!privy.ready || privy.authenticated}
|
|
54
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"
|
|
55
63
|
>
|
|
56
|
-
{
|
|
57
|
-
? 'Loading...'
|
|
58
|
-
: privy.authenticated
|
|
59
|
-
? 'Already Signed In'
|
|
60
|
-
: 'Sign In with Email or Social'}
|
|
64
|
+
{loginButtonLabel(privy)}
|
|
61
65
|
</button>
|
|
62
66
|
) : (
|
|
63
67
|
<div className="text-center space-y-4">
|
|
@@ -77,20 +81,18 @@ function LoginContent() {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
export default function LoginPage() {
|
|
80
|
-
// Always wrap in PrivyStack - it uses dev credentials automatically when no appId is provided
|
|
81
84
|
if (PrivyStackComponent) {
|
|
82
85
|
return (
|
|
83
86
|
<PrivyStackComponent
|
|
84
87
|
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
|
|
85
88
|
thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
|
|
86
89
|
loginMethods={['email', 'google']}
|
|
87
|
-
appearance={{ theme: 'light', accentColor: '#2563EB'
|
|
90
|
+
appearance={{ theme: 'light', accentColor: '#2563EB' }}
|
|
88
91
|
>
|
|
89
92
|
<LoginContent />
|
|
90
93
|
</PrivyStackComponent>
|
|
91
94
|
);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
// Fallback if ui-kit package isn't installed
|
|
95
97
|
return <LoginContent />;
|
|
96
98
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { appNavigate } from '@/lib/utils';
|
|
1
|
+
import Link from 'next/link';
|
|
4
2
|
|
|
5
3
|
export default function NotFound() {
|
|
6
4
|
return (
|
|
@@ -8,13 +6,12 @@ export default function NotFound() {
|
|
|
8
6
|
<div className="text-center">
|
|
9
7
|
<h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
|
|
10
8
|
<p className="text-xl text-gray-600 mb-8">Page not found</p>
|
|
11
|
-
<
|
|
9
|
+
<Link
|
|
12
10
|
href="/"
|
|
13
|
-
onClick={(e) => { e.preventDefault(); appNavigate('/'); }}
|
|
14
11
|
className="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-6 py-3 text-base font-medium text-white hover:bg-primary-700 transition-all"
|
|
15
12
|
>
|
|
16
13
|
Go Home
|
|
17
|
-
</
|
|
14
|
+
</Link>
|
|
18
15
|
</div>
|
|
19
16
|
</div>
|
|
20
17
|
);
|
|
@@ -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
|
-
|
|
6
5
|
import { Pricing } from '@/components/landing/Pricing';
|
|
7
6
|
import { CTA } from '@/components/landing/CTA';
|
|
8
7
|
import { Footer } from '@/components/shared/Footer';
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { DataTable } from '@varity-labs/ui-kit';
|
|
5
|
-
import { TaskStatusBadge, PriorityBadge } from '@varity-labs/ui-kit';
|
|
3
|
+
import Link from 'next/link';
|
|
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';
|
|
@@ -43,14 +42,13 @@ export function RecentActivity({ tasks, loading = false }: RecentActivityProps)
|
|
|
43
42
|
Recent Activity
|
|
44
43
|
</h3>
|
|
45
44
|
{tasks.length > 5 && (
|
|
46
|
-
<
|
|
47
|
-
href="/dashboard/tasks
|
|
48
|
-
onClick={(e) => { e.preventDefault(); appNavigate('/dashboard/tasks/'); }}
|
|
45
|
+
<Link
|
|
46
|
+
href="/dashboard/tasks"
|
|
49
47
|
className="flex items-center gap-1 text-sm font-medium text-primary-600 hover:text-primary-700 transition-colors"
|
|
50
48
|
>
|
|
51
49
|
View all tasks
|
|
52
50
|
<ArrowRight className="h-4 w-4" />
|
|
53
|
-
</
|
|
51
|
+
</Link>
|
|
54
52
|
)}
|
|
55
53
|
</div>
|
|
56
54
|
<DataTable
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { appNavigate } from '@/lib/utils';
|
|
1
|
+
import Link from 'next/link';
|
|
4
2
|
import { ArrowRight, CheckCircle2 } from 'lucide-react';
|
|
5
3
|
import { APP_NAME } from '@/lib/constants';
|
|
6
4
|
|
|
@@ -30,14 +28,13 @@ export function CTA() {
|
|
|
30
28
|
))}
|
|
31
29
|
</div>
|
|
32
30
|
<div className="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
|
|
33
|
-
<
|
|
34
|
-
href="/login
|
|
35
|
-
onClick={(e) => { e.preventDefault(); appNavigate('/login/'); }}
|
|
31
|
+
<Link
|
|
32
|
+
href="/login"
|
|
36
33
|
className="group inline-flex items-center gap-2 rounded-lg bg-white px-6 py-3 text-base font-medium text-gray-900 shadow-lg hover:bg-gray-100 transition-all"
|
|
37
34
|
>
|
|
38
35
|
Get Started Free
|
|
39
36
|
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
|
|
40
|
-
</
|
|
37
|
+
</Link>
|
|
41
38
|
</div>
|
|
42
39
|
</div>
|
|
43
40
|
</section>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import Link from 'next/link';
|
|
4
4
|
import { ArrowRight, BarChart3, Shield, Zap, CheckCircle2 } from 'lucide-react';
|
|
5
5
|
import { APP_NAME } from '@/lib/constants';
|
|
6
6
|
|
|
@@ -97,14 +97,13 @@ export function Hero() {
|
|
|
97
97
|
Stop juggling spreadsheets and start delivering on time.
|
|
98
98
|
</p>
|
|
99
99
|
<div className="animate-fade-in-up-delay-2 mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
|
|
100
|
-
<
|
|
101
|
-
href="/login
|
|
102
|
-
onClick={(e) => { e.preventDefault(); appNavigate('/login/'); }}
|
|
100
|
+
<Link
|
|
101
|
+
href="/login"
|
|
103
102
|
className="group inline-flex items-center gap-2 rounded-lg bg-primary-600 px-6 py-3 text-base font-medium text-white shadow-lg shadow-primary-200 hover:bg-primary-700 hover:shadow-xl hover:shadow-primary-200/50 transition-all"
|
|
104
103
|
>
|
|
105
104
|
Start Free
|
|
106
105
|
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
|
|
107
|
-
</
|
|
106
|
+
</Link>
|
|
108
107
|
<a
|
|
109
108
|
href="#how-it-works"
|
|
110
109
|
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-6 py-3 text-base font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-400 transition-all"
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { appNavigate } from '@/lib/utils';
|
|
1
|
+
import Link from 'next/link';
|
|
4
2
|
import { Check } from 'lucide-react';
|
|
5
3
|
|
|
6
4
|
const plans = [
|
|
@@ -55,6 +53,12 @@ const plans = [
|
|
|
55
53
|
},
|
|
56
54
|
];
|
|
57
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
|
+
|
|
58
62
|
export function Pricing() {
|
|
59
63
|
return (
|
|
60
64
|
<section id="pricing" className="py-24 bg-white">
|
|
@@ -105,19 +109,12 @@ export function Pricing() {
|
|
|
105
109
|
</ul>
|
|
106
110
|
|
|
107
111
|
<div className="mt-8">
|
|
108
|
-
<
|
|
109
|
-
href="/login
|
|
110
|
-
|
|
111
|
-
className={`block w-full rounded-lg py-3 text-center font-medium transition-colors ${
|
|
112
|
-
plan.popular
|
|
113
|
-
? 'bg-primary-600 text-white hover:bg-primary-700'
|
|
114
|
-
: plan.price === 0
|
|
115
|
-
? 'bg-gray-100 text-gray-900 hover:bg-gray-200'
|
|
116
|
-
: 'bg-gray-900 text-white hover:bg-gray-800'
|
|
117
|
-
}`}
|
|
112
|
+
<Link
|
|
113
|
+
href="/login"
|
|
114
|
+
className={`block w-full rounded-lg py-3 text-center font-medium transition-colors ${buttonStyle(plan)}`}
|
|
118
115
|
>
|
|
119
116
|
{plan.cta}
|
|
120
|
-
</
|
|
117
|
+
</Link>
|
|
121
118
|
</div>
|
|
122
119
|
</div>
|
|
123
120
|
))}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { appNavigate } from '@/lib/utils';
|
|
1
|
+
import Link from 'next/link';
|
|
4
2
|
import { CheckCircle } from 'lucide-react';
|
|
5
3
|
import { APP_NAME } from '@/lib/constants';
|
|
6
4
|
|
|
@@ -25,9 +23,9 @@ export function Footer() {
|
|
|
25
23
|
<a href="#pricing" className="hover:text-gray-700 transition-colors">
|
|
26
24
|
Pricing
|
|
27
25
|
</a>
|
|
28
|
-
<
|
|
26
|
+
<Link href="/login" className="hover:text-gray-700 transition-colors">
|
|
29
27
|
Sign In
|
|
30
|
-
</
|
|
28
|
+
</Link>
|
|
31
29
|
</div>
|
|
32
30
|
</div>
|
|
33
31
|
<div className="mt-8 border-t border-gray-100 pt-6 text-center text-sm text-gray-400">
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
4
5
|
import { CheckCircle, Menu, X } from 'lucide-react';
|
|
5
6
|
import { APP_NAME } from '@/lib/constants';
|
|
6
|
-
import { appNavigate } from '@/lib/utils';
|
|
7
7
|
|
|
8
8
|
const navLinks = [
|
|
9
9
|
{ label: 'Features', href: '#features' },
|
|
@@ -18,10 +18,10 @@ export function Navbar() {
|
|
|
18
18
|
<nav className="sticky top-0 z-40 border-b border-gray-200 bg-white/80 backdrop-blur-md">
|
|
19
19
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
20
20
|
<div className="flex h-16 items-center justify-between">
|
|
21
|
-
<
|
|
21
|
+
<Link href="/" className="flex items-center gap-2">
|
|
22
22
|
<CheckCircle className="h-7 w-7 text-primary-600" />
|
|
23
23
|
<span className="text-xl font-bold text-gray-900">{APP_NAME}</span>
|
|
24
|
-
</
|
|
24
|
+
</Link>
|
|
25
25
|
<div className="hidden items-center gap-8 sm:flex">
|
|
26
26
|
{navLinks.map((link) => (
|
|
27
27
|
<a
|
|
@@ -34,21 +34,18 @@ export function Navbar() {
|
|
|
34
34
|
))}
|
|
35
35
|
</div>
|
|
36
36
|
<div className="flex items-center gap-3">
|
|
37
|
-
<
|
|
38
|
-
href="/login
|
|
39
|
-
onClick={(e) => { e.preventDefault(); appNavigate('/login/'); }}
|
|
37
|
+
<Link
|
|
38
|
+
href="/login"
|
|
40
39
|
className="hidden text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors sm:block"
|
|
41
40
|
>
|
|
42
41
|
Sign In
|
|
43
|
-
</
|
|
44
|
-
<
|
|
45
|
-
href="/login
|
|
46
|
-
onClick={(e) => { e.preventDefault(); appNavigate('/login/'); }}
|
|
42
|
+
</Link>
|
|
43
|
+
<Link
|
|
44
|
+
href="/login"
|
|
47
45
|
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700 transition-colors"
|
|
48
46
|
>
|
|
49
47
|
Get Started
|
|
50
|
-
</
|
|
51
|
-
{/* Mobile menu toggle */}
|
|
48
|
+
</Link>
|
|
52
49
|
<button
|
|
53
50
|
onClick={() => setMobileOpen(!mobileOpen)}
|
|
54
51
|
className="rounded-lg p-2 text-gray-600 hover:bg-gray-100 sm:hidden"
|
|
@@ -60,7 +57,6 @@ export function Navbar() {
|
|
|
60
57
|
</div>
|
|
61
58
|
</div>
|
|
62
59
|
|
|
63
|
-
{/* Mobile dropdown */}
|
|
64
60
|
{mobileOpen && (
|
|
65
61
|
<div className="border-t border-gray-100 bg-white px-4 pb-4 pt-2 sm:hidden">
|
|
66
62
|
<div className="space-y-1">
|
|
@@ -74,13 +70,13 @@ export function Navbar() {
|
|
|
74
70
|
{link.label}
|
|
75
71
|
</a>
|
|
76
72
|
))}
|
|
77
|
-
<
|
|
78
|
-
href="/login
|
|
79
|
-
onClick={(
|
|
73
|
+
<Link
|
|
74
|
+
href="/login"
|
|
75
|
+
onClick={() => setMobileOpen(false)}
|
|
80
76
|
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors"
|
|
81
77
|
>
|
|
82
78
|
Sign In
|
|
83
|
-
</
|
|
79
|
+
</Link>
|
|
84
80
|
</div>
|
|
85
81
|
</div>
|
|
86
82
|
)}
|
|
@@ -44,27 +44,6 @@ export function cn(...classes: (string | false | undefined | null)[]): string {
|
|
|
44
44
|
return classes.filter(Boolean).join(' ');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
* Detect the app's base path at runtime from the current URL.
|
|
49
|
-
* Works on any host: localhost, varity.app/saas-template, ipfs.io/ipfs/QmX, etc.
|
|
50
|
-
*/
|
|
51
|
-
export function getAppBase(): string {
|
|
52
|
-
if (typeof window === 'undefined') return '';
|
|
53
|
-
const path = window.location.pathname;
|
|
54
|
-
const routes = ['/login', '/dashboard/projects', '/dashboard/settings', '/dashboard/tasks', '/dashboard/team', '/dashboard'];
|
|
55
|
-
for (const route of routes) {
|
|
56
|
-
const idx = path.indexOf(route);
|
|
57
|
-
if (idx > 0) return path.substring(0, idx);
|
|
58
|
-
}
|
|
59
|
-
// On landing page — pathname is the base (strip trailing slash)
|
|
60
|
-
return path.replace(/\/+$/, '');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Navigate to an absolute app path (e.g. '/login/', '/dashboard/'). */
|
|
64
|
-
export function appNavigate(absolutePath: string): void {
|
|
65
|
-
window.location.href = getAppBase() + absolutePath;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
47
|
export function downloadCSV(data: Record<string, unknown>[], filename: string) {
|
|
69
48
|
if (data.length === 0) return;
|
|
70
49
|
const headers = Object.keys(data[0]);
|
|
@@ -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
|
-
}
|