create-varity-app 2.0.0-beta.11 → 2.0.0-beta.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-varity-app",
3
- "version": "2.0.0-beta.11",
3
+ "version": "2.0.0-beta.13",
4
4
  "description": "Create production-ready apps with auth, database, and payments built in",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -3,6 +3,7 @@ const nextConfig = {
3
3
  output: 'export',
4
4
  images: { unoptimized: true },
5
5
  trailingSlash: true,
6
+ basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
6
7
  productionBrowserSourceMaps: false,
7
8
  webpack: (config, { isServer, dev }) => {
8
9
  // Suppress MetaMask SDK warning for @react-native-async-storage
@@ -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 router = useRouter();
117
116
  const pathname = usePathname();
117
+ const router = useRouter();
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
- window.location.href = '/';
157
+ router.push('/');
158
158
  };
159
159
 
160
160
  // Fallback layout when DashboardLayout from ui-kit isn't available
@@ -257,7 +257,6 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
257
257
  name: userName,
258
258
  address: userEmail,
259
259
  }}
260
- onNavigate={(path: string) => router.push(path)}
261
260
  onLogout={handleLogout}
262
261
  onNavigateToProfile={() => router.push('/dashboard/settings')}
263
262
  onNavigateToSettings={() => router.push('/dashboard/settings')}
