create-varity-app 2.0.0-beta.14 → 2.0.0-beta.16
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/next.config.js +0 -1
- package/template/package.json +1 -2
- package/template/src/app/dashboard/layout.tsx +12 -12
- package/template/src/app/dashboard/page.tsx +5 -7
- 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 +10 -7
- package/template/src/app/login/page.tsx +8 -9
- package/template/src/app/not-found.tsx +0 -2
- package/template/src/app/page.tsx +1 -2
- package/template/src/components/dashboard/RecentActivity.tsx +1 -2
- package/template/src/components/landing/Pricing.tsx +10 -9
- package/template/src/components/shared/Navbar.tsx +0 -2
- package/template/src/lib/constants.ts +1 -1
- package/template/src/lib/hooks.ts +16 -4
- package/template/src/lib/utils.ts +0 -15
package/package.json
CHANGED
package/template/next.config.js
CHANGED
|
@@ -3,7 +3,6 @@ const nextConfig = {
|
|
|
3
3
|
output: 'export',
|
|
4
4
|
images: { unoptimized: true },
|
|
5
5
|
trailingSlash: true,
|
|
6
|
-
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
|
|
7
6
|
productionBrowserSourceMaps: false,
|
|
8
7
|
webpack: (config, { isServer, dev }) => {
|
|
9
8
|
// Suppress MetaMask SDK warning for @react-native-async-storage
|
package/template/package.json
CHANGED
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"test:e2e:ui": "playwright test --ui",
|
|
14
14
|
"test:e2e:headed": "playwright test --headed",
|
|
15
15
|
"test:e2e:debug": "playwright test --debug",
|
|
16
|
-
"prepare": "husky install"
|
|
17
|
-
"deploy": "varitykit app deploy"
|
|
16
|
+
"prepare": "husky install"
|
|
18
17
|
},
|
|
19
18
|
"dependencies": {
|
|
20
19
|
"@varity-labs/sdk": "workspace:^",
|
|
@@ -4,7 +4,6 @@ import { useState, useEffect, useCallback } from 'react';
|
|
|
4
4
|
import { useRouter, usePathname } from 'next/navigation';
|
|
5
5
|
import { APP_NAME, NAVIGATION_ITEMS } from '@/lib/constants';
|
|
6
6
|
import { useProjects, useTasks, useTeam } from '@/lib/hooks';
|
|
7
|
-
import { withBasePath } from '@/lib/utils';
|
|
8
7
|
import { CommandPalette } from '@varity-labs/ui-kit';
|
|
9
8
|
import { Menu, X } from 'lucide-react';
|
|
10
9
|
|
|
@@ -25,7 +24,7 @@ try {
|
|
|
25
24
|
function RedirectToLogin() {
|
|
26
25
|
const router = useRouter();
|
|
27
26
|
useEffect(() => {
|
|
28
|
-
router.push(
|
|
27
|
+
router.push('/login');
|
|
29
28
|
}, [router]);
|
|
30
29
|
return null;
|
|
31
30
|
}
|
|
@@ -114,8 +113,8 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
114
113
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
115
114
|
const privy = usePrivyHook ? usePrivyHook() : { user: null, logout: async () => {} };
|
|
116
115
|
const { user, logout } = privy;
|
|
117
|
-
const pathname = usePathname();
|
|
118
116
|
const router = useRouter();
|
|
117
|
+
const pathname = usePathname();
|
|
119
118
|
const isMobile = useIsMobile();
|
|
120
119
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
121
120
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
@@ -155,7 +154,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
155
154
|
|
|
156
155
|
const handleLogout = async () => {
|
|
157
156
|
await logout();
|
|
158
|
-
window.location.href =
|
|
157
|
+
window.location.href = '/';
|
|
159
158
|
};
|
|
160
159
|
|
|
161
160
|
// Fallback layout when DashboardLayout from ui-kit isn't available
|
|
@@ -165,7 +164,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
165
164
|
<CommandPalette
|
|
166
165
|
open={commandPaletteOpen}
|
|
167
166
|
onClose={() => setCommandPaletteOpen(false)}
|
|
168
|
-
onNavigate={(path: string) => router.push(
|
|
167
|
+
onNavigate={(path: string) => router.push(path)}
|
|
169
168
|
projects={projects}
|
|
170
169
|
tasks={tasks}
|
|
171
170
|
team={team}
|
|
@@ -182,7 +181,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
182
181
|
{navWithActive.map((item) => (
|
|
183
182
|
<button
|
|
184
183
|
key={item.path}
|
|
185
|
-
onClick={() => router.push(
|
|
184
|
+
onClick={() => router.push(item.path)}
|
|
186
185
|
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
|
|
187
186
|
item.active
|
|
188
187
|
? 'bg-primary-50 text-primary-700'
|
|
@@ -212,7 +211,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
212
211
|
navItems={navWithActive}
|
|
213
212
|
userEmail={userEmail}
|
|
214
213
|
onLogout={handleLogout}
|
|
215
|
-
onNavigate={(path) => router.push(
|
|
214
|
+
onNavigate={(path) => router.push(path)}
|
|
216
215
|
/>
|
|
217
216
|
)}
|
|
218
217
|
|
|
@@ -229,7 +228,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
229
228
|
<CommandPalette
|
|
230
229
|
open={commandPaletteOpen}
|
|
231
230
|
onClose={() => setCommandPaletteOpen(false)}
|
|
232
|
-
onNavigate={(path: string) => router.push(
|
|
231
|
+
onNavigate={(path: string) => router.push(path)}
|
|
233
232
|
projects={projects}
|
|
234
233
|
tasks={tasks}
|
|
235
234
|
team={team}
|
|
@@ -243,7 +242,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
243
242
|
navItems={navWithActive}
|
|
244
243
|
userEmail={userEmail}
|
|
245
244
|
onLogout={handleLogout}
|
|
246
|
-
onNavigate={(path) => router.push(
|
|
245
|
+
onNavigate={(path) => router.push(path)}
|
|
247
246
|
/>
|
|
248
247
|
)}
|
|
249
248
|
|
|
@@ -258,9 +257,10 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
258
257
|
name: userName,
|
|
259
258
|
address: userEmail,
|
|
260
259
|
}}
|
|
260
|
+
onNavigate={(path: string) => router.push(path)}
|
|
261
261
|
onLogout={handleLogout}
|
|
262
|
-
onNavigateToProfile={() => router.push(
|
|
263
|
-
onNavigateToSettings={() => router.push(
|
|
262
|
+
onNavigateToProfile={() => router.push('/dashboard/settings')}
|
|
263
|
+
onNavigateToSettings={() => router.push('/dashboard/settings')}
|
|
264
264
|
onSearchClick={() => setCommandPaletteOpen(true)}
|
|
265
265
|
searchPlaceholder="Search projects, tasks, team..."
|
|
266
266
|
>
|
|
@@ -289,7 +289,7 @@ export default function DashboardRootLayout({
|
|
|
289
289
|
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
|
|
290
290
|
thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
|
|
291
291
|
loginMethods={['email', 'google']}
|
|
292
|
-
appearance={{ theme: 'light', accentColor: '#2563EB'
|
|
292
|
+
appearance={{ theme: 'light', accentColor: '#2563EB' }}
|
|
293
293
|
>
|
|
294
294
|
<PrivyProtectedRoute fallback={<RedirectToLogin />}>
|
|
295
295
|
<DashboardShell>{children}</DashboardShell>
|
|
@@ -9,7 +9,6 @@ import { FolderKanban, ListTodo, Users, ArrowRight } from 'lucide-react';
|
|
|
9
9
|
|
|
10
10
|
function QuickActions() {
|
|
11
11
|
const router = useRouter();
|
|
12
|
-
|
|
13
12
|
const actions = [
|
|
14
13
|
{
|
|
15
14
|
label: 'New Project',
|
|
@@ -138,7 +137,6 @@ function GettingStarted({
|
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
export default function DashboardPage() {
|
|
141
|
-
const router = useRouter();
|
|
142
140
|
const { name } = useCurrentUser();
|
|
143
141
|
const { data: projects, loading: projectsLoading, error: projectsError, refresh: refreshProjects } = useProjects();
|
|
144
142
|
const { data: tasks, loading: tasksLoading, error: tasksError, refresh: refreshTasks } = useTasks();
|
|
@@ -163,14 +161,14 @@ export default function DashboardPage() {
|
|
|
163
161
|
</p>
|
|
164
162
|
</div>
|
|
165
163
|
|
|
166
|
-
{error && (
|
|
167
|
-
<div className="flex items-center justify-between rounded-lg border border-
|
|
168
|
-
<p className="text-sm text-
|
|
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>
|
|
169
167
|
<button
|
|
170
168
|
onClick={() => { refreshProjects(); refreshTasks(); refreshTeam(); }}
|
|
171
|
-
className="text-sm font-medium text-
|
|
169
|
+
className="text-sm font-medium text-amber-700 hover:text-amber-800 underline"
|
|
172
170
|
>
|
|
173
|
-
|
|
171
|
+
Refresh
|
|
174
172
|
</button>
|
|
175
173
|
</div>
|
|
176
174
|
)}
|
|
@@ -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 && !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>
|
|
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 && !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>
|
|
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 && !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>
|
|
253
253
|
</div>
|
|
254
254
|
)}
|
|
255
255
|
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import { Providers } from '@/components/providers';
|
|
3
|
+
import { APP_NAME } from '@/lib/constants';
|
|
3
4
|
import './globals.css';
|
|
4
5
|
|
|
6
|
+
const description = 'Built with Varity — auth, database, and deployment included.';
|
|
7
|
+
|
|
5
8
|
export const metadata: Metadata = {
|
|
6
|
-
title:
|
|
7
|
-
description
|
|
8
|
-
metadataBase: new URL('https://
|
|
9
|
+
title: APP_NAME,
|
|
10
|
+
description,
|
|
11
|
+
metadataBase: new URL('https://varity.app'),
|
|
9
12
|
openGraph: {
|
|
10
|
-
title:
|
|
11
|
-
description
|
|
13
|
+
title: APP_NAME,
|
|
14
|
+
description,
|
|
12
15
|
type: 'website',
|
|
13
16
|
},
|
|
14
17
|
twitter: {
|
|
15
18
|
card: 'summary',
|
|
16
|
-
title:
|
|
17
|
-
description
|
|
19
|
+
title: APP_NAME,
|
|
20
|
+
description,
|
|
18
21
|
},
|
|
19
22
|
};
|
|
20
23
|
|
|
@@ -5,7 +5,6 @@ import { useRouter } from 'next/navigation';
|
|
|
5
5
|
import Link from 'next/link';
|
|
6
6
|
import { CheckCircle } from 'lucide-react';
|
|
7
7
|
import { APP_NAME } from '@/lib/constants';
|
|
8
|
-
import { withBasePath } from '@/lib/utils';
|
|
9
8
|
|
|
10
9
|
let PrivyStackComponent: any = null;
|
|
11
10
|
let usePrivyHook: (() => { authenticated: boolean; ready: boolean; login: () => void }) | null = null;
|
|
@@ -16,6 +15,12 @@ try {
|
|
|
16
15
|
usePrivyHook = uiKit.usePrivy;
|
|
17
16
|
} catch {}
|
|
18
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
|
+
|
|
19
24
|
function LoginContent() {
|
|
20
25
|
const router = useRouter();
|
|
21
26
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
@@ -23,7 +28,7 @@ function LoginContent() {
|
|
|
23
28
|
|
|
24
29
|
useEffect(() => {
|
|
25
30
|
if (privy?.authenticated) {
|
|
26
|
-
router.push(
|
|
31
|
+
router.push('/dashboard');
|
|
27
32
|
}
|
|
28
33
|
}, [privy?.authenticated, router]);
|
|
29
34
|
|
|
@@ -56,11 +61,7 @@ function LoginContent() {
|
|
|
56
61
|
disabled={!privy.ready || privy.authenticated}
|
|
57
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"
|
|
58
63
|
>
|
|
59
|
-
{
|
|
60
|
-
? 'Loading...'
|
|
61
|
-
: privy.authenticated
|
|
62
|
-
? 'Already Signed In'
|
|
63
|
-
: 'Sign In with Email or Social'}
|
|
64
|
+
{loginButtonLabel(privy)}
|
|
64
65
|
</button>
|
|
65
66
|
) : (
|
|
66
67
|
<div className="text-center space-y-4">
|
|
@@ -80,7 +81,6 @@ function LoginContent() {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export default function LoginPage() {
|
|
83
|
-
// Always wrap in PrivyStack - it uses dev credentials automatically when no appId is provided
|
|
84
84
|
if (PrivyStackComponent) {
|
|
85
85
|
return (
|
|
86
86
|
<PrivyStackComponent
|
|
@@ -94,6 +94,5 @@ export default function LoginPage() {
|
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// Fallback if ui-kit package isn't installed
|
|
98
97
|
return <LoginContent />;
|
|
99
98
|
}
|
|
@@ -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
|
-
import { Testimonials } from '@/components/landing/Testimonials';
|
|
6
5
|
import { Pricing } from '@/components/landing/Pricing';
|
|
7
6
|
import { CTA } from '@/components/landing/CTA';
|
|
8
7
|
import { Footer } from '@/components/shared/Footer';
|
|
@@ -14,7 +13,7 @@ export default function HomePage() {
|
|
|
14
13
|
<Hero />
|
|
15
14
|
<Features />
|
|
16
15
|
<HowItWorks />
|
|
17
|
-
|
|
16
|
+
|
|
18
17
|
<Pricing />
|
|
19
18
|
<CTA />
|
|
20
19
|
<Footer />
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { DataTable } from '@varity-labs/ui-kit';
|
|
5
|
-
import { TaskStatusBadge, PriorityBadge } from '@varity-labs/ui-kit';
|
|
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';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
1
2
|
import { Check } from 'lucide-react';
|
|
2
3
|
|
|
3
4
|
const plans = [
|
|
@@ -52,6 +53,12 @@ const plans = [
|
|
|
52
53
|
},
|
|
53
54
|
];
|
|
54
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
|
+
|
|
55
62
|
export function Pricing() {
|
|
56
63
|
return (
|
|
57
64
|
<section id="pricing" className="py-24 bg-white">
|
|
@@ -102,18 +109,12 @@ export function Pricing() {
|
|
|
102
109
|
</ul>
|
|
103
110
|
|
|
104
111
|
<div className="mt-8">
|
|
105
|
-
<
|
|
112
|
+
<Link
|
|
106
113
|
href="/login"
|
|
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
|
-
}`}
|
|
114
|
+
className={`block w-full rounded-lg py-3 text-center font-medium transition-colors ${buttonStyle(plan)}`}
|
|
114
115
|
>
|
|
115
116
|
{plan.cta}
|
|
116
|
-
</
|
|
117
|
+
</Link>
|
|
117
118
|
</div>
|
|
118
119
|
</div>
|
|
119
120
|
))}
|
|
@@ -46,7 +46,6 @@ export function Navbar() {
|
|
|
46
46
|
>
|
|
47
47
|
Get Started
|
|
48
48
|
</Link>
|
|
49
|
-
{/* Mobile menu toggle */}
|
|
50
49
|
<button
|
|
51
50
|
onClick={() => setMobileOpen(!mobileOpen)}
|
|
52
51
|
className="rounded-lg p-2 text-gray-600 hover:bg-gray-100 sm:hidden"
|
|
@@ -58,7 +57,6 @@ export function Navbar() {
|
|
|
58
57
|
</div>
|
|
59
58
|
</div>
|
|
60
59
|
|
|
61
|
-
{/* Mobile dropdown */}
|
|
62
60
|
{mobileOpen && (
|
|
63
61
|
<div className="border-t border-gray-100 bg-white px-4 pb-4 pt-2 sm:hidden">
|
|
64
62
|
<div className="space-y-1">
|
|
@@ -10,6 +10,18 @@ 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
|
+
|
|
13
25
|
export function useCurrentUser() {
|
|
14
26
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
15
27
|
const privy = usePrivyHook ? usePrivyHook() : { user: null, authenticated: false, logout: async () => {} };
|
|
@@ -54,7 +66,7 @@ export function useProjects(): UseCollectionReturn<Project> {
|
|
|
54
66
|
try {
|
|
55
67
|
setLoading(true);
|
|
56
68
|
setError(null);
|
|
57
|
-
const result = await projects().get();
|
|
69
|
+
const result = await fetchWithRetry(() => projects().get());
|
|
58
70
|
setData(result as Project[]);
|
|
59
71
|
} catch (err) {
|
|
60
72
|
setError(err instanceof Error ? err.message : 'Failed to load projects');
|
|
@@ -126,7 +138,7 @@ export function useTasks(projectId?: string): UseCollectionReturn<Task> {
|
|
|
126
138
|
try {
|
|
127
139
|
setLoading(true);
|
|
128
140
|
setError(null);
|
|
129
|
-
const result = await tasks().get();
|
|
141
|
+
const result = await fetchWithRetry(() => tasks().get());
|
|
130
142
|
setAllTasks(result as Task[]);
|
|
131
143
|
} catch (err) {
|
|
132
144
|
setError(err instanceof Error ? err.message : 'Failed to load tasks');
|
|
@@ -202,7 +214,7 @@ export function useTeam(): UseCollectionReturn<TeamMember> {
|
|
|
202
214
|
try {
|
|
203
215
|
setLoading(true);
|
|
204
216
|
setError(null);
|
|
205
|
-
const result = await teamMembers().get();
|
|
217
|
+
const result = await fetchWithRetry(() => teamMembers().get());
|
|
206
218
|
setData(result as TeamMember[]);
|
|
207
219
|
} catch (err) {
|
|
208
220
|
setError(err instanceof Error ? err.message : 'Failed to load team');
|
|
@@ -289,7 +301,7 @@ export function useUserSettings() {
|
|
|
289
301
|
try {
|
|
290
302
|
setLoading(true);
|
|
291
303
|
setError(null);
|
|
292
|
-
const all = await userSettings().get();
|
|
304
|
+
const all = await fetchWithRetry(() => userSettings().get());
|
|
293
305
|
const mine = (all as UserSettings[]).find((s) => s.user_id === userId);
|
|
294
306
|
if (mine) {
|
|
295
307
|
setSettings(mine);
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Navigate using Next.js basePath-aware paths.
|
|
3
|
-
* In static export with basePath, router.push does NOT prepend basePath
|
|
4
|
-
* automatically, so we do it manually.
|
|
5
|
-
*/
|
|
6
|
-
export function getBasePath(): string {
|
|
7
|
-
return process.env.NEXT_PUBLIC_BASE_PATH || '';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function withBasePath(path: string): string {
|
|
11
|
-
const base = getBasePath();
|
|
12
|
-
if (!base) return path;
|
|
13
|
-
return `${base}${path}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
1
|
export function formatDate(dateString: string | undefined | null): string {
|
|
17
2
|
if (!dateString) return '—';
|
|
18
3
|
const date = new Date(dateString);
|