create-varity-app 2.0.0-beta.10 → 2.0.0-beta.12
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 +13 -13
- package/template/src/app/dashboard/page.tsx +14 -11
- package/template/src/app/dashboard/projects/page.tsx +4 -4
- package/template/src/app/dashboard/tasks/page.tsx +4 -4
- package/template/src/app/dashboard/team/page.tsx +4 -4
- package/template/src/app/layout.tsx +1 -4
- package/template/src/app/login/page.tsx +7 -5
- package/template/src/app/not-found.tsx +3 -4
- package/template/src/app/page.tsx +2 -2
- package/template/src/components/dashboard/RecentActivity.tsx +4 -5
- 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 +1 -5
- package/template/src/components/shared/Footer.tsx +3 -5
- package/template/src/components/shared/Navbar.tsx +13 -15
- package/template/src/lib/hooks.ts +4 -16
- package/template/src/lib/utils.ts +0 -21
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
|
|
|
@@ -114,6 +114,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
114
114
|
const privy = usePrivyHook ? usePrivyHook() : { user: null, logout: async () => {} };
|
|
115
115
|
const { user, logout } = privy;
|
|
116
116
|
const pathname = usePathname();
|
|
117
|
+
const router = useRouter();
|
|
117
118
|
const isMobile = useIsMobile();
|
|
118
119
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
119
120
|
const [commandPaletteOpen, setCommandPaletteOpen] = 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
|
+
router.push('/');
|
|
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,9 @@ 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
260
|
onLogout={handleLogout}
|
|
261
|
-
onNavigateToProfile={() =>
|
|
262
|
-
onNavigateToSettings={() =>
|
|
261
|
+
onNavigateToProfile={() => router.push('/dashboard/settings')}
|
|
262
|
+
onNavigateToSettings={() => router.push('/dashboard/settings')}
|
|
263
263
|
onSearchClick={() => setCommandPaletteOpen(true)}
|
|
264
264
|
searchPlaceholder="Search projects, tasks, team..."
|
|
265
265
|
>
|
|
@@ -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,8 @@ 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
13
|
const actions = [
|
|
13
14
|
{
|
|
14
15
|
label: 'New Project',
|
|
@@ -48,7 +49,7 @@ function QuickActions() {
|
|
|
48
49
|
{actions.map((action) => (
|
|
49
50
|
<button
|
|
50
51
|
key={action.path}
|
|
51
|
-
onClick={() =>
|
|
52
|
+
onClick={() => router.push(action.path)}
|
|
52
53
|
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
54
|
>
|
|
54
55
|
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${action.color}`}>
|
|
@@ -73,7 +74,8 @@ function GettingStarted({
|
|
|
73
74
|
hasTasks: boolean;
|
|
74
75
|
hasTeam: boolean;
|
|
75
76
|
}) {
|
|
76
|
-
|
|
77
|
+
const router = useRouter();
|
|
78
|
+
const completedCount = [hasProjects, hasTasks, hasTeam].filter(Boolean).length;
|
|
77
79
|
|
|
78
80
|
if (completedCount === 3) return null;
|
|
79
81
|
|
|
@@ -111,7 +113,7 @@ function GettingStarted({
|
|
|
111
113
|
{steps.map((step) => (
|
|
112
114
|
<button
|
|
113
115
|
key={step.label}
|
|
114
|
-
onClick={() => !step.done &&
|
|
116
|
+
onClick={() => !step.done && router.push(step.path)}
|
|
115
117
|
disabled={step.done}
|
|
116
118
|
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors ${
|
|
117
119
|
step.done
|
|
@@ -136,7 +138,8 @@ function GettingStarted({
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
export default function DashboardPage() {
|
|
139
|
-
|
|
141
|
+
const router = useRouter();
|
|
142
|
+
const { name } = useCurrentUser();
|
|
140
143
|
const { data: projects, loading: projectsLoading, error: projectsError, refresh: refreshProjects } = useProjects();
|
|
141
144
|
const { data: tasks, loading: tasksLoading, error: tasksError, refresh: refreshTasks } = useTasks();
|
|
142
145
|
const { data: team, loading: teamLoading, error: teamError, refresh: refreshTeam } = useTeam();
|
|
@@ -160,14 +163,14 @@ export default function DashboardPage() {
|
|
|
160
163
|
</p>
|
|
161
164
|
</div>
|
|
162
165
|
|
|
163
|
-
{error &&
|
|
164
|
-
<div className="flex items-center justify-between rounded-lg border border-
|
|
165
|
-
<p className="text-sm text-
|
|
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>
|
|
166
169
|
<button
|
|
167
170
|
onClick={() => { refreshProjects(); refreshTasks(); refreshTeam(); }}
|
|
168
|
-
className="text-sm font-medium text-
|
|
171
|
+
className="text-sm font-medium text-red-700 hover:text-red-800 underline"
|
|
169
172
|
>
|
|
170
|
-
|
|
173
|
+
Retry
|
|
171
174
|
</button>
|
|
172
175
|
</div>
|
|
173
176
|
)}
|
|
@@ -606,10 +606,10 @@ export default function ProjectsPage() {
|
|
|
606
606
|
</div>
|
|
607
607
|
</Dialog>
|
|
608
608
|
|
|
609
|
-
{error &&
|
|
610
|
-
<div className="flex items-center justify-between rounded-lg border border-
|
|
611
|
-
<p className="text-sm text-
|
|
612
|
-
<button onClick={refresh} className="text-sm font-medium text-
|
|
609
|
+
{error && (
|
|
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 &&
|
|
262
|
-
<div className="flex items-center justify-between rounded-lg border border-
|
|
263
|
-
<p className="text-sm text-
|
|
264
|
-
<button onClick={refresh} className="text-sm font-medium text-
|
|
261
|
+
{error && (
|
|
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 &&
|
|
250
|
-
<div className="flex items-center justify-between rounded-lg border border-
|
|
251
|
-
<p className="text-sm text-
|
|
252
|
-
<button onClick={refresh} className="text-sm font-medium text-
|
|
249
|
+
{error && (
|
|
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
|
|
|
@@ -5,7 +5,7 @@ import './globals.css';
|
|
|
5
5
|
export const metadata: Metadata = {
|
|
6
6
|
title: 'TaskFlow - Project Management',
|
|
7
7
|
description: 'Manage projects, track tasks, and collaborate with your team.',
|
|
8
|
-
metadataBase: new URL('https://
|
|
8
|
+
metadataBase: new URL('https://example.com'),
|
|
9
9
|
openGraph: {
|
|
10
10
|
title: 'TaskFlow - Project Management',
|
|
11
11
|
description: 'Manage projects, track tasks, and collaborate with your team.',
|
|
@@ -25,9 +25,6 @@ export default function RootLayout({
|
|
|
25
25
|
}) {
|
|
26
26
|
return (
|
|
27
27
|
<html lang="en">
|
|
28
|
-
<head>
|
|
29
|
-
<base href="./" />
|
|
30
|
-
</head>
|
|
31
28
|
<body>
|
|
32
29
|
<Providers>{children}</Providers>
|
|
33
30
|
</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
|
|
|
@@ -15,14 +16,15 @@ try {
|
|
|
15
16
|
} catch {}
|
|
16
17
|
|
|
17
18
|
function LoginContent() {
|
|
19
|
+
const router = useRouter();
|
|
18
20
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
19
21
|
const privy = usePrivyHook ? usePrivyHook() : null;
|
|
20
22
|
|
|
21
23
|
useEffect(() => {
|
|
22
24
|
if (privy?.authenticated) {
|
|
23
|
-
|
|
25
|
+
router.push('/dashboard');
|
|
24
26
|
}
|
|
25
|
-
}, [privy?.authenticated]);
|
|
27
|
+
}, [privy?.authenticated, router]);
|
|
26
28
|
|
|
27
29
|
const handleLogin = () => {
|
|
28
30
|
if (privy?.login) {
|
|
@@ -34,10 +36,10 @@ function LoginContent() {
|
|
|
34
36
|
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4">
|
|
35
37
|
<div className="w-full max-w-md space-y-8">
|
|
36
38
|
<div className="text-center">
|
|
37
|
-
<
|
|
39
|
+
<Link href="/" className="inline-flex items-center gap-2">
|
|
38
40
|
<CheckCircle className="h-8 w-8 text-primary-600" />
|
|
39
41
|
<span className="text-2xl font-bold text-gray-900">{APP_NAME}</span>
|
|
40
|
-
</
|
|
42
|
+
</Link>
|
|
41
43
|
<h2 className="mt-6 text-2xl font-bold text-gray-900">
|
|
42
44
|
Sign in to your account
|
|
43
45
|
</h2>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import Link from 'next/link';
|
|
4
4
|
|
|
5
5
|
export default function NotFound() {
|
|
6
6
|
return (
|
|
@@ -8,13 +8,12 @@ export default function NotFound() {
|
|
|
8
8
|
<div className="text-center">
|
|
9
9
|
<h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
|
|
10
10
|
<p className="text-xl text-gray-600 mb-8">Page not found</p>
|
|
11
|
-
<
|
|
11
|
+
<Link
|
|
12
12
|
href="/"
|
|
13
|
-
onClick={(e) => { e.preventDefault(); appNavigate('/'); }}
|
|
14
13
|
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
14
|
>
|
|
16
15
|
Go Home
|
|
17
|
-
</
|
|
16
|
+
</Link>
|
|
18
17
|
</div>
|
|
19
18
|
</div>
|
|
20
19
|
);
|
|
@@ -2,7 +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
|
-
|
|
5
|
+
import { Testimonials } from '@/components/landing/Testimonials';
|
|
6
6
|
import { Pricing } from '@/components/landing/Pricing';
|
|
7
7
|
import { CTA } from '@/components/landing/CTA';
|
|
8
8
|
import { Footer } from '@/components/shared/Footer';
|
|
@@ -14,7 +14,7 @@ export default function HomePage() {
|
|
|
14
14
|
<Hero />
|
|
15
15
|
<Features />
|
|
16
16
|
<HowItWorks />
|
|
17
|
-
|
|
17
|
+
<Testimonials />
|
|
18
18
|
<Pricing />
|
|
19
19
|
<CTA />
|
|
20
20
|
<Footer />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import Link from 'next/link';
|
|
4
4
|
import { DataTable } from '@varity-labs/ui-kit';
|
|
5
5
|
import { TaskStatusBadge, PriorityBadge } from '@varity-labs/ui-kit';
|
|
6
6
|
import { formatRelativeDate } from '@/lib/utils';
|
|
@@ -43,14 +43,13 @@ export function RecentActivity({ tasks, loading = false }: RecentActivityProps)
|
|
|
43
43
|
Recent Activity
|
|
44
44
|
</h3>
|
|
45
45
|
{tasks.length > 5 && (
|
|
46
|
-
<
|
|
47
|
-
href="/dashboard/tasks
|
|
48
|
-
onClick={(e) => { e.preventDefault(); appNavigate('/dashboard/tasks/'); }}
|
|
46
|
+
<Link
|
|
47
|
+
href="/dashboard/tasks"
|
|
49
48
|
className="flex items-center gap-1 text-sm font-medium text-primary-600 hover:text-primary-700 transition-colors"
|
|
50
49
|
>
|
|
51
50
|
View all tasks
|
|
52
51
|
<ArrowRight className="h-4 w-4" />
|
|
53
|
-
</
|
|
52
|
+
</Link>
|
|
54
53
|
)}
|
|
55
54
|
</div>
|
|
56
55
|
<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,3 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { appNavigate } from '@/lib/utils';
|
|
4
1
|
import { Check } from 'lucide-react';
|
|
5
2
|
|
|
6
3
|
const plans = [
|
|
@@ -106,8 +103,7 @@ export function Pricing() {
|
|
|
106
103
|
|
|
107
104
|
<div className="mt-8">
|
|
108
105
|
<a
|
|
109
|
-
href="/login
|
|
110
|
-
onClick={(e) => { e.preventDefault(); appNavigate('/login/'); }}
|
|
106
|
+
href="/login"
|
|
111
107
|
className={`block w-full rounded-lg py-3 text-center font-medium transition-colors ${
|
|
112
108
|
plan.popular
|
|
113
109
|
? 'bg-primary-600 text-white hover:bg-primary-700'
|
|
@@ -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,20 +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
|
-
</
|
|
48
|
+
</Link>
|
|
51
49
|
{/* Mobile menu toggle */}
|
|
52
50
|
<button
|
|
53
51
|
onClick={() => setMobileOpen(!mobileOpen)}
|
|
@@ -74,13 +72,13 @@ export function Navbar() {
|
|
|
74
72
|
{link.label}
|
|
75
73
|
</a>
|
|
76
74
|
))}
|
|
77
|
-
<
|
|
78
|
-
href="/login
|
|
79
|
-
onClick={(
|
|
75
|
+
<Link
|
|
76
|
+
href="/login"
|
|
77
|
+
onClick={() => setMobileOpen(false)}
|
|
80
78
|
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
79
|
>
|
|
82
80
|
Sign In
|
|
83
|
-
</
|
|
81
|
+
</Link>
|
|
84
82
|
</div>
|
|
85
83
|
</div>
|
|
86
84
|
)}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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);
|
|
@@ -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]);
|