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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-varity-app",
3
- "version": "2.0.0-beta.10",
3
+ "version": "2.0.0-beta.11",
4
4
  "description": "Create production-ready apps with auth, database, and payments built in",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- appNavigate('/login/');
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
- appNavigate('/');
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) => appNavigate(path.endsWith('/') ? path : path + '/')}
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={() => appNavigate(item.path + '/')}
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) => appNavigate(path.endsWith('/') ? path : 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) => appNavigate(path.endsWith('/') ? path : path + '/')}
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) => appNavigate(path.endsWith('/') ? path : 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) => appNavigate(path.endsWith('/') ? path : path + '/')}
260
+ onNavigate={(path: string) => router.push(path)}
260
261
  onLogout={handleLogout}
261
- onNavigateToProfile={() => appNavigate('/dashboard/settings/')}
262
- onNavigateToSettings={() => appNavigate('/dashboard/settings/')}
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', logo: '/logo.svg' }}
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 { appNavigate } from '@/lib/utils';
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={() => appNavigate(action.path + '/')}
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
- const completedCount = [hasProjects, hasTasks, hasTeam].filter(Boolean).length;
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 && appNavigate(step.path + '/')}
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
- const { name } = useCurrentUser();
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: 'TaskFlow - Project Management',
7
- description: 'Manage projects, track tasks, and collaborate with your team.',
9
+ title: APP_NAME,
10
+ description,
8
11
  metadataBase: new URL('https://varity.app'),
9
12
  openGraph: {
10
- title: 'TaskFlow - Project Management',
11
- description: 'Manage projects, track tasks, and collaborate with your team.',
13
+ title: APP_NAME,
14
+ description,
12
15
  type: 'website',
13
16
  },
14
17
  twitter: {
15
18
  card: 'summary',
16
- title: 'TaskFlow - Project Management',
17
- description: 'Manage projects, track tasks, and collaborate with your team.',
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 { appNavigate } from '@/lib/utils';
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
- appNavigate('/dashboard/');
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
- <a href="/" onClick={(e) => { e.preventDefault(); appNavigate('/'); }} className="inline-flex items-center gap-2">
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
- </a>
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
- {!privy.ready
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', logo: '/logo.svg' }}
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
- 'use client';
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
- <a
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
- </a>
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 { appNavigate } from '@/lib/utils';
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
- <a
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
- </a>
51
+ </Link>
54
52
  )}
55
53
  </div>
56
54
  <DataTable
@@ -1,6 +1,4 @@
1
- 'use client';
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
- <a
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
- </a>
37
+ </Link>
41
38
  </div>
42
39
  </div>
43
40
  </section>
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { appNavigate } from '@/lib/utils';
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
- <a
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
- </a>
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
- 'use client';
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
- <a
109
- href="/login/"
110
- onClick={(e) => { e.preventDefault(); appNavigate('/login/'); }}
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
- </a>
117
+ </Link>
121
118
  </div>
122
119
  </div>
123
120
  ))}
@@ -1,6 +1,4 @@
1
- 'use client';
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
- <a href="/login/" onClick={(e) => { e.preventDefault(); appNavigate('/login/'); }} className="hover:text-gray-700 transition-colors">
26
+ <Link href="/login" className="hover:text-gray-700 transition-colors">
29
27
  Sign In
30
- </a>
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
- <a href="/" onClick={(e) => { e.preventDefault(); appNavigate('/'); }} className="flex items-center gap-2">
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
- </a>
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
- <a
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
- </a>
44
- <a
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
- </a>
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
- <a
78
- href="/login/"
79
- onClick={(e) => { e.preventDefault(); setMobileOpen(false); appNavigate('/login/'); }}
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
- </a>
79
+ </Link>
84
80
  </div>
85
81
  </div>
86
82
  )}
@@ -1,6 +1,6 @@
1
1
  import type { NavigationItem } from '@varity-labs/ui-kit';
2
2
 
3
- export const APP_NAME = 'TaskFlow';
3
+ export const APP_NAME = 'My App';
4
4
 
5
5
  export const NAVIGATION_ITEMS: NavigationItem[] = [
6
6
  { label: 'Dashboard', icon: 'dashboard', path: '/dashboard' },
@@ -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
- }