Skip to content

Commit df8ada4

Browse files
multiple improvements
1 parent e3dc207 commit df8ada4

10 files changed

Lines changed: 858 additions & 104 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import Link from 'next/link'
5+
import { createClient } from '@/lib/supabase/client'
6+
import { Button } from '@/components/ui/button'
7+
import { Input } from '@/components/ui/input'
8+
import { Label } from '@/components/ui/label'
9+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
10+
import { toast } from 'sonner'
11+
import { Loader2 } from 'lucide-react'
12+
13+
export default function ForgotPasswordPage() {
14+
const [email, setEmail] = useState('')
15+
const [loading, setLoading] = useState(false)
16+
const [sent, setSent] = useState(false)
17+
const supabase = createClient()
18+
19+
async function handleSubmit(e: React.FormEvent) {
20+
e.preventDefault()
21+
setLoading(true)
22+
23+
const { error } = await supabase.auth.resetPasswordForEmail(email, {
24+
redirectTo: `${window.location.origin}/reset-password`,
25+
})
26+
27+
if (error) {
28+
toast.error(error.message)
29+
setLoading(false)
30+
return
31+
}
32+
33+
setSent(true)
34+
setLoading(false)
35+
}
36+
37+
if (sent) {
38+
return (
39+
<Card>
40+
<CardHeader className="text-center">
41+
<CardTitle className="text-2xl font-bold">Check your email</CardTitle>
42+
<CardDescription>
43+
We sent a password reset link to <span className="font-medium text-foreground">{email}</span>
44+
</CardDescription>
45+
</CardHeader>
46+
<CardContent className="space-y-4">
47+
<p className="text-center text-sm text-muted-foreground">
48+
Click the link in the email to reset your password. If you don&apos;t see it, check your spam folder.
49+
</p>
50+
<Button variant="outline" className="w-full" onClick={() => setSent(false)}>
51+
Try a different email
52+
</Button>
53+
</CardContent>
54+
<CardFooter className="justify-center">
55+
<p className="text-sm text-muted-foreground">
56+
Remember your password?{' '}
57+
<Link href="/login" className="text-primary underline-offset-4 hover:underline">Sign in</Link>
58+
</p>
59+
</CardFooter>
60+
</Card>
61+
)
62+
}
63+
64+
return (
65+
<Card>
66+
<CardHeader className="text-center">
67+
<CardTitle className="text-2xl font-bold">Forgot your password?</CardTitle>
68+
<CardDescription>Enter your email and we&apos;ll send you a reset link</CardDescription>
69+
</CardHeader>
70+
<CardContent>
71+
<form onSubmit={handleSubmit} className="space-y-4">
72+
<div className="space-y-2">
73+
<Label htmlFor="email">Email</Label>
74+
<Input id="email" type="email" placeholder="you@example.com" value={email} onChange={(e) => setEmail(e.target.value)} required />
75+
</div>
76+
<Button type="submit" className="w-full" disabled={loading}>
77+
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
78+
Send reset link
79+
</Button>
80+
</form>
81+
</CardContent>
82+
<CardFooter className="justify-center">
83+
<p className="text-sm text-muted-foreground">
84+
Remember your password?{' '}
85+
<Link href="/login" className="text-primary underline-offset-4 hover:underline">Sign in</Link>
86+
</p>
87+
</CardFooter>
88+
</Card>
89+
)
90+
}

