create-varity-app 2.0.0-beta.1

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/create.js +141 -0
  4. package/dist/index.js +45 -0
  5. package/dist/utils.js +29 -0
  6. package/package.json +61 -0
  7. package/template/.env.example +17 -0
  8. package/template/KNOWN_ISSUES.md +69 -0
  9. package/template/LICENSE +21 -0
  10. package/template/README.md +241 -0
  11. package/template/gitignore +42 -0
  12. package/template/next-env.d.ts +6 -0
  13. package/template/next.config.js +21 -0
  14. package/template/package.json +39 -0
  15. package/template/postcss.config.js +6 -0
  16. package/template/public/logo.svg +4 -0
  17. package/template/public/robots.txt +4 -0
  18. package/template/public/sitemap.xml +4 -0
  19. package/template/src/app/dashboard/layout.tsx +298 -0
  20. package/template/src/app/dashboard/page.tsx +209 -0
  21. package/template/src/app/dashboard/projects/page.tsx +638 -0
  22. package/template/src/app/dashboard/settings/page.tsx +749 -0
  23. package/template/src/app/dashboard/tasks/page.tsx +301 -0
  24. package/template/src/app/dashboard/team/page.tsx +295 -0
  25. package/template/src/app/globals.css +177 -0
  26. package/template/src/app/icon.svg +4 -0
  27. package/template/src/app/layout.tsx +33 -0
  28. package/template/src/app/login/page.tsx +98 -0
  29. package/template/src/app/not-found.tsx +20 -0
  30. package/template/src/app/page.tsx +23 -0
  31. package/template/src/components/dashboard/DashboardStats.tsx +137 -0
  32. package/template/src/components/dashboard/RecentActivity.tsx +63 -0
  33. package/template/src/components/landing/CTA.tsx +42 -0
  34. package/template/src/components/landing/Features.tsx +116 -0
  35. package/template/src/components/landing/Hero.tsx +146 -0
  36. package/template/src/components/landing/HowItWorks.tsx +80 -0
  37. package/template/src/components/landing/Pricing.tsx +124 -0
  38. package/template/src/components/landing/Testimonials.tsx +78 -0
  39. package/template/src/components/providers.tsx +11 -0
  40. package/template/src/components/shared/Footer.tsx +71 -0
  41. package/template/src/components/shared/Navbar.tsx +87 -0
  42. package/template/src/lib/constants.ts +35 -0
  43. package/template/src/lib/database.ts +7 -0
  44. package/template/src/lib/hooks.ts +331 -0
  45. package/template/src/lib/utils.ts +68 -0
  46. package/template/src/lib/varity.ts +1 -0
  47. package/template/src/services/dashboardService.ts +589 -0
  48. package/template/src/types/index.ts +52 -0
  49. package/template/tailwind.config.js +27 -0
  50. package/template/tsconfig.json +23 -0
  51. package/template/varity.config.json +14 -0
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { CheckCircle, Menu, X } from 'lucide-react';
6
+ import { APP_NAME } from '@/lib/constants';
7
+
8
+ const navLinks = [
9
+ { label: 'Features', href: '#features' },
10
+ { label: 'How It Works', href: '#how-it-works' },
11
+ { label: 'Pricing', href: '#pricing' },
12
+ ];
13
+
14
+ export function Navbar() {
15
+ const [mobileOpen, setMobileOpen] = useState(false);
16
+
17
+ return (
18
+ <nav className="sticky top-0 z-40 border-b border-gray-200 bg-white/80 backdrop-blur-md">
19
+ <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
20
+ <div className="flex h-16 items-center justify-between">
21
+ <Link href="/" className="flex items-center gap-2">
22
+ <CheckCircle className="h-7 w-7 text-primary-600" />
23
+ <span className="text-xl font-bold text-gray-900">{APP_NAME}</span>
24
+ </Link>
25
+ <div className="hidden items-center gap-8 sm:flex">
26
+ {navLinks.map((link) => (
27
+ <a
28
+ key={link.href}
29
+ href={link.href}
30
+ className="text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
31
+ >
32
+ {link.label}
33
+ </a>
34
+ ))}
35
+ </div>
36
+ <div className="flex items-center gap-3">
37
+ <Link
38
+ href="/login"
39
+ className="hidden text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors sm:block"
40
+ >
41
+ Sign In
42
+ </Link>
43
+ <Link
44
+ href="/login"
45
+ className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700 transition-colors"
46
+ >
47
+ Get Started
48
+ </Link>
49
+ {/* Mobile menu toggle */}
50
+ <button
51
+ onClick={() => setMobileOpen(!mobileOpen)}
52
+ className="rounded-lg p-2 text-gray-600 hover:bg-gray-100 sm:hidden"
53
+ aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
54
+ >
55
+ {mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
56
+ </button>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ {/* Mobile dropdown */}
62
+ {mobileOpen && (
63
+ <div className="border-t border-gray-100 bg-white px-4 pb-4 pt-2 sm:hidden">
64
+ <div className="space-y-1">
65
+ {navLinks.map((link) => (
66
+ <a
67
+ key={link.href}
68
+ href={link.href}
69
+ onClick={() => setMobileOpen(false)}
70
+ 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"
71
+ >
72
+ {link.label}
73
+ </a>
74
+ ))}
75
+ <Link
76
+ href="/login"
77
+ onClick={() => setMobileOpen(false)}
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"
79
+ >
80
+ Sign In
81
+ </Link>
82
+ </div>
83
+ </div>
84
+ )}
85
+ </nav>
86
+ );
87
+ }
@@ -0,0 +1,35 @@
1
+ import type { NavigationItem } from '@varity-labs/ui-kit';
2
+
3
+ export const APP_NAME = 'TaskFlow';
4
+
5
+ export const NAVIGATION_ITEMS: NavigationItem[] = [
6
+ { label: 'Dashboard', icon: 'dashboard', path: '/dashboard' },
7
+ { label: 'Projects', icon: 'folder', path: '/dashboard/projects' },
8
+ { label: 'Tasks', icon: 'list', path: '/dashboard/tasks' },
9
+ { label: 'Team', icon: 'people', path: '/dashboard/team' },
10
+ { label: 'Settings', icon: 'settings', path: '/dashboard/settings' },
11
+ ];
12
+
13
+ export const PRIORITY_OPTIONS = [
14
+ { value: 'low', label: 'Low' },
15
+ { value: 'medium', label: 'Medium' },
16
+ { value: 'high', label: 'High' },
17
+ ] as const;
18
+
19
+ export const ROLE_OPTIONS = [
20
+ { value: 'member', label: 'Member' },
21
+ { value: 'admin', label: 'Admin' },
22
+ { value: 'viewer', label: 'Viewer' },
23
+ ] as const;
24
+
25
+ export const TASK_STATUS_OPTIONS = [
26
+ { value: 'todo', label: 'To Do' },
27
+ { value: 'in_progress', label: 'In Progress' },
28
+ { value: 'done', label: 'Done' },
29
+ ] as const;
30
+
31
+ export const PROJECT_STATUS_OPTIONS = [
32
+ { value: 'active', label: 'Active' },
33
+ { value: 'paused', label: 'Paused' },
34
+ { value: 'completed', label: 'Completed' },
35
+ ] as const;
@@ -0,0 +1,7 @@
1
+ import { db } from './varity';
2
+ import type { Project, Task, TeamMember, UserSettings } from '../types';
3
+
4
+ export const projects = () => db.collection<Project>('projects');
5
+ export const tasks = () => db.collection<Task>('tasks');
6
+ export const teamMembers = () => db.collection<TeamMember>('team_members');
7
+ export const userSettings = () => db.collection<UserSettings>('user_settings');
@@ -0,0 +1,331 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { projects, tasks, teamMembers, userSettings } from './database';
5
+ import type { Project, Task, TeamMember, UserSettings } from '../types';
6
+
7
+ let usePrivyHook: any = null;
8
+ try {
9
+ const uiKit = require('@varity-labs/ui-kit');
10
+ usePrivyHook = uiKit.usePrivy;
11
+ } catch {}
12
+
13
+ export function useCurrentUser() {
14
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
15
+ const privy = usePrivyHook ? usePrivyHook() : { user: null, authenticated: false, logout: async () => {} };
16
+
17
+ // Extract email from any Privy auth method (email, Google, GitHub, etc.)
18
+ const user = privy.user;
19
+ const email =
20
+ user?.email?.address ||
21
+ user?.google?.email ||
22
+ user?.github?.email ||
23
+ user?.linkedAccounts?.find((a: any) => a.address && a.type === 'email')?.address ||
24
+ user?.linkedAccounts?.find((a: any) => a.email)?.email ||
25
+ '';
26
+
27
+ const name = email ? email.split('@')[0] : (user?.id ? 'User' : 'User');
28
+
29
+ return {
30
+ id: user?.id || 'dev-user-id',
31
+ email,
32
+ name,
33
+ authenticated: privy.authenticated,
34
+ logout: privy.logout,
35
+ };
36
+ }
37
+
38
+ interface UseCollectionReturn<T> {
39
+ data: T[];
40
+ loading: boolean;
41
+ error: string | null;
42
+ create: (item: any) => Promise<void>;
43
+ update: (id: string, updates: Partial<T>) => Promise<void>;
44
+ remove: (id: string) => Promise<void>;
45
+ refresh: () => Promise<void>;
46
+ }
47
+
48
+ export function useProjects(): UseCollectionReturn<Project> {
49
+ const [data, setData] = useState<Project[]>([]);
50
+ const [loading, setLoading] = useState(true);
51
+ const [error, setError] = useState<string | null>(null);
52
+
53
+ const refresh = useCallback(async () => {
54
+ try {
55
+ setLoading(true);
56
+ setError(null);
57
+ const result = await projects().get();
58
+ setData(result as Project[]);
59
+ } catch (err) {
60
+ setError(err instanceof Error ? err.message : 'Failed to load projects');
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ }, []);
65
+
66
+ useEffect(() => {
67
+ refresh();
68
+ }, [refresh]);
69
+
70
+ const create = async (input: Omit<Project, 'id' | 'createdAt'>) => {
71
+ const newProject: Project = {
72
+ ...input,
73
+ id: crypto.randomUUID(),
74
+ createdAt: new Date().toISOString(),
75
+ };
76
+ setData((prev) => [newProject, ...prev]);
77
+
78
+ try {
79
+ await projects().add({ ...input, createdAt: newProject.createdAt });
80
+ await refresh();
81
+ } catch (err) {
82
+ setData((prev) => prev.filter((p) => p.id !== newProject.id));
83
+ throw err;
84
+ }
85
+ };
86
+
87
+ const update = async (id: string, updates: Partial<Project>) => {
88
+ const original = data.find((p) => p.id === id);
89
+ setData((prev) =>
90
+ prev.map((p) => (p.id === id ? { ...p, ...updates } : p))
91
+ );
92
+
93
+ try {
94
+ await projects().update(id, updates);
95
+ } catch (err) {
96
+ if (original) {
97
+ setData((prev) =>
98
+ prev.map((p) => (p.id === id ? original : p))
99
+ );
100
+ }
101
+ throw err;
102
+ }
103
+ };
104
+
105
+ const remove = async (id: string) => {
106
+ const original = data.find((p) => p.id === id);
107
+ setData((prev) => prev.filter((p) => p.id !== id));
108
+
109
+ try {
110
+ await projects().delete(id);
111
+ } catch (err) {
112
+ if (original) setData((prev) => [...prev, original]);
113
+ throw err;
114
+ }
115
+ };
116
+
117
+ return { data, loading, error, create, update, remove, refresh };
118
+ }
119
+
120
+ export function useTasks(projectId?: string): UseCollectionReturn<Task> {
121
+ const [allTasks, setAllTasks] = useState<Task[]>([]);
122
+ const [loading, setLoading] = useState(true);
123
+ const [error, setError] = useState<string | null>(null);
124
+
125
+ const refresh = useCallback(async () => {
126
+ try {
127
+ setLoading(true);
128
+ setError(null);
129
+ const result = await tasks().get();
130
+ setAllTasks(result as Task[]);
131
+ } catch (err) {
132
+ setError(err instanceof Error ? err.message : 'Failed to load tasks');
133
+ } finally {
134
+ setLoading(false);
135
+ }
136
+ }, []);
137
+
138
+ useEffect(() => {
139
+ refresh();
140
+ }, [refresh]);
141
+
142
+ const data = projectId
143
+ ? allTasks.filter((t) => t.projectId === projectId)
144
+ : allTasks;
145
+
146
+ const create = async (input: Omit<Task, 'id' | 'createdAt'>) => {
147
+ const newTask: Task = {
148
+ ...input,
149
+ id: crypto.randomUUID(),
150
+ createdAt: new Date().toISOString(),
151
+ };
152
+ setAllTasks((prev) => [newTask, ...prev]);
153
+
154
+ try {
155
+ await tasks().add({ ...input, createdAt: newTask.createdAt });
156
+ await refresh();
157
+ } catch (err) {
158
+ setAllTasks((prev) => prev.filter((t) => t.id !== newTask.id));
159
+ throw err;
160
+ }
161
+ };
162
+
163
+ const update = async (id: string, updates: Partial<Task>) => {
164
+ const original = allTasks.find((t) => t.id === id);
165
+ setAllTasks((prev) =>
166
+ prev.map((t) => (t.id === id ? { ...t, ...updates } : t))
167
+ );
168
+
169
+ try {
170
+ await tasks().update(id, updates);
171
+ } catch (err) {
172
+ if (original) {
173
+ setAllTasks((prev) =>
174
+ prev.map((t) => (t.id === id ? original : t))
175
+ );
176
+ }
177
+ throw err;
178
+ }
179
+ };
180
+
181
+ const remove = async (id: string) => {
182
+ const original = allTasks.find((t) => t.id === id);
183
+ setAllTasks((prev) => prev.filter((t) => t.id !== id));
184
+
185
+ try {
186
+ await tasks().delete(id);
187
+ } catch (err) {
188
+ if (original) setAllTasks((prev) => [...prev, original]);
189
+ throw err;
190
+ }
191
+ };
192
+
193
+ return { data, loading, error, create, update, remove, refresh };
194
+ }
195
+
196
+ export function useTeam(): UseCollectionReturn<TeamMember> {
197
+ const [data, setData] = useState<TeamMember[]>([]);
198
+ const [loading, setLoading] = useState(true);
199
+ const [error, setError] = useState<string | null>(null);
200
+
201
+ const refresh = useCallback(async () => {
202
+ try {
203
+ setLoading(true);
204
+ setError(null);
205
+ const result = await teamMembers().get();
206
+ setData(result as TeamMember[]);
207
+ } catch (err) {
208
+ setError(err instanceof Error ? err.message : 'Failed to load team');
209
+ } finally {
210
+ setLoading(false);
211
+ }
212
+ }, []);
213
+
214
+ useEffect(() => {
215
+ refresh();
216
+ }, [refresh]);
217
+
218
+ const create = async (input: Omit<TeamMember, 'id' | 'joinedAt'>) => {
219
+ const newMember: TeamMember = {
220
+ ...input,
221
+ id: crypto.randomUUID(),
222
+ joinedAt: new Date().toISOString(),
223
+ };
224
+ setData((prev) => [newMember, ...prev]);
225
+
226
+ try {
227
+ await teamMembers().add({ ...input, joinedAt: newMember.joinedAt });
228
+ await refresh();
229
+ } catch (err) {
230
+ setData((prev) => prev.filter((m) => m.id !== newMember.id));
231
+ throw err;
232
+ }
233
+ };
234
+
235
+ const update = async (id: string, updates: Partial<TeamMember>) => {
236
+ const original = data.find((m) => m.id === id);
237
+ setData((prev) =>
238
+ prev.map((m) => (m.id === id ? { ...m, ...updates } : m))
239
+ );
240
+
241
+ try {
242
+ await teamMembers().update(id, updates);
243
+ } catch (err) {
244
+ if (original) {
245
+ setData((prev) =>
246
+ prev.map((m) => (m.id === id ? original : m))
247
+ );
248
+ }
249
+ throw err;
250
+ }
251
+ };
252
+
253
+ const remove = async (id: string) => {
254
+ const original = data.find((m) => m.id === id);
255
+ setData((prev) => prev.filter((m) => m.id !== id));
256
+
257
+ try {
258
+ await teamMembers().delete(id);
259
+ } catch (err) {
260
+ if (original) setData((prev) => [...prev, original]);
261
+ throw err;
262
+ }
263
+ };
264
+
265
+ return { data, loading, error, create, update, remove, refresh };
266
+ }
267
+
268
+ const DEFAULT_SETTINGS: Omit<UserSettings, 'id' | 'user_id' | 'updated_at'> = {
269
+ theme: 'system',
270
+ email_notifications: true,
271
+ marketing_emails: false,
272
+ product_updates: true,
273
+ date_format: 'MM/DD/YYYY',
274
+ timezone: 'America/Los_Angeles',
275
+ language: 'en',
276
+ dashboard_layout: 'comfortable',
277
+ two_factor_enabled: false,
278
+ analytics_enabled: true,
279
+ cookies_enabled: true,
280
+ };
281
+
282
+ export function useUserSettings() {
283
+ const { id: userId } = useCurrentUser();
284
+ const [settings, setSettings] = useState<UserSettings | null>(null);
285
+ const [loading, setLoading] = useState(true);
286
+ const [error, setError] = useState<string | null>(null);
287
+
288
+ const refresh = useCallback(async () => {
289
+ try {
290
+ setLoading(true);
291
+ setError(null);
292
+ const all = await userSettings().get();
293
+ const mine = (all as UserSettings[]).find((s) => s.user_id === userId);
294
+ if (mine) {
295
+ setSettings(mine);
296
+ } else {
297
+ const newSettings = {
298
+ ...DEFAULT_SETTINGS,
299
+ user_id: userId,
300
+ updated_at: new Date().toISOString(),
301
+ };
302
+ const created = await userSettings().add(newSettings);
303
+ setSettings(created as UserSettings);
304
+ }
305
+ } catch (err) {
306
+ setError(err instanceof Error ? err.message : 'Failed to load settings');
307
+ } finally {
308
+ setLoading(false);
309
+ }
310
+ }, [userId]);
311
+
312
+ useEffect(() => {
313
+ refresh();
314
+ }, [refresh]);
315
+
316
+ const update = async (updates: Partial<UserSettings>) => {
317
+ if (!settings) return;
318
+ const original = { ...settings };
319
+ const patched = { ...settings, ...updates, updated_at: new Date().toISOString() };
320
+ setSettings(patched);
321
+
322
+ try {
323
+ await userSettings().update(settings.id, { ...updates, updated_at: patched.updated_at });
324
+ } catch (err) {
325
+ setSettings(original);
326
+ throw err;
327
+ }
328
+ };
329
+
330
+ return { settings, loading, error, update, refresh };
331
+ }
@@ -0,0 +1,68 @@
1
+ export function formatDate(dateString: string | undefined | null): string {
2
+ if (!dateString) return '—';
3
+ const date = new Date(dateString);
4
+ if (isNaN(date.getTime())) return '—';
5
+ return date.toLocaleDateString('en-US', {
6
+ month: 'short',
7
+ day: 'numeric',
8
+ year: 'numeric',
9
+ });
10
+ }
11
+
12
+ export function formatDateShort(dateString: string | undefined | null): string {
13
+ if (!dateString) return '—';
14
+ const date = new Date(dateString);
15
+ if (isNaN(date.getTime())) return '—';
16
+ return date.toLocaleDateString('en-US', {
17
+ month: 'short',
18
+ day: 'numeric',
19
+ });
20
+ }
21
+
22
+ export function formatRelativeDate(dateString: string | undefined | null): string {
23
+ if (!dateString) return '—';
24
+ const date = new Date(dateString);
25
+ if (isNaN(date.getTime())) return '—';
26
+ const now = new Date();
27
+ const diffMs = now.getTime() - date.getTime();
28
+ const diffMins = Math.floor(diffMs / 60000);
29
+ const diffHours = Math.floor(diffMins / 60);
30
+ const diffDays = Math.floor(diffHours / 24);
31
+
32
+ if (diffMins < 1) return 'Just now';
33
+ if (diffMins < 60) return `${diffMins}m ago`;
34
+ if (diffHours < 24) return `${diffHours}h ago`;
35
+ if (diffDays < 7) return `${diffDays}d ago`;
36
+ return formatDateShort(dateString);
37
+ }
38
+
39
+ export function isValidEmail(email: string): boolean {
40
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
41
+ }
42
+
43
+ export function cn(...classes: (string | false | undefined | null)[]): string {
44
+ return classes.filter(Boolean).join(' ');
45
+ }
46
+
47
+ export function downloadCSV(data: Record<string, unknown>[], filename: string) {
48
+ if (data.length === 0) return;
49
+ const headers = Object.keys(data[0]);
50
+ const rows = data.map((row) =>
51
+ headers.map((h) => {
52
+ const val = row[h];
53
+ const str = val === null || val === undefined ? '' : String(val);
54
+ // Escape quotes and wrap in quotes if needed
55
+ return str.includes(',') || str.includes('"') || str.includes('\n')
56
+ ? `"${str.replace(/"/g, '""')}"`
57
+ : str;
58
+ }).join(',')
59
+ );
60
+ const csv = [headers.join(','), ...rows].join('\n');
61
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
62
+ const url = URL.createObjectURL(blob);
63
+ const a = document.createElement('a');
64
+ a.href = url;
65
+ a.download = filename;
66
+ a.click();
67
+ URL.revokeObjectURL(url);
68
+ }
@@ -0,0 +1 @@
1
+ export { db } from '@varity-labs/sdk';