create-varity-app 2.0.0-beta.15 → 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 +3 -2
- 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 +7 -7
- 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/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:^",
|
|
@@ -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 pathname = usePathname();
|
|
117
116
|
const router = useRouter();
|
|
117
|
+
const pathname = usePathname();
|
|
118
118
|
const isMobile = useIsMobile();
|
|
119
119
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
120
120
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
@@ -257,6 +257,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
257
257
|
name: userName,
|
|
258
258
|
address: userEmail,
|
|
259
259
|
}}
|
|
260
|
+
onNavigate={(path: string) => router.push(path)}
|
|
260
261
|
onLogout={handleLogout}
|
|
261
262
|
onNavigateToProfile={() => router.push('/dashboard/settings')}
|
|
262
263
|
onNavigateToSettings={() => router.push('/dashboard/settings')}
|
|
@@ -288,7 +289,7 @@ export default function DashboardRootLayout({
|
|
|
288
289
|
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
|
|
289
290
|
thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
|
|
290
291
|
loginMethods={['email', 'google']}
|
|
291
|
-
appearance={{ theme: 'light', accentColor: '#2563EB'
|
|
292
|
+
appearance={{ theme: 'light', accentColor: '#2563EB' }}
|
|
292
293
|
>
|
|
293
294
|
<PrivyProtectedRoute fallback={<RedirectToLogin />}>
|
|
294
295
|
<DashboardShell>{children}</DashboardShell>
|
|
@@ -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
|
|
|
@@ -15,6 +15,12 @@ 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
|
+
|
|
18
24
|
function LoginContent() {
|
|
19
25
|
const router = useRouter();
|
|
20
26
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
@@ -55,11 +61,7 @@ function LoginContent() {
|
|
|
55
61
|
disabled={!privy.ready || privy.authenticated}
|
|
56
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"
|
|
57
63
|
>
|
|
58
|
-
{
|
|
59
|
-
? 'Loading...'
|
|
60
|
-
: privy.authenticated
|
|
61
|
-
? 'Already Signed In'
|
|
62
|
-
: 'Sign In with Email or Social'}
|
|
64
|
+
{loginButtonLabel(privy)}
|
|
63
65
|
</button>
|
|
64
66
|
) : (
|
|
65
67
|
<div className="text-center space-y-4">
|
|
@@ -79,7 +81,6 @@ function LoginContent() {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export default function LoginPage() {
|
|
82
|
-
// Always wrap in PrivyStack - it uses dev credentials automatically when no appId is provided
|
|
83
84
|
if (PrivyStackComponent) {
|
|
84
85
|
return (
|
|
85
86
|
<PrivyStackComponent
|
|
@@ -93,6 +94,5 @@ export default function LoginPage() {
|
|
|
93
94
|
);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
// Fallback if ui-kit package isn't installed
|
|
97
97
|
return <LoginContent />;
|
|
98
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);
|