@@ -289,7 +288,7 @@ export default function DashboardRootLayout({
289
288
  appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
290
289
  thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
291
290
  loginMethods={['email', 'google']}
292
- appearance={{ theme: 'light', accentColor: '#2563EB' }}
291
+ appearance={{ theme: 'light', accentColor: '#2563EB', logo: '/logo.svg' }}
293
292
  >
294
293
  <PrivyProtectedRoute fallback={<RedirectToLogin />}>
295
294
  <DashboardShell>{children}</DashboardShell>
@@ -9,6 +9,7 @@ import { FolderKanban, ListTodo, Users, ArrowRight } from 'lucide-react';
9
9
 
10
10
  function QuickActions() {
11
11
  const router = useRouter();
12
+
12
13
  const actions = [
13
14
  {
14
15
  label: 'New Project',
@@ -137,6 +138,7 @@ function GettingStarted({
137
138
  }
138
139
 
139
140
  export default function DashboardPage() {
141
+ const router = useRouter();
140
142
  const { name } = useCurrentUser();
141
143
  const { data: projects, loading: projectsLoading, error: projectsError, refresh: refreshProjects } = useProjects();
142
144
  const { data: tasks, loading: tasksLoading, error: tasksError, refresh: refreshTasks } = useTasks();
@@ -161,14 +163,14 @@ export default function DashboardPage() {
161
163
  </p>
162
164
  </div>
163
165
 
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>
166
+ {error && (
167
+ <div className="flex items-center justify-between rounded-lg border border-red-200 bg-red-50 px-4 py-3">
168
+ <p className="text-sm text-red-700">Failed to load data. Please check your connection and try again.</p>
167
169
  <button
168
170
  onClick={() => { refreshProjects(); refreshTasks(); refreshTeam(); }}
169
- className="text-sm font-medium text-amber-700 hover:text-amber-800 underline"
171
+ className="text-sm font-medium text-red-700 hover:text-red-800 underline"
170
172
  >
171
- Refresh
173
+ Retry
172
174
  </button>
173
175
  </div>
174
176
  )}
@@ -606,10 +606,10 @@ export default function ProjectsPage() {
606
606
  </div>
607
607
  </Dialog>
608
608
 
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>
609
+ {error && (
610
+ <div className="flex items-center justify-between rounded-lg border border-red-200 bg-red-50 px-4 py-3">
611
+ <p className="text-sm text-red-700">Failed to load projects. Please check your connection and try again.</p>
612
+ <button onClick={refresh} className="text-sm font-medium text-red-700 hover:text-red-800 underline">Retry</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 && !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>
261
+ {error && (
262
+ <div className="flex items-center justify-between rounded-lg border border-red-200 bg-red-50 px-4 py-3">
263
+ <p className="text-sm text-red-700">Failed to load tasks. Please check your connection and try again.</p>
264
+ <button onClick={refresh} className="text-sm font-medium text-red-700 hover:text-red-800 underline">Retry</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 && !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>
249
+ {error && (
250
+ <div className="flex items-center justify-between rounded-lg border border-red-200 bg-red-50 px-4 py-3">
251
+ <p className="text-sm text-red-700">Failed to load team data. Please check your connection and try again.</p>
252
+ <button onClick={refresh} className="text-sm font-medium text-red-700 hover:text-red-800 underline">Retry</button>
253
253
  </div>
254
254
  )}
255
255
 
@@ -1,23 +1,20 @@
1
1
  import type { Metadata } from 'next';
2
2
  import { Providers } from '@/components/providers';
3
- import { APP_NAME } from '@/lib/constants';
4
3
  import './globals.css';
5
4
 
6
- const description = 'Built with Varity — auth, database, and deployment included.';
7
-
8
5
  export const metadata: Metadata = {
9
- title: APP_NAME,
10
- description,
11
- metadataBase: new URL('https://varity.app'),
6
+ title: 'TaskFlow - Project Management',
7
+ description: 'Manage projects, track tasks, and collaborate with your team.',
8
+ metadataBase: new URL('https://example.com'),
12
9
  openGraph: {
13
- title: APP_NAME,
14
- description,
10
+ title: 'TaskFlow - Project Management',
11
+ description: 'Manage projects, track tasks, and collaborate with your team.',
15
12
  type: 'website',
16
13
  },
17
14
  twitter: {
18
15
  card: 'summary',
19
- title: APP_NAME,
20
- description,
16
+ title: 'TaskFlow - Project Management',
17
+ description: 'Manage projects, track tasks, and collaborate with your team.',
21
18
  },
22
19
  };
23
20
 
@@ -15,12 +15,6 @@ 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
-
24
18
  function LoginContent() {
25
19
  const router = useRouter();
26
20
  // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
@@ -61,7 +55,11 @@ function LoginContent() {
61
55
  disabled={!privy.ready || privy.authenticated}
62
56
  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"
63
57
  >
64
- {loginButtonLabel(privy)}
58
+ {!privy.ready
59
+ ? 'Loading...'
60
+ : privy.authenticated
61
+ ? 'Already Signed In'
62
+ : 'Sign In with Email or Social'}
65
63
  </button>
66
64
  ) : (
67
65
  <div className="text-center space-y-4">
@@ -81,18 +79,20 @@ function LoginContent() {
81
79
  }
82
80
 
83
81
  export default function LoginPage() {
82
+ // Always wrap in PrivyStack - it uses dev credentials automatically when no appId is provided
84
83
  if (PrivyStackComponent) {
85
84
  return (
86
85
  <PrivyStackComponent
87
86
  appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
88
87
  thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
89
88
  loginMethods={['email', 'google']}
90
- appearance={{ theme: 'light', accentColor: '#2563EB' }}
89
+ appearance={{ theme: 'light', accentColor: '#2563EB', logo: '/logo.svg' }}
91
90
  >
92
91
  <LoginContent />
93
92
  </PrivyStackComponent>
94
93
  );
95
94
  }
96
95
 
96
+ // Fallback if ui-kit package isn't installed
97
97
  return <LoginContent />;
98
98
  }
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import Link from 'next/link';
2
4
 
3
5
  export default function NotFound() {
@@ -2,6 +2,7 @@ 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';
5
6
  import { Pricing } from '@/components/landing/Pricing';
6
7
  import { CTA } from '@/components/landing/CTA';
7
8
  import { Footer } from '@/components/shared/Footer';
@@ -13,7 +14,7 @@ export default function HomePage() {
13
14
  <Hero />
14
15
  <Features />
15
16
  <HowItWorks />
16
-
17
+ <Testimonials />
17
18
  <Pricing />
18
19
  <CTA />
19
20
  <Footer />
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import Link from 'next/link';
4
- import { DataTable, TaskStatusBadge, PriorityBadge } from '@varity-labs/ui-kit';
4
+ import { DataTable } from '@varity-labs/ui-kit';
5
+ import { TaskStatusBadge, PriorityBadge } from '@varity-labs/ui-kit';
5
6
  import { formatRelativeDate } from '@/lib/utils';
6
7
  import { ArrowRight } from 'lucide-react';
7
8
  import type { Task } from '@/types';
@@ -1,4 +1,3 @@
1
- import Link from 'next/link';
2
1
  import { Check } from 'lucide-react';
3
2
 
4
3
  const plans = [
@@ -53,12 +52,6 @@ const plans = [
53
52
  },
54
53
  ];
55
54
 
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
-
62
55
  export function Pricing() {
63
56
  return (
64
57
  <section id="pricing" className="py-24 bg-white">
@@ -109,12 +102,18 @@ export function Pricing() {
109
102
  </ul>
110
103
 
111
104
  <div className="mt-8">
112
- <Link
105
+ <a
113
106
  href="/login"
114
- className={`block w-full rounded-lg py-3 text-center font-medium transition-colors ${buttonStyle(plan)}`}
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
+ }`}
115
114
  >
116
115
  {plan.cta}
117
- </Link>
116
+ </a>
118
117
  </div>
119
118
  </div>
120
119
  ))}
@@ -46,6 +46,7 @@ export function Navbar() {
46
46
  >
47
47
  Get Started
48
48
  </Link>
49
+ {/* Mobile menu toggle */}
49
50
  <button
50
51
  onClick={() => setMobileOpen(!mobileOpen)}
51
52
  className="rounded-lg p-2 text-gray-600 hover:bg-gray-100 sm:hidden"
@@ -57,6 +58,7 @@ export function Navbar() {
57
58
  </div>
58
59
  </div>
59
60
 
61
+ {/* Mobile dropdown */}
60
62
  {mobileOpen && (
61
63
  <div className="border-t border-gray-100 bg-white px-4 pb-4 pt-2 sm:hidden">
62
64
  <div className="space-y-1">
@@ -1,6 +1,6 @@
1
1
  import type { NavigationItem } from '@varity-labs/ui-kit';
2
2
 
3
- export const APP_NAME = 'My App';
3
+ export const APP_NAME = 'TaskFlow';
4
4
 
5
5
  export const NAVIGATION_ITEMS: NavigationItem[] = [
6
6
  { label: 'Dashboard', icon: 'dashboard', path: '/dashboard' },
@@ -10,18 +10,6 @@ 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
-
25
13
  export function useCurrentUser() {
26
14
  // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
27
15
  const privy = usePrivyHook ? usePrivyHook() : { user: null, authenticated: false, logout: async () => {} };
@@ -66,7 +54,7 @@ export function useProjects(): UseCollectionReturn<Project> {
66
54
  try {
67
55
  setLoading(true);
68
56
  setError(null);
69
- const result = await fetchWithRetry(() => projects().get());
57
+ const result = await projects().get();
70
58
  setData(result as Project[]);
71
59
  } catch (err) {
72
60
  setError(err instanceof Error ? err.message : 'Failed to load projects');
@@ -138,7 +126,7 @@ export function useTasks(projectId?: string): UseCollectionReturn<Task> {
138
126
  try {
139
127
  setLoading(true);
140
128
  setError(null);
141
- const result = await fetchWithRetry(() => tasks().get());
129
+ const result = await tasks().get();
142
130
  setAllTasks(result as Task[]);
143
131
  } catch (err) {
144
132
  setError(err instanceof Error ? err.message : 'Failed to load tasks');
@@ -214,7 +202,7 @@ export function useTeam(): UseCollectionReturn<TeamMember> {
214
202
  try {
215
203
  setLoading(true);
216
204
  setError(null);
217
- const result = await fetchWithRetry(() => teamMembers().get());
205
+ const result = await teamMembers().get();
218
206
  setData(result as TeamMember[]);
219
207
  } catch (err) {
220
208
  setError(err instanceof Error ? err.message : 'Failed to load team');
@@ -301,7 +289,7 @@ export function useUserSettings() {
301
289
  try {
302
290
  setLoading(true);
303
291
  setError(null);
304
- const all = await fetchWithRetry(() => userSettings().get());
292
+ const all = await userSettings().get();
305
293
  const mine = (all as UserSettings[]).find((s) => s.user_id === userId);
306
294
  if (mine) {
307
295
  setSettings(mine);
@@ -0,0 +1,589 @@
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
+ }