gohorsejobs/frontend/src/app/dashboard/settings/page.tsx

295 lines
14 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { settingsApi, credentialsApi, ConfiguredService } from "@/lib/api"
import { toast } from "sonner"
import { Loader2, Check, Key, Trash2, Eye, EyeOff } from "lucide-react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
import { Textarea } from "@/components/ui/textarea"
interface ThemeConfig {
logoUrl: string
primaryColor: string
companyName: string
}
const DEFAULT_THEME: ThemeConfig = {
logoUrl: "/logo.png",
primaryColor: "#000000",
companyName: "GoHorseJobs"
}
export default function SettingsPage() {
const [config, setConfig] = useState<ThemeConfig>(DEFAULT_THEME)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
// Credentials State
const [credentials, setCredentials] = useState<ConfiguredService[]>([])
const [loadingCredentials, setLoadingCredentials] = useState(false)
const [selectedService, setSelectedService] = useState<string | null>(null)
const [credentialPayload, setCredentialPayload] = useState("")
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const fetchSettings = async () => {
try {
const data = await settingsApi.get("theme")
if (data && Object.keys(data).length > 0) {
setConfig({ ...DEFAULT_THEME, ...data })
}
} catch (error) {
console.error("Failed to fetch theme settings", error)
}
}
const fetchCredentials = async () => {
setLoadingCredentials(true)
try {
const res = await credentialsApi.list()
// Ensure we handle the response correctly (api.ts wraps it in { services: ... })
if (res && res.services) {
setCredentials(res.services)
}
} catch (error) {
console.error("Failed to fetch credentials", error)
toast.error("Failed to load credentials status")
} finally {
setLoadingCredentials(false)
}
}
useEffect(() => {
Promise.all([fetchSettings(), fetchCredentials()]).finally(() => setLoading(false))
}, [])
const handleSaveTheme = async () => {
setSaving(true)
try {
await settingsApi.save("theme", config)
toast.success("Theme settings saved")
window.location.reload()
} catch (error) {
console.error("Failed to save settings", error)
toast.error("Failed to save settings")
} finally {
setSaving(false)
}
}
const handleOpenCredentialDialog = (serviceName: string) => {
setSelectedService(serviceName)
setCredentialPayload("")
setShowPassword(false)
setIsDialogOpen(true)
}
const handleSaveCredential = async () => {
if (!selectedService || !credentialPayload) return
setSaving(true)
try {
await credentialsApi.save(selectedService, credentialPayload)
toast.success(`Credentials for ${selectedService} saved`)
setIsDialogOpen(false)
fetchCredentials() // Refresh list
} catch (error) {
console.error("Failed to save credential", error)
toast.error("Failed to save credential")
} finally {
setSaving(false)
}
}
const handleDeleteCredential = async (serviceName: string) => {
if (!confirm(`Are you sure you want to remove credentials for ${serviceName}?`)) return
try {
await credentialsApi.delete(serviceName)
toast.success(`Credentials for ${serviceName} removed`)
fetchCredentials()
} catch (error) {
console.error("Failed to delete credential", error)
toast.error("Failed to delete credential")
}
}
if (loading) {
return <div className="flex justify-center p-8"><Loader2 className="animate-spin" /></div>
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">System Settings</h1>
<p className="text-muted-foreground">Manage application appearance and integrations.</p>
</div>
<Separator />
<Tabs defaultValue="theme" className="space-y-4">
<TabsList>
<TabsTrigger value="theme">Branding & Theme</TabsTrigger>
<TabsTrigger value="integrations">Integrations & Credentials</TabsTrigger>
</TabsList>
<TabsContent value="theme" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Branding & Theme</CardTitle>
<CardDescription>Customize the look and feel of your dashboard.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="companyName">Company Name</Label>
<Input
id="companyName"
value={config.companyName}
onChange={(e) => setConfig({ ...config, companyName: e.target.value })}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="logoUrl">Logo URL</Label>
<div className="flex gap-4 items-center">
<Input
id="logoUrl"
value={config.logoUrl}
onChange={(e) => setConfig({ ...config, logoUrl: e.target.value })}
/>
{config.logoUrl && (
<div className="h-10 w-10 border rounded bg-muted flex items-center justify-center overflow-hidden">
<img src={config.logoUrl} alt="Preview" className="max-h-full max-w-full" onError={(e) => e.currentTarget.style.display = 'none'} />
</div>
)}
</div>
<p className="text-xs text-muted-foreground">Enter a public URL for your logo.</p>
</div>
<div className="grid gap-2">
<Label htmlFor="primaryColor">Primary Color</Label>
<div className="flex gap-4 items-center">
<Input
id="primaryColor"
type="color"
className="w-20 h-10 p-1 cursor-pointer"
value={config.primaryColor}
onChange={(e) => setConfig({ ...config, primaryColor: e.target.value })}
/>
<div className="flex-1 p-2 rounded text-white text-center text-sm" style={{ backgroundColor: config.primaryColor }}>
Sample Button
</div>
</div>
</div>
</CardContent>
<div className="p-6 pt-0 flex justify-end">
<Button onClick={handleSaveTheme} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
</Button>
</div>
</Card>
</TabsContent>
<TabsContent value="integrations" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>External Services</CardTitle>
<CardDescription>Manage credentials for third-party integrations securely.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{credentials.map((service) => (
<div key={service.service_name} className="flex items-center justify-between border-b pb-4 last:border-0 last:pb-0">
<div className="space-y-1">
<div className="flex items-center gap-2">
<p className="font-medium capitalize">{service.service_name}</p>
{service.is_configured ? (
<Badge variant="default" className="bg-green-600 hover:bg-green-700">
<Check className="w-3 h-3 mr-1" /> Configured
</Badge>
) : (
<Badge variant="secondary">Not Configured</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
{service.is_configured
? `Updated on ${new Date(service.updated_at).toLocaleDateString()} by ${service.updated_by || 'Unknown'}`
: 'No credentials saved for this service.'}
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => handleOpenCredentialDialog(service.service_name)}>
<Key className="w-4 h-4 mr-2" />
{service.is_configured ? "Update" : "Setup"}
</Button>
{service.is_configured && (
<Button variant="destructive" size="sm" onClick={() => handleDeleteCredential(service.service_name)}>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Configure {selectedService}</DialogTitle>
<DialogDescription>
Enter the secret credentials for {selectedService}. These will be encrypted and stored securely.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="payload">Secret Payload (JSON or Token)</Label>
<div className="relative">
<Textarea
id="payload"
value={credentialPayload}
onChange={(e) => setCredentialPayload(e.target.value)}
placeholder={selectedService === 'firebase' ? 'Paste service-account.json content here...' : 'Paste API Key or Connection String...'}
className="min-h-[100px] font-mono text-xs pr-10" // Monospace for keys
// Hack to masking if needed? No real way to mask textarea easily.
// But typically service account JSONs are visible when pasting.
/>
</div>
<p className="text-xs text-muted-foreground">
{selectedService === 'firebase' && "Paste the entire content of your service-account.json"}
{selectedService === 'stripe' && "Paste your Stripe Secret Key (sk_...)"}
{selectedService === 'appwrite' && "Paste your Appwrite API Key"}
{selectedService === 'lavinmq' && "Paste your AMQP URL"}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
<Button onClick={handleSaveCredential} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Credentials
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}