@digilogiclabs/create-saas-app 2.10.0 → 2.11.0

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 (60) hide show
  1. package/README.md +153 -113
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cli/commands/create.d.ts.map +1 -1
  4. package/dist/cli/commands/create.js +2 -6
  5. package/dist/cli/commands/create.js.map +1 -1
  6. package/dist/templates/web/ai-platform/template/src/app/api/auth/route.ts +57 -0
  7. package/dist/templates/web/ai-platform/template/src/app/login/page.tsx +112 -0
  8. package/dist/templates/web/ai-platform/template/src/app/models/page.tsx +186 -0
  9. package/dist/templates/web/ai-platform/template/src/app/playground/page.tsx +251 -0
  10. package/dist/templates/web/ai-platform/template/src/app/settings/page.tsx +190 -0
  11. package/dist/templates/web/ai-platform/template/src/app/signup/page.tsx +133 -0
  12. package/dist/templates/web/ai-platform/template/src/lib/auth-session.ts +52 -0
  13. package/dist/templates/web/iot-dashboard/template/src/app/alerts/page.tsx +157 -0
  14. package/dist/templates/web/iot-dashboard/template/src/app/api/auth/route.ts +57 -0
  15. package/dist/templates/web/iot-dashboard/template/src/app/devices/[id]/page.tsx +204 -0
  16. package/dist/templates/web/iot-dashboard/template/src/app/devices/new/page.tsx +139 -0
  17. package/dist/templates/web/iot-dashboard/template/src/app/devices/page.tsx +171 -0
  18. package/dist/templates/web/iot-dashboard/template/src/app/login/page.tsx +112 -0
  19. package/dist/templates/web/iot-dashboard/template/src/app/settings/page.tsx +186 -0
  20. package/dist/templates/web/iot-dashboard/template/src/app/signup/page.tsx +133 -0
  21. package/dist/templates/web/iot-dashboard/template/src/lib/auth-session.ts +52 -0
  22. package/dist/templates/web/marketplace/template/src/app/api/auth/route.ts +57 -0
  23. package/dist/templates/web/marketplace/template/src/app/login/page.tsx +112 -0
  24. package/dist/templates/web/marketplace/template/src/app/orders/page.tsx +160 -0
  25. package/dist/templates/web/marketplace/template/src/app/products/[id]/page.tsx +218 -0
  26. package/dist/templates/web/marketplace/template/src/app/settings/page.tsx +150 -0
  27. package/dist/templates/web/marketplace/template/src/app/signup/page.tsx +133 -0
  28. package/dist/templates/web/marketplace/template/src/lib/auth-session.ts +52 -0
  29. package/dist/templates/web/micro-saas/template/src/app/api/auth/route.ts +57 -0
  30. package/dist/templates/web/micro-saas/template/src/app/login/page.tsx +14 -3
  31. package/dist/templates/web/micro-saas/template/src/app/signup/page.tsx +15 -4
  32. package/dist/templates/web/micro-saas/template/src/lib/auth-session.ts +52 -0
  33. package/package.json +1 -1
  34. package/src/templates/web/ai-platform/template/src/app/api/auth/route.ts +57 -0
  35. package/src/templates/web/ai-platform/template/src/app/login/page.tsx +112 -0
  36. package/src/templates/web/ai-platform/template/src/app/models/page.tsx +186 -0
  37. package/src/templates/web/ai-platform/template/src/app/playground/page.tsx +251 -0
  38. package/src/templates/web/ai-platform/template/src/app/settings/page.tsx +190 -0
  39. package/src/templates/web/ai-platform/template/src/app/signup/page.tsx +133 -0
  40. package/src/templates/web/ai-platform/template/src/lib/auth-session.ts +52 -0
  41. package/src/templates/web/iot-dashboard/template/src/app/alerts/page.tsx +157 -0
  42. package/src/templates/web/iot-dashboard/template/src/app/api/auth/route.ts +57 -0
  43. package/src/templates/web/iot-dashboard/template/src/app/devices/[id]/page.tsx +204 -0
  44. package/src/templates/web/iot-dashboard/template/src/app/devices/new/page.tsx +139 -0
  45. package/src/templates/web/iot-dashboard/template/src/app/devices/page.tsx +171 -0
  46. package/src/templates/web/iot-dashboard/template/src/app/login/page.tsx +112 -0
  47. package/src/templates/web/iot-dashboard/template/src/app/settings/page.tsx +186 -0
  48. package/src/templates/web/iot-dashboard/template/src/app/signup/page.tsx +133 -0
  49. package/src/templates/web/iot-dashboard/template/src/lib/auth-session.ts +52 -0
  50. package/src/templates/web/marketplace/template/src/app/api/auth/route.ts +57 -0
  51. package/src/templates/web/marketplace/template/src/app/login/page.tsx +112 -0
  52. package/src/templates/web/marketplace/template/src/app/orders/page.tsx +160 -0
  53. package/src/templates/web/marketplace/template/src/app/products/[id]/page.tsx +218 -0
  54. package/src/templates/web/marketplace/template/src/app/settings/page.tsx +150 -0
  55. package/src/templates/web/marketplace/template/src/app/signup/page.tsx +133 -0
  56. package/src/templates/web/marketplace/template/src/lib/auth-session.ts +52 -0
  57. package/src/templates/web/micro-saas/template/src/app/api/auth/route.ts +57 -0
  58. package/src/templates/web/micro-saas/template/src/app/login/page.tsx +14 -3
  59. package/src/templates/web/micro-saas/template/src/app/signup/page.tsx +15 -4
  60. package/src/templates/web/micro-saas/template/src/lib/auth-session.ts +52 -0
