Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 158 additions & 134 deletions src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@ import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Save, Eye, EyeOff, CloudUpload, CheckCircle2, XCircle, RefreshCw } from "lucide-react";
import {
Settings as SettingsIcon,
Save,
Eye,
EyeOff,
CloudUpload,
CheckCircle2,
XCircle,
RefreshCw,
} from "lucide-react";

export function SettingsPage() {
const { settings, loading, saving, save, testConnection, testResult, triggerSync, syncing } =
useSettings();

if (loading || !settings) {
return (
<div className="max-w-2xl mx-auto px-6 py-12 text-muted-foreground">Loading settings...</div>
<div className="max-w-4xl mx-auto px-6 py-12 text-muted-foreground">Loading settings...</div>
);
}

Expand Down Expand Up @@ -80,154 +89,169 @@ function SettingsForm({
};

return (
<div className="max-w-2xl mx-auto px-6 py-6 space-y-6">
<h1 className="text-2xl font-bold tracking-tight">Settings</h1>

{/* Pocket API Key */}
<Card className="bg-[#2b2b2b] border-border">
<CardContent className="pt-6 space-y-4">
<div className="min-h-full bg-background">
<div className="max-w-4xl mx-auto px-6 py-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Pocket API Key</h2>
<p className="text-sm text-muted-foreground">
Required to sync recordings.{" "}
<a
href="https://app.heypocket.com/app/settings/api-keys"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground transition-colors"
>
Get your key
</a>
<h1 className="text-lg font-bold flex items-center gap-2">
<SettingsIcon className="h-5 w-5" />
Settings
</h1>
<p className="text-xs text-muted-foreground">
Configure your Pocket API key and optional S3 backup
</p>
</div>
<div className="flex gap-2">
<Input
type={showKey ? "text" : "password"}
value={apiKey}
onChange={(e) => {
markDirty(setApiKey)(e.target.value);
setApiKeyEdited(true);
}}
placeholder="pk_your_api_key_here"
className="font-mono text-sm"
/>
<div className="flex items-center gap-3">
{saved && <span className="text-sm text-green-500">Saved</span>}
<Button
variant="outline"
size="icon"
onClick={() => setShowKey(!showKey)}
className="shrink-0 cursor-pointer"
aria-label={showKey ? "Hide API key" : "Show API key"}
onClick={handleSave}
disabled={!dirty || saving}
size="sm"
className="gap-1.5 cursor-pointer"
>
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
<Save className="h-4 w-4" />
{saving ? "Saving..." : "Save"}
</Button>
</div>
</CardContent>
</Card>
</div>

{/* S3 Backup */}
<Card className="bg-[#2b2b2b] border-border">
<CardContent className="pt-6 space-y-4">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">S3 Backup</h2>
{/* Pocket API Key */}
<div>
<h2 className="text-sm font-semibold text-muted-foreground mb-3">Pocket API Key</h2>
<Card>
<CardContent className="p-4 space-y-3">
<p className="text-xs text-muted-foreground">
Required to sync recordings.{" "}
<a
href="https://app.heypocket.com/app/settings/api-keys"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground transition-colors"
>
Get your key
</a>
</p>
<div className="flex gap-2">
<Input
type={showKey ? "text" : "password"}
value={apiKey}
onChange={(e) => {
markDirty(setApiKey)(e.target.value);
setApiKeyEdited(true);
}}
placeholder="pk_your_api_key_here"
className="font-mono text-sm"
/>
<Button
variant="outline"
size="icon"
onClick={() => setShowKey(!showKey)}
className="shrink-0 cursor-pointer"
aria-label={showKey ? "Hide API key" : "Show API key"}
>
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</CardContent>
</Card>
</div>

{/* S3 Backup */}
<div>
<div className="flex items-center gap-2 mb-3">
<h2 className="text-sm font-semibold text-muted-foreground">S3 Backup</h2>
<Badge variant="outline" className="text-xs">
Optional
</Badge>
</div>
<p className="text-sm text-muted-foreground">
Automatically sync your data to an S3 bucket after every change. Works with any
S3-compatible store (AWS, MinIO, R2, etc.).
</p>

<Separator />

<div className="space-y-3">
<div>
<label className="text-sm font-medium" htmlFor="s3-bucket">
Bucket
</label>
<Input
id="s3-bucket"
value={bucket}
onChange={(e) => markDirty(setBucket)(e.target.value)}
placeholder="my-seam-backup"
className="mt-1"
/>
</div>
<div>
<label className="text-sm font-medium" htmlFor="s3-prefix">
Prefix
</label>
<Input
id="s3-prefix"
value={prefix}
onChange={(e) => markDirty(setPrefix)(e.target.value)}
placeholder="seam/"
className="mt-1"
/>
<p className="text-xs text-muted-foreground mt-1">
Key prefix for all objects (e.g. &quot;seam/&quot;)
</p>
</div>
<div>
<label className="text-sm font-medium" htmlFor="aws-profile">
AWS Profile
</label>
<Input
id="aws-profile"
value={profile}
onChange={(e) => markDirty(setProfile)(e.target.value)}
placeholder="default"
className="mt-1"
/>
<p className="text-xs text-muted-foreground mt-1">
For SSO or named profile users. Leave blank for default credential chain.
<Card>
<CardContent className="p-4 space-y-4">
<p className="text-xs text-muted-foreground">
Automatically sync your data to an S3 bucket after every change. Works with any
S3-compatible store (AWS, MinIO, R2, etc.).
</p>
</div>
</div>

<Separator />
<Separator />

<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={testConnection}
disabled={!bucket}
className="gap-1.5 cursor-pointer"
>
{testResult?.ok ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : testResult && !testResult.ok ? (
<XCircle className="h-4 w-4 text-red-500" />
) : (
<CloudUpload className="h-4 w-4" />
)}
Test Connection
</Button>
<Button
variant="outline"
size="sm"
onClick={triggerSync}
disabled={!bucket || syncing}
className="gap-1.5 cursor-pointer"
>
<RefreshCw className={`h-4 w-4 ${syncing ? "animate-spin" : ""}`} />
{syncing ? "Syncing..." : "Sync Now"}
</Button>
{testResult && !testResult.ok && testResult.error && (
<span className="text-xs text-red-400 truncate max-w-xs">{testResult.error}</span>
)}
</div>
</CardContent>
</Card>
<div className="space-y-3">
<div>
<label className="text-xs text-muted-foreground block mb-1" htmlFor="s3-bucket">
Bucket
</label>
<Input
id="s3-bucket"
value={bucket}
onChange={(e) => markDirty(setBucket)(e.target.value)}
placeholder="my-seam-backup"
/>
</div>
<div>
<label className="text-xs text-muted-foreground block mb-1" htmlFor="s3-prefix">
Prefix
</label>
<Input
id="s3-prefix"
value={prefix}
onChange={(e) => markDirty(setPrefix)(e.target.value)}
placeholder="seam/"
/>
<p className="text-xs text-muted-foreground mt-1">
Key prefix for all objects (e.g. &quot;seam/&quot;)
</p>
</div>
<div>
<label className="text-xs text-muted-foreground block mb-1" htmlFor="aws-profile">
AWS Profile
</label>
<Input
id="aws-profile"
value={profile}
onChange={(e) => markDirty(setProfile)(e.target.value)}
placeholder="default"
/>
<p className="text-xs text-muted-foreground mt-1">
For SSO or named profile users. Leave blank for default credential chain.
</p>
</div>
</div>

<Separator />

{/* Save */}
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={!dirty || saving} className="gap-1.5 cursor-pointer">
<Save className="h-4 w-4" />
{saving ? "Saving..." : "Save"}
</Button>
{saved && <span className="text-sm text-green-500">Settings saved</span>}
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={testConnection}
disabled={!bucket}
className="gap-1.5 cursor-pointer"
>
{testResult?.ok ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : testResult && !testResult.ok ? (
<XCircle className="h-4 w-4 text-red-500" />
) : (
<CloudUpload className="h-4 w-4" />
)}
Test Connection
</Button>
<Button
variant="outline"
size="sm"
onClick={triggerSync}
disabled={!bucket || syncing}
className="gap-1.5 cursor-pointer"
>
<RefreshCw className={`h-4 w-4 ${syncing ? "animate-spin" : ""}`} />
{syncing ? "Syncing..." : "Sync Now"}
</Button>
{testResult && !testResult.ok && testResult.error && (
<span className="text-xs text-red-400 truncate max-w-xs">{testResult.error}</span>
)}
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
Expand Down
Loading