src/app/(auth)/login/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ export default function LoginPage() {
5555
<Input id="email" type="email" placeholder="you@example.com" value={email} onChange={(e) => setEmail(e.target.value)} required />
5656
</div>
5757
<div className="space-y-2">
58-
<Label htmlFor="password">Password</Label>
58+
<div className="flex items-center justify-between">
59+
<Label htmlFor="password">Password</Label>
60+
<Link href="/forgot-password" className="text-xs text-muted-foreground hover:text-primary underline-offset-4 hover:underline">
61+
Forgot password?
62+
</Link>
63+
</div>
5964
<Input id="password" type="password" placeholder="••••••••" value={password} onChange={(e) => setPassword(e.target.value)} required />
6065
</div>
6166
<Button type="submit" className="w-full" disabled={loading}>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
'use client'
2+
3+
import { useState, useEffect } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
import Link from 'next/link'
6+
import { createClient } from '@/lib/supabase/client'
7+
import { Button } from '@/components/ui/button'
8+
import { Input } from '@/components/ui/input'
9+
import { Label } from '@/components/ui/label'
10+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
11+
import { toast } from 'sonner'
12+
import { Loader2 } from 'lucide-react'
13+
14+
const MIN_PASSWORD_LENGTH = 8
15+
16+
export default function ResetPasswordPage() {
17+
const [password, setPassword] = useState('')
18+
const [confirmPassword, setConfirmPassword] = useState('')
19+
const [loading, setLoading] = useState(false)
20+
const [sessionReady, setSessionReady] = useState(false)
21+
const [checking, setChecking] = useState(true)
22+
const router = useRouter()
23+
const supabase = createClient()
24+
25+
useEffect(() => {
26+
const { data: { subscription } } = supabase.auth.onAuthStateChange((event) => {
27+
if (event === 'PASSWORD_RECOVERY') {
28+
setSessionReady(true)
29+
setChecking(false)
30+
}
31+
})
32+
33+
const timer = setTimeout(() => {
34+
setChecking(false)
35+
}, 3000)
36+
37+
return () => {
38+
subscription.unsubscribe()
39+
clearTimeout(timer)
40+
}
41+
}, [supabase.auth])
42+
43+
async function handleReset(e: React.FormEvent) {
44+
e.preventDefault()
45+
46+
if (password.length < MIN_PASSWORD_LENGTH) {
47+
toast.error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`)
48+
return
49+
}
50+
if (password !== confirmPassword) {
51+
toast.error('Passwords do not match')
52+
return
53+
}
54+
55+
setLoading(true)
56+
57+
const { error } = await supabase.auth.updateUser({ password })
58+
59+
if (error) {
60+
toast.error(error.message)
61+
setLoading(false)
62+
return
63+
}
64+
65+
toast.success('Password updated successfully')
66+
router.push('/')
67+
router.refresh()
68+
}
69+
70+
if (checking) {
71+
return (
72+
<Card>
73+
<CardContent className="flex items-center justify-center py-12">
74+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
75+
</CardContent>
76+
</Card>
77+
)
78+
}
79+
80+
if (!sessionReady) {
81+
return (
82+
<Card>
83+
<CardHeader className="text-center">
84+
<CardTitle className="text-2xl font-bold">Invalid or expired link</CardTitle>
85+
<CardDescription>This password reset link is no longer valid. Please request a new one.</CardDescription>
86+
</CardHeader>
87+
<CardContent>
88+
<Link href="/forgot-password">
89+
<Button className="w-full">Request new reset link</Button>
90+
</Link>
91+
</CardContent>
92+
<CardFooter className="justify-center">
93+
<p className="text-sm text-muted-foreground">
94+
Remember your password?{' '}
95+
<Link href="/login" className="text-primary underline-offset-4 hover:underline">Sign in</Link>
96+
</p>
97+
</CardFooter>
98+
</Card>
99+
)
100+
}
101+
102+
return (
103+
<Card>
104+
<CardHeader className="text-center">
105+
<CardTitle className="text-2xl font-bold">Set new password</CardTitle>
106+
<CardDescription>Enter your new password below</CardDescription>
107+
</CardHeader>
108+
<CardContent>
109+
<form onSubmit={handleReset} className="space-y-4">
110+
<div className="space-y-2">
111+
<Label htmlFor="password">New password</Label>
112+
<Input id="password" type="password" placeholder="••••••••" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={MIN_PASSWORD_LENGTH} />
113+
</div>
114+
<div className="space-y-2">
115+
<Label htmlFor="confirm-password">Confirm password</Label>
116+
<Input id="confirm-password" type="password" placeholder="••••••••" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} required minLength={MIN_PASSWORD_LENGTH} />
117+
</div>
118+
<Button type="submit" className="w-full" disabled={loading}>
119+
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
120+
Update password
121+
</Button>
122+
</form>
123+
</CardContent>
124+
</Card>
125+
)
126+
}
Lines changed: 101 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,116 @@
11
'use client'
22

3-
import { PanelTopOpen, Sparkles } from 'lucide-react'
3+
import { AppWindow, PanelTopOpen, Sparkles, TerminalSquare } from 'lucide-react'
4+
import { InstanceTerminalWorkspace } from '@/components/instances/instance-terminal-workspace'
5+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
6+
import { buttonVariants } from '@/components/ui/button'
47
import type { Instance } from '@/types/instance'
58

69
export function InstanceDashboard({ instance }: { instance: Instance }) {
7-
if (instance.status !== 'running' || !instance.ip_address) {
8-
return (
9-
<div className="flex flex-col items-center justify-center rounded-2xl border border-border bg-card p-12 text-center">
10-
<p className="text-sm text-muted-foreground">
11-
{instance.status === 'provisioning'
12-
? 'Dashboard will be available once provisioning completes...'
13-
: 'Instance is not running. Start it to access the dashboard.'}
14-
</p>
15-
</div>
16-
)
17-
}
18-
19-
const baseUrl = instance.dashboard_url ?? `http://${instance.ip_address}`
20-
const dashboardUrl = instance.gateway_token
21-
? `${baseUrl}/#token=${instance.gateway_token}`
22-
: baseUrl
10+
const baseUrl = instance.dashboard_url ?? (instance.ip_address ? `http://${instance.ip_address}` : null)
11+
const dashboardUrl = baseUrl
12+
? instance.gateway_token
13+
? `${baseUrl}/#token=${instance.gateway_token}`
14+
: baseUrl
15+
: null
2316

2417
return (
2518
<section className="flex min-h-[calc(100dvh-12rem)] flex-col gap-4">
26-
<div className="flex flex-col gap-3 rounded-2xl border border-border/70 bg-card/70 p-4 backdrop-blur sm:flex-row sm:items-center sm:justify-between">
27-
<div className="space-y-1">
28-
<div className="inline-flex items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">
29-
<Sparkles className="h-3.5 w-3.5 text-primary" />
30-
Embedded dashboard
31-
</div>
32-
<div>
33-
<h2 className="text-lg font-semibold tracking-tight">OpenClaw Control UI</h2>
34-
<p className="text-sm text-muted-foreground">
35-
Full-size embedded view with a quick escape hatch when you want the native tab.
36-
</p>
19+
<div className="relative overflow-hidden rounded-[1.75rem] border border-border/70 bg-card/80 p-5 backdrop-blur">
20+
<div className="absolute inset-x-0 top-0 h-28 bg-[radial-gradient(circle_at_top_left,rgba(16,185,129,0.14),transparent_52%),radial-gradient(circle_at_top_right,rgba(59,130,246,0.12),transparent_45%)]" />
21+
<div className="relative flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
22+
<div className="space-y-3">
23+
<div className="inline-flex items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">
24+
<Sparkles className="h-3.5 w-3.5 text-primary" />
25+
Embedded workspace
26+
</div>
27+
<div className="space-y-1">
28+
<h2 className="text-xl font-semibold tracking-tight">Instance dashboard</h2>
29+
<p className="max-w-2xl text-sm text-muted-foreground">
30+
The terminal workspace is the reliable default here, and the raw Control UI is still one click away when you need it.
31+
</p>
32+
</div>
3733
</div>
34+
35+
{dashboardUrl && (
36+
<a
37+
href={dashboardUrl}
38+
target="_blank"
39+
rel="noopener noreferrer"
40+
className={buttonVariants({ variant: 'outline', size: 'sm' })}
41+
>
42+
<PanelTopOpen className="h-4 w-4" />
43+
Open Control UI
44+
</a>
45+
)}
3846
</div>
39-
<a
40-
href={dashboardUrl}
41-
target="_blank"
42-
rel="noopener noreferrer"
43-
className="inline-flex items-center justify-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium transition-colors hover:bg-accent sm:self-start"
44-
>
45-
<PanelTopOpen className="h-4 w-4" />
46-
Open in new tab
47-
</a>
4847
</div>
4948

50-
<div className="relative flex min-h-0 flex-1 overflow-hidden rounded-[1.5rem] border border-border/80 bg-card shadow-sm">
51-
<div className="pointer-events-none absolute inset-x-0 top-0 z-10 h-16 bg-gradient-to-b from-background/10 to-transparent" />
52-
<iframe
53-
src={dashboardUrl}
54-
className="h-full min-h-[calc(100dvh-18rem)] w-full flex-1 bg-background"
55-
title="OpenClaw Control UI"
56-
allow="clipboard-read; clipboard-write"
57-
/>
58-
</div>
49+
<Tabs defaultValue="terminal" className="gap-4">
50+
<TabsList
51+
variant="line"
52+
className="h-auto w-fit rounded-2xl border border-border/70 bg-card/70 p-1"
53+
>
54+
<TabsTrigger value="terminal" className="px-3 py-2">
55+
<TerminalSquare className="h-4 w-4" />
56+
Terminal workspace
57+
</TabsTrigger>
58+
{dashboardUrl && (
59+
<TabsTrigger value="control" className="px-3 py-2">
60+
<AppWindow className="h-4 w-4" />
61+
Control UI
62+
</TabsTrigger>
63+
)}
64+
</TabsList>
65+
66+
<TabsContent value="terminal">
67+
<InstanceTerminalWorkspace
68+
instance={instance}
69+
showHero={false}
70+
showSummaryCards={false}
71+
/>
72+
</TabsContent>
73+
74+
{dashboardUrl && (
75+
<TabsContent value="control">
76+
<div className="space-y-4">
77+
<div className="flex flex-col gap-3 rounded-2xl border border-border/70 bg-card/70 p-4 backdrop-blur sm:flex-row sm:items-center sm:justify-between">
78+
<div className="space-y-1">
79+
<div className="inline-flex items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">
80+
<AppWindow className="h-3.5 w-3.5 text-primary" />
81+
Raw instance UI
82+
</div>
83+
<div>
84+
<h3 className="text-lg font-semibold tracking-tight">OpenClaw Control UI</h3>
85+
<p className="text-sm text-muted-foreground">
86+
This is the app that runs inside the instance itself. If its frontend assets are missing, the terminal workspace tab remains fully usable.
87+
</p>
88+
</div>
89+
</div>
90+
<a
91+
href={dashboardUrl}
92+
target="_blank"
93+
rel="noopener noreferrer"
94+
className={buttonVariants({ variant: 'outline', size: 'sm' })}
95+
>
96+
<PanelTopOpen className="h-4 w-4" />
97+
Open in new tab
98+
</a>
99+
</div>
100+
101+
<div className="relative flex min-h-0 flex-1 overflow-hidden rounded-[1.5rem] border border-border/80 bg-card shadow-sm">
102+
<div className="pointer-events-none absolute inset-x-0 top-0 z-10 h-16 bg-gradient-to-b from-background/10 to-transparent" />
103+
<iframe
104+
src={dashboardUrl}
105+
className="h-full min-h-[calc(100dvh-20rem)] w-full flex-1 bg-background"
106+
title="OpenClaw Control UI"
107+
allow="clipboard-read; clipboard-write"
108+
/>
109+
</div>
110+
</div>
111+
</TabsContent>
112+
)}
113+
</Tabs>
59114
</section>
60115
)
61116
}

0 commit comments

Comments
 (0)