@@ -0,0 +1,190 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Button, Card } from '@digilogiclabs/saas-factory-ui'
5
+ import { User, Key, CreditCard, Copy, Eye, EyeOff, Trash2, Plus, Zap } from 'lucide-react'
6
+
7
+ const MOCK_KEYS = [
8
+ { id: '1', name: 'Production', key: 'sk-proj-abc...xyz', created: '2026-01-15', lastUsed: '2 hours ago' },
9
+ { id: '2', name: 'Development', key: 'sk-proj-def...uvw', created: '2026-02-20', lastUsed: '3 days ago' },
10
+ ]
11
+
12
+ export default function SettingsPage() {
13
+ const [activeTab, setActiveTab] = useState<'profile' | 'keys' | 'billing'>('profile')
14
+ const [name, setName] = useState('Test User')
15
+ const [revealedKeys, setRevealedKeys] = useState<Set<string>>(new Set())
16
+
17
+ const toggleReveal = (id: string) => {
18
+ setRevealedKeys(prev => {
19
+ const next = new Set(prev)
20
+ if (next.has(id)) next.delete(id)
21
+ else next.add(id)
22
+ return next
23
+ })
24
+ }
25
+
26
+ const tabs = [
27
+ { id: 'profile' as const, label: 'Profile', icon: User },
28
+ { id: 'keys' as const, label: 'API Keys', icon: Key },
29
+ { id: 'billing' as const, label: 'Billing', icon: CreditCard },
30
+ ]
31
+
32
+ return (
33
+ <div className="min-h-screen bg-background">
34
+ <div className="max-w-4xl mx-auto px-4 sm:px-6 py-8">
35
+ <h1 className="text-3xl font-bold text-foreground mb-8">Settings</h1>
36
+
37
+ {/* Tabs */}
38
+ <div className="flex gap-1 mb-8 border-b border-border" role="tablist">
39
+ {tabs.map(({ id, label, icon: Icon }) => (
40
+ <button
41
+ key={id}
42
+ role="tab"
43
+ aria-selected={activeTab === id}
44
+ onClick={() => setActiveTab(id)}
45
+ className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px ${
46
+ activeTab === id
47
+ ? 'border-primary text-foreground'
48
+ : 'border-transparent text-muted-foreground hover:text-foreground'
49
+ }`}
50
+ >
51
+ <Icon className="w-4 h-4" />
52
+ {label}
53
+ </button>
54
+ ))}
55
+ </div>
56
+
57
+ {/* Profile Tab */}
58
+ {activeTab === 'profile' && (
59
+ <Card className="p-6 bg-secondary border-border">
60
+ <h2 className="text-lg font-semibold text-foreground mb-6">Profile Information</h2>
61
+ <div className="space-y-4 max-w-md">
62
+ <div>
63
+ <label htmlFor="name" className="block text-sm font-medium mb-1.5">Name</label>
64
+ <input
65
+ id="name"
66
+ type="text"
67
+ value={name}
68
+ onChange={(e) => setName(e.target.value)}
69
+ className="w-full px-3 py-2.5 rounded-lg border border-input bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
70
+ />
71
+ </div>
72
+ <div>
73
+ <label htmlFor="email" className="block text-sm font-medium mb-1.5">Email</label>
74
+ <input
75
+ id="email"
76
+ type="email"
77
+ value="user@example.com"
78
+ readOnly
79
+ className="w-full px-3 py-2.5 rounded-lg border border-input bg-muted text-muted-foreground cursor-not-allowed"
80
+ />
81
+ <p className="text-xs text-muted-foreground mt-1">Contact support to change your email.</p>
82
+ </div>
83
+ <Button className="mt-4">Save Changes</Button>
84
+ </div>
85
+ </Card>
86
+ )}
87
+
88
+ {/* API Keys Tab */}
89
+ {activeTab === 'keys' && (
90
+ <div className="space-y-6">
91
+ <div className="flex items-center justify-between">
92
+ <p className="text-sm text-muted-foreground">
93
+ API keys allow programmatic access to the platform. Keep them secret.
94
+ </p>
95
+ <Button size="sm">
96
+ <Plus className="w-4 h-4 mr-1" />
97
+ Create Key
98
+ </Button>
99
+ </div>
100
+
101
+ <div className="space-y-3">
102
+ {MOCK_KEYS.map((apiKey) => (
103
+ <Card key={apiKey.id} className="p-4 bg-secondary border-border">
104
+ <div className="flex items-center justify-between">
105
+ <div className="flex-1">
106
+ <div className="flex items-center gap-3 mb-1">
107
+ <span className="font-medium text-foreground">{apiKey.name}</span>
108
+ <span className="text-xs text-muted-foreground">Created {apiKey.created}</span>
109
+ </div>
110
+ <div className="flex items-center gap-2">
111
+ <code className="text-sm bg-muted px-2 py-0.5 rounded font-mono text-muted-foreground">
112
+ {revealedKeys.has(apiKey.id) ? 'sk-proj-abcdef1234567890xyz' : apiKey.key}
113
+ </code>
114
+ <button
115
+ onClick={() => toggleReveal(apiKey.id)}
116
+ className="text-muted-foreground hover:text-foreground"
117
+ aria-label={revealedKeys.has(apiKey.id) ? 'Hide key' : 'Show key'}
118
+ >
119
+ {revealedKeys.has(apiKey.id) ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
120
+ </button>
121
+ <button className="text-muted-foreground hover:text-foreground" aria-label="Copy key">
122
+ <Copy className="w-4 h-4" />
123
+ </button>
124
+ </div>
125
+ <p className="text-xs text-muted-foreground mt-1">Last used {apiKey.lastUsed}</p>
126
+ </div>
127
+ <Button variant="ghost" size="sm" className="text-destructive hover:text-destructive/80" aria-label="Delete key">
128
+ <Trash2 className="w-4 h-4" />
129
+ </Button>
130
+ </div>
131
+ </Card>
132
+ ))}
133
+ </div>
134
+ </div>
135
+ )}
136
+
137
+ {/* Billing Tab */}
138
+ {activeTab === 'billing' && (
139
+ <div className="space-y-6">
140
+ <Card className="p-6 bg-secondary border-border">
141
+ <div className="flex items-center justify-between mb-4">
142
+ <div>
143
+ <h2 className="text-lg font-semibold text-foreground">Free Tier</h2>
144
+ <p className="text-sm text-muted-foreground">Your current plan</p>
145
+ </div>
146
+ <span className="text-xs px-2.5 py-1 rounded-full bg-primary/10 text-primary font-medium">Active</span>
147
+ </div>
148
+
149
+ <div className="mb-4">
150
+ <div className="flex items-center justify-between text-sm mb-1">
151
+ <span className="text-muted-foreground">Token usage this month</span>
152
+ <span className="text-foreground font-medium">45,678 / 100,000</span>
153
+ </div>
154
+ <div className="w-full h-2 rounded-full bg-muted overflow-hidden">
155
+ <div className="h-full rounded-full bg-primary" style={{ width: '46%' }} />
156
+ </div>
157
+ </div>
158
+
159
+ <div className="grid grid-cols-3 gap-4 pt-4 border-t border-border">
160
+ <div>
161
+ <p className="text-xs text-muted-foreground">API Calls</p>
162
+ <p className="text-lg font-bold text-foreground">1,234</p>
163
+ </div>
164
+ <div>
165
+ <p className="text-xs text-muted-foreground">Models Used</p>
166
+ <p className="text-lg font-bold text-foreground">3</p>
167
+ </div>
168
+ <div>
169
+ <p className="text-xs text-muted-foreground">Avg Response</p>
170
+ <p className="text-lg font-bold text-foreground">1.2s</p>
171
+ </div>
172
+ </div>
173
+ </Card>
174
+
175
+ <Card className="p-6 bg-gradient-to-r from-brand-from/10 via-brand-via/10 to-brand-to/10 border-primary/20">
176
+ <div className="flex items-center gap-3 mb-3">
177
+ <Zap className="w-5 h-5 text-primary" />
178
+ <h3 className="font-semibold text-foreground">Upgrade to Pro</h3>
179
+ </div>
180
+ <p className="text-sm text-muted-foreground mb-4">
181
+ Get 1M tokens/month, all models, priority support, and custom fine-tuning.
182
+ </p>
183
+ <Button>Upgrade — $29/month</Button>
184
+ </Card>
185
+ </div>
186
+ )}
187
+ </div>
188
+ </div>
189
+ )
190
+ }
@@ -0,0 +1,133 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Button, Card, CardContent } from '@digilogiclabs/saas-factory-ui'
5
+ import { Bot } from 'lucide-react'
6
+ import { useRouter } from 'next/navigation'
7
+ import Link from 'next/link'
8
+
9
+ export default function SignupPage() {
10
+ const [email, setEmail] = useState('')
11
+ const [password, setPassword] = useState('')
12
+ const [confirmPassword, setConfirmPassword] = useState('')
13
+ const [error, setError] = useState('')
14
+ const [loading, setLoading] = useState(false)
15
+ const router = useRouter()
16
+
17
+ const handleSubmit = async (e: React.FormEvent) => {
18
+ e.preventDefault()
19
+ setError('')
20
+ if (password !== confirmPassword) {
21
+ setError('Passwords do not match')
22
+ return
23
+ }
24
+ setLoading(true)
25
+ try {
26
+ const res = await fetch('/api/auth', {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ email, password, action: 'signup' }),
30
+ })
31
+ if (!res.ok) {
32
+ const data = await res.json()
33
+ throw new Error(data.error || 'Failed to create account')
34
+ }
35
+ router.push('/dashboard')
36
+ router.refresh()
37
+ } catch (err) {
38
+ setError(err instanceof Error ? err.message : 'Failed to create account')
39
+ } finally {
40
+ setLoading(false)
41
+ }
42
+ }
43
+
44
+ return (
45
+ <div className="min-h-screen flex items-center justify-center bg-background px-4">
46
+ <div className="w-full max-w-md">
47
+ {/* Logo */}
48
+ <Link href="/" className="flex items-center justify-center gap-2 mb-8 hover:opacity-80 transition-opacity">
49
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-from via-brand-via to-brand-to flex items-center justify-center">
50
+ <Bot className="w-5 h-5 text-white" />
51
+ </div>
52
+ <span className="font-bold text-xl">AI Platform</span>
53
+ </Link>
54
+
55
+ <Card className="border border-border">
56
+ <CardContent className="p-8">
57
+ <div className="text-center mb-8">
58
+ <h1 className="text-2xl font-bold">Create your account</h1>
59
+ <p className="text-muted-foreground mt-2">Get started with AI models</p>
60
+ </div>
61
+
62
+ {error && (
63
+ <div className="bg-destructive/10 text-destructive p-3 rounded-lg mb-6 text-sm" role="alert">
64
+ {error}
65
+ </div>
66
+ )}
67
+
68
+ <form onSubmit={handleSubmit} className="space-y-4">
69
+ <div>
70
+ <label htmlFor="email" className="block text-sm font-medium mb-1.5">
71
+ Email
72
+ </label>
73
+ <input
74
+ id="email"
75
+ type="email"
76
+ value={email}
77
+ onChange={(e) => setEmail(e.target.value)}
78
+ className="w-full px-3 py-2.5 rounded-lg border border-input bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent transition-shadow"
79
+ placeholder="you@example.com"
80
+ required
81
+ />
82
+ </div>
83
+
84
+ <div>
85
+ <label htmlFor="password" className="block text-sm font-medium mb-1.5">
86
+ Password
87
+ </label>
88
+ <input
89
+ id="password"
90
+ type="password"
91
+ value={password}
92
+ onChange={(e) => setPassword(e.target.value)}
93
+ className="w-full px-3 py-2.5 rounded-lg border border-input bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent transition-shadow"
94
+ placeholder="Minimum 8 characters"
95
+ required
96
+ minLength={8}
97
+ />
98
+ </div>
99
+
100
+ <div>
101
+ <label htmlFor="confirmPassword" className="block text-sm font-medium mb-1.5">
102
+ Confirm Password
103
+ </label>
104
+ <input
105
+ id="confirmPassword"
106
+ type="password"
107
+ value={confirmPassword}
108
+ onChange={(e) => setConfirmPassword(e.target.value)}
109
+ className="w-full px-3 py-2.5 rounded-lg border border-input bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent transition-shadow"
110
+ placeholder="Re-enter your password"
111
+ required
112
+ />
113
+ </div>
114
+
115
+ <Button type="submit" className="w-full h-11 text-base cursor-pointer hover:brightness-110 active:scale-[0.98] transition-all duration-200" disabled={loading}>
116
+ {loading ? 'Creating account...' : 'Create Account'}
117
+ </Button>
118
+ </form>
119
+
120
+ <div className="mt-6 text-center">
121
+ <p className="text-muted-foreground text-sm">
122
+ Already have an account?{' '}
123
+ <Link href="/login" className="text-primary hover:underline font-medium">
124
+ Sign in
125
+ </Link>
126
+ </p>
127
+ </div>
128
+ </CardContent>
129
+ </Card>
130
+ </div>
131
+ </div>
132
+ )
133
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Auth session adapter — demo implementation.
3
+ *
4
+ * Replace this file with a real provider when configuring your app:
5
+ * - Keycloak: import { auth } from '@/auth' (Auth.js)
6
+ * - Supabase: import { createClient } from '@/lib/supabase/server'
7
+ * - Firebase: import { getServerSession } from 'next-auth'
8
+ *
9
+ * The getSession() and getUser() exports are consumed by auth-server.ts
10
+ * and must return the same shape regardless of provider.
11
+ */
12
+ import 'server-only'
13
+ import { cookies } from 'next/headers'
14
+
15
+ type SessionUser = {
16
+ id?: string
17
+ email?: string | null
18
+ name?: string | null
19
+ roles?: string[]
20
+ }
21
+
22
+ type Session = {
23
+ user?: SessionUser
24
+ }
25
+
26
+ /**
27
+ * Get the current session from cookies.
28
+ * Demo: reads a JSON cookie set at login. Replace with your auth provider.
29
+ */
30
+ export async function getSession(): Promise<Session | null> {
31
+ const cookieStore = await cookies()
32
+ const sessionCookie = cookieStore.get('session')
33
+
34
+ if (!sessionCookie?.value) {
35
+ return null
36
+ }
37
+
38
+ try {
39
+ const session = JSON.parse(sessionCookie.value) as Session
40
+ return session
41
+ } catch {
42
+ return null
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Get the current authenticated user, or null.
48
+ */
49
+ export async function getUser(): Promise<SessionUser | null> {
50
+ const session = await getSession()
51
+ return session?.user ?? null
52
+ }
@@ -0,0 +1,157 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Button, Card } from '@digilogiclabs/saas-factory-ui'
5
+ import { Bell, CheckCircle } from 'lucide-react'
6
+
7
+ type Severity = 'critical' | 'warning' | 'info'
8
+ type AlertStatus = 'active' | 'acknowledged'
9
+
10
+ interface Alert {
11
+ id: number
12
+ severity: Severity
13
+ message: string
14
+ device: string
15
+ timestamp: string
16
+ status: AlertStatus
17
+ }
18
+
19
+ const ALERTS: Alert[] = [
20
+ { id: 1, severity: 'critical', message: 'Temperature exceeded 45°C threshold', device: 'Temp Sensor T-001', timestamp: '2 min ago', status: 'active' },
21
+ { id: 2, severity: 'critical', message: 'Device unresponsive for 10+ minutes', device: 'Motion Detector M-001', timestamp: '15 min ago', status: 'active' },
22
+ { id: 3, severity: 'warning', message: 'Signal strength below -80 dBm', device: 'Gateway B2', timestamp: '28 min ago', status: 'active' },
23
+ { id: 4, severity: 'warning', message: 'Firmware update available (v2.5.0)', device: 'Sensor Hub A1', timestamp: '1 hour ago', status: 'active' },
24
+ { id: 5, severity: 'info', message: 'Scheduled maintenance window approaching', device: 'All Devices', timestamp: '2 hours ago', status: 'active' },
25
+ { id: 6, severity: 'warning', message: 'Battery level below 20%', device: 'Humidity Sensor H-001', timestamp: '3 hours ago', status: 'acknowledged' },
26
+ { id: 7, severity: 'critical', message: 'Pressure reading outside safe range', device: 'Pressure Gauge P-001', timestamp: '4 hours ago', status: 'acknowledged' },
27
+ { id: 8, severity: 'info', message: 'New device registered successfully', device: 'Temp Sensor T-002', timestamp: '5 hours ago', status: 'acknowledged' },
28
+ { id: 9, severity: 'warning', message: 'High packet loss detected on network', device: 'Gateway B2', timestamp: '6 hours ago', status: 'active' },
29
+ { id: 10, severity: 'info', message: 'Daily telemetry summary generated', device: 'System', timestamp: '8 hours ago', status: 'acknowledged' },
30
+ ]
31
+
32
+ const SEVERITY_OPTIONS = ['All', 'Critical', 'Warning', 'Info']
33
+ const STATUS_OPTIONS = ['All', 'Active', 'Acknowledged']
34
+
35
+ function severityDotColor(severity: Severity): string {
36
+ switch (severity) {
37
+ case 'critical': return 'bg-red-500'
38
+ case 'warning': return 'bg-amber-500'
39
+ case 'info': return 'bg-blue-500'
40
+ }
41
+ }
42
+
43
+ function severityLabel(severity: Severity): string {
44
+ return severity.charAt(0).toUpperCase() + severity.slice(1)
45
+ }
46
+
47
+ export default function AlertsPage() {
48
+ const [severityFilter, setSeverityFilter] = useState('All')
49
+ const [statusFilter, setStatusFilter] = useState('All')
50
+ const [alerts, setAlerts] = useState(ALERTS)
51
+
52
+ const filtered = alerts.filter((a) => {
53
+ if (severityFilter !== 'All' && a.severity !== severityFilter.toLowerCase()) return false
54
+ if (statusFilter !== 'All' && a.status !== statusFilter.toLowerCase()) return false
55
+ return true
56
+ })
57
+
58
+ const activeCount = alerts.filter((a) => a.status === 'active').length
59
+
60
+ const handleAcknowledge = (id: number) => {
61
+ setAlerts((prev) =>
62
+ prev.map((a) => (a.id === id ? { ...a, status: 'acknowledged' as AlertStatus } : a))
63
+ )
64
+ }
65
+
66
+ return (
67
+ <div className="min-h-screen bg-background">
68
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 py-8">
69
+ {/* Header */}
70
+ <div className="mb-8">
71
+ <h1 className="text-2xl font-bold text-foreground">Alerts</h1>
72
+ <p className="text-sm text-muted-foreground">{activeCount} active alert{activeCount !== 1 ? 's' : ''}</p>
73
+ </div>
74
+
75
+ {/* Filter Bar */}
76
+ <Card className="p-4 mb-6">
77
+ <div className="flex flex-col sm:flex-row gap-3">
78
+ <select
79
+ value={severityFilter}
80
+ onChange={(e) => setSeverityFilter(e.target.value)}
81
+ className="px-3 py-2 rounded-md border border-input bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-ring"
82
+ >
83
+ {SEVERITY_OPTIONS.map((s) => (
84
+ <option key={s} value={s}>{s === 'All' ? 'All Severities' : s}</option>
85
+ ))}
86
+ </select>
87
+ <select
88
+ value={statusFilter}
89
+ onChange={(e) => setStatusFilter(e.target.value)}
90
+ className="px-3 py-2 rounded-md border border-input bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-ring"
91
+ >
92
+ {STATUS_OPTIONS.map((s) => (
93
+ <option key={s} value={s}>{s === 'All' ? 'All Statuses' : s}</option>
94
+ ))}
95
+ </select>
96
+ </div>
97
+ </Card>
98
+
99
+ {/* Alert List */}
100
+ {filtered.length === 0 ? (
101
+ <Card className="p-0">
102
+ <div className="flex flex-col items-center justify-center py-16 px-4">
103
+ <div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
104
+ <Bell className="w-8 h-8 text-muted-foreground" />
105
+ </div>
106
+ <p className="text-foreground font-medium mb-1">No alerts found</p>
107
+ <p className="text-sm text-muted-foreground">Try adjusting your filters.</p>
108
+ </div>
109
+ </Card>
110
+ ) : (
111
+ <div className="space-y-3">
112
+ {filtered.map((alert) => (
113
+ <Card key={alert.id} className="p-4">
114
+ <div className="flex items-start gap-3">
115
+ <div className={`w-2.5 h-2.5 rounded-full mt-1.5 shrink-0 ${severityDotColor(alert.severity)}`} />
116
+ <div className="flex-1 min-w-0">
117
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
118
+ <div>
119
+ <p className="text-sm font-medium text-foreground">{alert.message}</p>
120
+ <div className="flex items-center gap-3 mt-1">
121
+ <span className="text-xs text-muted-foreground">{alert.device}</span>
122
+ <span className="text-xs text-muted-foreground">{alert.timestamp}</span>
123
+ <span className={`text-xs px-1.5 py-0.5 rounded ${
124
+ alert.severity === 'critical'
125
+ ? 'bg-red-500/10 text-red-600'
126
+ : alert.severity === 'warning'
127
+ ? 'bg-amber-500/10 text-amber-600'
128
+ : 'bg-blue-500/10 text-blue-600'
129
+ }`}>
130
+ {severityLabel(alert.severity)}
131
+ </span>
132
+ </div>
133
+ </div>
134
+ {alert.status === 'active' ? (
135
+ <Button
136
+ variant="outline"
137
+ size="sm"
138
+ onClick={() => handleAcknowledge(alert.id)}
139
+ className="shrink-0"
140
+ >
141
+ <CheckCircle className="w-4 h-4 mr-1" />
142
+ Acknowledge
143
+ </Button>
144
+ ) : (
145
+ <span className="text-xs text-muted-foreground italic shrink-0">Acknowledged</span>
146
+ )}
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </Card>
151
+ ))}
152
+ </div>
153
+ )}
154
+ </div>
155
+ </div>
156
+ )
157
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Demo auth API routes.
3
+ *
4
+ * POST /api/auth — Login or Signup
5
+ * DELETE /api/auth — Logout
6
+ *
7
+ * Replace with your real auth provider (Auth.js, Supabase Auth, etc.)
8
+ * when configuring your app for production.
9
+ */
10
+ import { NextRequest, NextResponse } from 'next/server'
11
+ import { cookies } from 'next/headers'
12
+
13
+ export async function POST(request: NextRequest) {
14
+ try {
15
+ const body = await request.json()
16
+ const { email, password, action } = body
17
+
18
+ if (!email || !password) {
19
+ return NextResponse.json(
20
+ { error: 'Email and password are required' },
21
+ { status: 400 }
22
+ )
23
+ }
24
+
25
+ // Demo: accept any email/password combination.
26
+ // Replace with real authentication (database lookup, bcrypt compare, etc.)
27
+ const user = {
28
+ id: crypto.randomUUID(),
29
+ email,
30
+ name: email.split('@')[0],
31
+ }
32
+
33
+ const session = JSON.stringify({ user })
34
+ const cookieStore = await cookies()
35
+
36
+ cookieStore.set('session', session, {
37
+ httpOnly: true,
38
+ secure: process.env.NODE_ENV === 'production',
39
+ sameSite: 'lax',
40
+ path: '/',
41
+ maxAge: 60 * 60 * 24 * 7, // 7 days
42
+ })
43
+
44
+ return NextResponse.json({ user })
45
+ } catch {
46
+ return NextResponse.json(
47
+ { error: 'Authentication failed' },
48
+ { status: 500 }
49
+ )
50
+ }
51
+ }
52
+
53
+ export async function DELETE() {
54
+ const cookieStore = await cookies()
55
+ cookieStore.delete('session')
56
+ return NextResponse.json({ success: true })
57
+ }