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.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/create.js +141 -0
- package/dist/index.js +45 -0
- package/dist/utils.js +29 -0
- package/package.json +61 -0
- package/template/.env.example +17 -0
- package/template/KNOWN_ISSUES.md +69 -0
- package/template/LICENSE +21 -0
- package/template/README.md +241 -0
- package/template/gitignore +42 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.js +21 -0
- package/template/package.json +39 -0
- package/template/postcss.config.js +6 -0
- package/template/public/logo.svg +4 -0
- package/template/public/robots.txt +4 -0
- package/template/public/sitemap.xml +4 -0
- package/template/src/app/dashboard/layout.tsx +298 -0
- package/template/src/app/dashboard/page.tsx +209 -0
- package/template/src/app/dashboard/projects/page.tsx +638 -0
- package/template/src/app/dashboard/settings/page.tsx +749 -0
- package/template/src/app/dashboard/tasks/page.tsx +301 -0
- package/template/src/app/dashboard/team/page.tsx +295 -0
- package/template/src/app/globals.css +177 -0
- package/template/src/app/icon.svg +4 -0
- package/template/src/app/layout.tsx +33 -0
- package/template/src/app/login/page.tsx +98 -0
- package/template/src/app/not-found.tsx +20 -0
- package/template/src/app/page.tsx +23 -0
- package/template/src/components/dashboard/DashboardStats.tsx +137 -0
- package/template/src/components/dashboard/RecentActivity.tsx +63 -0
- package/template/src/components/landing/CTA.tsx +42 -0
- package/template/src/components/landing/Features.tsx +116 -0
- package/template/src/components/landing/Hero.tsx +146 -0
- package/template/src/components/landing/HowItWorks.tsx +80 -0
- package/template/src/components/landing/Pricing.tsx +124 -0
- package/template/src/components/landing/Testimonials.tsx +78 -0
- package/template/src/components/providers.tsx +11 -0
- package/template/src/components/shared/Footer.tsx +71 -0
- package/template/src/components/shared/Navbar.tsx +87 -0
- package/template/src/lib/constants.ts +35 -0
- package/template/src/lib/database.ts +7 -0
- package/template/src/lib/hooks.ts +331 -0
- package/template/src/lib/utils.ts +68 -0
- package/template/src/lib/varity.ts +1 -0
- package/template/src/services/dashboardService.ts +589 -0
- package/template/src/types/index.ts +52 -0
- package/template/tailwind.config.js +27 -0
- package/template/tsconfig.json +23 -0
- package/template/varity.config.json +14 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* ============================================================
|
|
7
|
+
* BRAND COLORS — Switch themes in seconds
|
|
8
|
+
* ============================================================
|
|
9
|
+
*
|
|
10
|
+
* HOW TO SWITCH:
|
|
11
|
+
* 1. Comment out the active "Blue" :root block below
|
|
12
|
+
* 2. Uncomment one of the preset blocks (Purple, Green, or Orange)
|
|
13
|
+
* 3. Save — your entire app updates instantly
|
|
14
|
+
*
|
|
15
|
+
* Or set your own colors using any Tailwind color palette:
|
|
16
|
+
* https://tailwindcss.com/docs/customizing-colors
|
|
17
|
+
*
|
|
18
|
+
* ── Purple ──────────────────────────────────────────────────
|
|
19
|
+
* :root {
|
|
20
|
+
* --color-primary-50: #faf5ff; --color-primary-100: #f3e8ff;
|
|
21
|
+
* --color-primary-200: #e9d5ff; --color-primary-300: #d8b4fe;
|
|
22
|
+
* --color-primary-400: #c084fc; --color-primary-500: #a855f7;
|
|
23
|
+
* --color-primary-600: #9333ea; --color-primary-700: #7e22ce;
|
|
24
|
+
* --color-primary-800: #6b21a8; --color-primary-900: #581c87;
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* ── Green ───────────────────────────────────────────────────
|
|
28
|
+
* :root {
|
|
29
|
+
* --color-primary-50: #ecfdf5; --color-primary-100: #d1fae5;
|
|
30
|
+
* --color-primary-200: #a7f3d0; --color-primary-300: #6ee7b7;
|
|
31
|
+
* --color-primary-400: #34d399; --color-primary-500: #10b981;
|
|
32
|
+
* --color-primary-600: #059669; --color-primary-700: #047857;
|
|
33
|
+
* --color-primary-800: #065f46; --color-primary-900: #064e3b;
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* ── Orange ──────────────────────────────────────────────────
|
|
37
|
+
* :root {
|
|
38
|
+
* --color-primary-50: #fff7ed; --color-primary-100: #ffedd5;
|
|
39
|
+
* --color-primary-200: #fed7aa; --color-primary-300: #fdba74;
|
|
40
|
+
* --color-primary-400: #fb923c; --color-primary-500: #f97316;
|
|
41
|
+
* --color-primary-600: #ea580c; --color-primary-700: #c2410c;
|
|
42
|
+
* --color-primary-800: #9a3412; --color-primary-900: #7c2d12;
|
|
43
|
+
* }
|
|
44
|
+
* ============================================================
|
|
45
|
+
*/
|
|
46
|
+
:root {
|
|
47
|
+
/* Tailwind primary palette */
|
|
48
|
+
--color-primary-50: #eef2ff;
|
|
49
|
+
--color-primary-100: #e0e7ff;
|
|
50
|
+
--color-primary-200: #c7d2fe;
|
|
51
|
+
--color-primary-300: #a5b4fc;
|
|
52
|
+
--color-primary-400: #818cf8;
|
|
53
|
+
--color-primary-500: #6366f1;
|
|
54
|
+
--color-primary-600: #4f46e5;
|
|
55
|
+
--color-primary-700: #4338ca;
|
|
56
|
+
--color-primary-800: #3730a3;
|
|
57
|
+
--color-primary-900: #312e81;
|
|
58
|
+
|
|
59
|
+
/* Varity UI-Kit theme bridge — aligns inline-style CSS vars with Tailwind palette */
|
|
60
|
+
--varity-primary-color: #4f46e5;
|
|
61
|
+
--varity-bg-primary: #f9fafb;
|
|
62
|
+
--varity-bg-sidebar: #ffffff;
|
|
63
|
+
--varity-bg-header: #ffffff;
|
|
64
|
+
--varity-bg-footer: #ffffff;
|
|
65
|
+
--varity-bg-secondary: #f3f4f6;
|
|
66
|
+
--varity-bg-hover: #eef2ff;
|
|
67
|
+
--varity-text-primary: #111827;
|
|
68
|
+
--varity-text-secondary: #6b7280;
|
|
69
|
+
--varity-border-color: #e5e7eb;
|
|
70
|
+
--varity-accent-color: #ef4444;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Typography Scale */
|
|
74
|
+
.heading-hero {
|
|
75
|
+
font-size: 3.5rem;
|
|
76
|
+
font-weight: 800;
|
|
77
|
+
line-height: 1.1;
|
|
78
|
+
letter-spacing: -0.02em;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.heading-section {
|
|
82
|
+
font-size: 2.25rem;
|
|
83
|
+
font-weight: 700;
|
|
84
|
+
line-height: 1.2;
|
|
85
|
+
letter-spacing: -0.01em;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.heading-card {
|
|
89
|
+
font-size: 1.125rem;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
line-height: 1.4;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media (max-width: 640px) {
|
|
95
|
+
.heading-hero {
|
|
96
|
+
font-size: 2.5rem;
|
|
97
|
+
}
|
|
98
|
+
.heading-section {
|
|
99
|
+
font-size: 1.875rem;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
html {
|
|
104
|
+
scroll-behavior: smooth;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
body {
|
|
108
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
|
109
|
+
Ubuntu, Cantarell, sans-serif;
|
|
110
|
+
-webkit-font-smoothing: antialiased;
|
|
111
|
+
-moz-osx-font-smoothing: grayscale;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@keyframes fade-in-up {
|
|
115
|
+
from {
|
|
116
|
+
opacity: 0;
|
|
117
|
+
transform: translateY(20px);
|
|
118
|
+
}
|
|
119
|
+
to {
|
|
120
|
+
opacity: 1;
|
|
121
|
+
transform: translateY(0);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@keyframes fade-in {
|
|
126
|
+
from { opacity: 0; }
|
|
127
|
+
to { opacity: 1; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.animate-fade-in-up {
|
|
131
|
+
animation: fade-in-up 0.6s ease-out forwards;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.animate-fade-in-up-delay-1 {
|
|
135
|
+
animation: fade-in-up 0.6s ease-out 0.1s forwards;
|
|
136
|
+
opacity: 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.animate-fade-in-up-delay-2 {
|
|
140
|
+
animation: fade-in-up 0.6s ease-out 0.2s forwards;
|
|
141
|
+
opacity: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.animate-fade-in-up-delay-3 {
|
|
145
|
+
animation: fade-in-up 0.6s ease-out 0.3s forwards;
|
|
146
|
+
opacity: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.animate-fade-in-up-delay-4 {
|
|
150
|
+
animation: fade-in-up 0.6s ease-out 0.4s forwards;
|
|
151
|
+
opacity: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.animate-fade-in {
|
|
155
|
+
animation: fade-in 0.8s ease-out forwards;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@keyframes toast-enter {
|
|
159
|
+
from {
|
|
160
|
+
opacity: 0;
|
|
161
|
+
transform: translateX(100%);
|
|
162
|
+
}
|
|
163
|
+
to {
|
|
164
|
+
opacity: 1;
|
|
165
|
+
transform: translateX(0);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@keyframes toast-progress {
|
|
170
|
+
from { width: 100%; }
|
|
171
|
+
to { width: 0%; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@keyframes progress-stripes {
|
|
175
|
+
0% { background-position: 1rem 0; }
|
|
176
|
+
100% { background-position: 0 0; }
|
|
177
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="16" cy="16" r="14" fill="#4f46e5"/>
|
|
3
|
+
<path d="M10 16L14.5 20.5L22 11.5" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { Providers } from '@/components/providers';
|
|
3
|
+
import './globals.css';
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: 'TaskFlow - Project Management',
|
|
7
|
+
description: 'Manage projects, track tasks, and collaborate with your team.',
|
|
8
|
+
metadataBase: new URL('https://example.com'),
|
|
9
|
+
openGraph: {
|
|
10
|
+
title: 'TaskFlow - Project Management',
|
|
11
|
+
description: 'Manage projects, track tasks, and collaborate with your team.',
|
|
12
|
+
type: 'website',
|
|
13
|
+
},
|
|
14
|
+
twitter: {
|
|
15
|
+
card: 'summary',
|
|
16
|
+
title: 'TaskFlow - Project Management',
|
|
17
|
+
description: 'Manage projects, track tasks, and collaborate with your team.',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default function RootLayout({
|
|
22
|
+
children,
|
|
23
|
+
}: {
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
return (
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<body>
|
|
29
|
+
<Providers>{children}</Providers>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { CheckCircle } from 'lucide-react';
|
|
7
|
+
import { APP_NAME } from '@/lib/constants';
|
|
8
|
+
|
|
9
|
+
let PrivyStackComponent: any = null;
|
|
10
|
+
let usePrivyHook: (() => { authenticated: boolean; ready: boolean; login: () => void }) | null = null;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const uiKit = require('@varity-labs/ui-kit');
|
|
14
|
+
PrivyStackComponent = uiKit.PrivyStack;
|
|
15
|
+
usePrivyHook = uiKit.usePrivy;
|
|
16
|
+
} catch {}
|
|
17
|
+
|
|
18
|
+
function LoginContent() {
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
|
|
21
|
+
const privy = usePrivyHook ? usePrivyHook() : null;
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (privy?.authenticated) {
|
|
25
|
+
router.push('/dashboard');
|
|
26
|
+
}
|
|
27
|
+
}, [privy?.authenticated, router]);
|
|
28
|
+
|
|
29
|
+
const handleLogin = () => {
|
|
30
|
+
if (privy?.login) {
|
|
31
|
+
privy.login();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4">
|
|
37
|
+
<div className="w-full max-w-md space-y-8">
|
|
38
|
+
<div className="text-center">
|
|
39
|
+
<Link href="/" className="inline-flex items-center gap-2">
|
|
40
|
+
<CheckCircle className="h-8 w-8 text-primary-600" />
|
|
41
|
+
<span className="text-2xl font-bold text-gray-900">{APP_NAME}</span>
|
|
42
|
+
</Link>
|
|
43
|
+
<h2 className="mt-6 text-2xl font-bold text-gray-900">
|
|
44
|
+
Sign in to your account
|
|
45
|
+
</h2>
|
|
46
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
47
|
+
Use your email or social account to get started.
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className="rounded-xl border border-gray-200 bg-white p-8 shadow-sm">
|
|
52
|
+
{privy ? (
|
|
53
|
+
<button
|
|
54
|
+
onClick={handleLogin}
|
|
55
|
+
disabled={!privy.ready || privy.authenticated}
|
|
56
|
+
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
|
+
>
|
|
58
|
+
{!privy.ready
|
|
59
|
+
? 'Loading...'
|
|
60
|
+
: privy.authenticated
|
|
61
|
+
? 'Already Signed In'
|
|
62
|
+
: 'Sign In with Email or Social'}
|
|
63
|
+
</button>
|
|
64
|
+
) : (
|
|
65
|
+
<div className="text-center space-y-4">
|
|
66
|
+
<p className="text-sm text-gray-600">
|
|
67
|
+
Loading authentication...
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<p className="text-center text-xs text-gray-500">
|
|
74
|
+
By signing in, you agree to our Terms of Service and Privacy Policy.
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default function LoginPage() {
|
|
82
|
+
// Always wrap in PrivyStack - it uses dev credentials automatically when no appId is provided
|
|
83
|
+
if (PrivyStackComponent) {
|
|
84
|
+
return (
|
|
85
|
+
<PrivyStackComponent
|
|
86
|
+
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
|
|
87
|
+
thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
|
|
88
|
+
loginMethods={['email', 'google']}
|
|
89
|
+
appearance={{ theme: 'light', accentColor: '#2563EB', logo: '/logo.svg' }}
|
|
90
|
+
>
|
|
91
|
+
<LoginContent />
|
|
92
|
+
</PrivyStackComponent>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fallback if ui-kit package isn't installed
|
|
97
|
+
return <LoginContent />;
|
|
98
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
|
|
5
|
+
export default function NotFound() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="min-h-screen bg-white flex items-center justify-center px-4">
|
|
8
|
+
<div className="text-center">
|
|
9
|
+
<h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
|
|
10
|
+
<p className="text-xl text-gray-600 mb-8">Page not found</p>
|
|
11
|
+
<Link
|
|
12
|
+
href="/"
|
|
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"
|
|
14
|
+
>
|
|
15
|
+
Go Home
|
|
16
|
+
</Link>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Navbar } from '@/components/shared/Navbar';
|
|
2
|
+
import { Hero } from '@/components/landing/Hero';
|
|
3
|
+
import { Features } from '@/components/landing/Features';
|
|
4
|
+
import { HowItWorks } from '@/components/landing/HowItWorks';
|
|
5
|
+
import { Testimonials } from '@/components/landing/Testimonials';
|
|
6
|
+
import { Pricing } from '@/components/landing/Pricing';
|
|
7
|
+
import { CTA } from '@/components/landing/CTA';
|
|
8
|
+
import { Footer } from '@/components/shared/Footer';
|
|
9
|
+
|
|
10
|
+
export default function HomePage() {
|
|
11
|
+
return (
|
|
12
|
+
<div className="min-h-screen bg-white">
|
|
13
|
+
<Navbar />
|
|
14
|
+
<Hero />
|
|
15
|
+
<Features />
|
|
16
|
+
<HowItWorks />
|
|
17
|
+
<Testimonials />
|
|
18
|
+
<Pricing />
|
|
19
|
+
<CTA />
|
|
20
|
+
<Footer />
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { EnhancedKPICard, Skeleton } from '@varity-labs/ui-kit';
|
|
5
|
+
import type { Project, Task, TeamMember } from '@/types';
|
|
6
|
+
|
|
7
|
+
interface DashboardStatsProps {
|
|
8
|
+
projects: Project[];
|
|
9
|
+
tasks: Task[];
|
|
10
|
+
team: TeamMember[];
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DashboardStats({
|
|
15
|
+
projects,
|
|
16
|
+
tasks,
|
|
17
|
+
team,
|
|
18
|
+
loading = false,
|
|
19
|
+
}: DashboardStatsProps) {
|
|
20
|
+
const activeProjects = projects.filter((p) => p.status === 'active').length;
|
|
21
|
+
const activeTasks = tasks.filter((t) => t.status !== 'done').length;
|
|
22
|
+
const doneTasks = tasks.filter((t) => t.status === 'done').length;
|
|
23
|
+
const completionRate =
|
|
24
|
+
tasks.length > 0 ? Math.round((doneTasks / tasks.length) * 100) : 0;
|
|
25
|
+
|
|
26
|
+
// Generate sparkline data (mock trend data for each metric)
|
|
27
|
+
const projectsSparkline = useMemo(() => {
|
|
28
|
+
const baseValue = projects.length || 8;
|
|
29
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
30
|
+
const progress = i / 6;
|
|
31
|
+
const trend = baseValue * (1 - 0.125 * (1 - progress));
|
|
32
|
+
return Math.max(1, Math.round(trend + (Math.random() - 0.5) * 2));
|
|
33
|
+
});
|
|
34
|
+
}, [projects.length]);
|
|
35
|
+
|
|
36
|
+
const tasksSparkline = useMemo(() => {
|
|
37
|
+
const baseValue = tasks.length || 12;
|
|
38
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
39
|
+
const progress = i / 6;
|
|
40
|
+
const trend = baseValue * (1 + 0.15 * progress);
|
|
41
|
+
return Math.max(1, Math.round(trend + (Math.random() - 0.5) * 2));
|
|
42
|
+
});
|
|
43
|
+
}, [tasks.length]);
|
|
44
|
+
|
|
45
|
+
const teamSparkline = useMemo(() => {
|
|
46
|
+
const baseValue = team.length || 3;
|
|
47
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
48
|
+
const progress = i / 6;
|
|
49
|
+
const trend = baseValue * (1 + 0.08 * progress);
|
|
50
|
+
return Math.max(1, Math.round(trend + (Math.random() - 0.5) * 0.5));
|
|
51
|
+
});
|
|
52
|
+
}, [team.length]);
|
|
53
|
+
|
|
54
|
+
const completionSparkline = useMemo(() => {
|
|
55
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
56
|
+
const progress = i / 6;
|
|
57
|
+
const trend = completionRate * (1 - 0.10 * (1 - progress));
|
|
58
|
+
return Math.max(0, Math.min(100, trend + (Math.random() - 0.5) * 5));
|
|
59
|
+
});
|
|
60
|
+
}, [completionRate]);
|
|
61
|
+
|
|
62
|
+
if (loading) {
|
|
63
|
+
return (
|
|
64
|
+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
65
|
+
<Skeleton height={180} />
|
|
66
|
+
<Skeleton height={180} />
|
|
67
|
+
<Skeleton height={180} />
|
|
68
|
+
<Skeleton height={180} />
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
75
|
+
<EnhancedKPICard
|
|
76
|
+
title="Active Projects"
|
|
77
|
+
value={activeProjects.toString()}
|
|
78
|
+
change={{
|
|
79
|
+
value: 12.5,
|
|
80
|
+
period: 'vs last month'
|
|
81
|
+
}}
|
|
82
|
+
icon="📊"
|
|
83
|
+
trend="up"
|
|
84
|
+
color="blue"
|
|
85
|
+
sparklineData={projectsSparkline}
|
|
86
|
+
showSparkline
|
|
87
|
+
lastSynced={new Date().toISOString()}
|
|
88
|
+
helpText={`${projects.length} total projects (${activeProjects} active)`}
|
|
89
|
+
/>
|
|
90
|
+
<EnhancedKPICard
|
|
91
|
+
title="Open Tasks"
|
|
92
|
+
value={activeTasks.toString()}
|
|
93
|
+
change={{
|
|
94
|
+
value: 8.3,
|
|
95
|
+
period: 'vs last week'
|
|
96
|
+
}}
|
|
97
|
+
icon="✓"
|
|
98
|
+
trend="up"
|
|
99
|
+
color="orange"
|
|
100
|
+
sparklineData={tasksSparkline}
|
|
101
|
+
showSparkline
|
|
102
|
+
lastSynced={new Date().toISOString()}
|
|
103
|
+
helpText={`${doneTasks} completed tasks out of ${tasks.length} total`}
|
|
104
|
+
/>
|
|
105
|
+
<EnhancedKPICard
|
|
106
|
+
title="Team Members"
|
|
107
|
+
value={team.length.toString()}
|
|
108
|
+
change={{
|
|
109
|
+
value: 5.0,
|
|
110
|
+
period: 'vs last month'
|
|
111
|
+
}}
|
|
112
|
+
icon="👥"
|
|
113
|
+
trend="up"
|
|
114
|
+
color="green"
|
|
115
|
+
sparklineData={teamSparkline}
|
|
116
|
+
showSparkline
|
|
117
|
+
lastSynced={new Date().toISOString()}
|
|
118
|
+
helpText="Active team members with access"
|
|
119
|
+
/>
|
|
120
|
+
<EnhancedKPICard
|
|
121
|
+
title="Completion Rate"
|
|
122
|
+
value={`${completionRate}%`}
|
|
123
|
+
change={{
|
|
124
|
+
value: completionRate > 0 ? 3.2 : 0,
|
|
125
|
+
period: 'vs last week'
|
|
126
|
+
}}
|
|
127
|
+
icon="📈"
|
|
128
|
+
trend={completionRate >= 50 ? 'up' : completionRate > 0 ? 'neutral' : 'down'}
|
|
129
|
+
color={completionRate >= 50 ? 'green' : completionRate > 0 ? 'blue' : 'red'}
|
|
130
|
+
sparklineData={completionSparkline}
|
|
131
|
+
showSparkline
|
|
132
|
+
lastSynced={new Date().toISOString()}
|
|
133
|
+
helpText="Percentage of tasks marked as complete"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { DataTable } from '@varity-labs/ui-kit';
|
|
5
|
+
import { TaskStatusBadge, PriorityBadge } from '@varity-labs/ui-kit';
|
|
6
|
+
import { formatRelativeDate } from '@/lib/utils';
|
|
7
|
+
import { ArrowRight } from 'lucide-react';
|
|
8
|
+
import type { Task } from '@/types';
|
|
9
|
+
|
|
10
|
+
interface RecentActivityProps {
|
|
11
|
+
tasks: Task[];
|
|
12
|
+
loading?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function RecentActivity({ tasks, loading = false }: RecentActivityProps) {
|
|
16
|
+
const recentTasks = [...tasks]
|
|
17
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
18
|
+
.slice(0, 5);
|
|
19
|
+
|
|
20
|
+
const columns = [
|
|
21
|
+
{ key: 'title', header: 'Task', sortable: true },
|
|
22
|
+
{
|
|
23
|
+
key: 'status',
|
|
24
|
+
header: 'Status',
|
|
25
|
+
render: (value: string) => <TaskStatusBadge status={value} />,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: 'priority',
|
|
29
|
+
header: 'Priority',
|
|
30
|
+
render: (value: string) => <PriorityBadge priority={value} />,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: 'createdAt',
|
|
34
|
+
header: 'Created',
|
|
35
|
+
render: (value: string) => formatRelativeDate(value),
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
41
|
+
<div className="mb-4 flex items-center justify-between">
|
|
42
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
|
43
|
+
Recent Activity
|
|
44
|
+
</h3>
|
|
45
|
+
{tasks.length > 5 && (
|
|
46
|
+
<Link
|
|
47
|
+
href="/dashboard/tasks"
|
|
48
|
+
className="flex items-center gap-1 text-sm font-medium text-primary-600 hover:text-primary-700 transition-colors"
|
|
49
|
+
>
|
|
50
|
+
View all tasks
|
|
51
|
+
<ArrowRight className="h-4 w-4" />
|
|
52
|
+
</Link>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
<DataTable
|
|
56
|
+
columns={columns}
|
|
57
|
+
data={recentTasks}
|
|
58
|
+
loading={loading}
|
|
59
|
+
emptyMessage="No tasks yet. Create a project and add tasks to get started."
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { ArrowRight, CheckCircle2 } from 'lucide-react';
|
|
3
|
+
import { APP_NAME } from '@/lib/constants';
|
|
4
|
+
|
|
5
|
+
const perks = [
|
|
6
|
+
'Free to get started',
|
|
7
|
+
'No credit card required',
|
|
8
|
+
'Set up in under a minute',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function CTA() {
|
|
12
|
+
return (
|
|
13
|
+
<section className="relative overflow-hidden bg-gray-900 py-24">
|
|
14
|
+
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-primary-900/20 via-transparent to-transparent" />
|
|
15
|
+
<div className="relative mx-auto max-w-7xl px-4 text-center">
|
|
16
|
+
<h2 className="heading-section text-white">
|
|
17
|
+
Ready to get your team organized?
|
|
18
|
+
</h2>
|
|
19
|
+
<p className="mx-auto mt-4 max-w-xl text-lg text-gray-400">
|
|
20
|
+
Join teams who use {APP_NAME} to ship projects on time.
|
|
21
|
+
</p>
|
|
22
|
+
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-2">
|
|
23
|
+
{perks.map((perk) => (
|
|
24
|
+
<div key={perk} className="flex items-center gap-2 text-sm text-gray-300">
|
|
25
|
+
<CheckCircle2 className="h-4 w-4 text-green-400" />
|
|
26
|
+
{perk}
|
|
27
|
+
</div>
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
<div className="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
|
|
31
|
+
<Link
|
|
32
|
+
href="/login"
|
|
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"
|
|
34
|
+
>
|
|
35
|
+
Get Started Free
|
|
36
|
+
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
|
|
37
|
+
</Link>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
);
|
|
42
|
+
}
|