- Backend: Email producer (LavinMQ), EmailService interface - Backend: CRUD API for email_templates and email_settings - Backend: avatar_url field in users table + UpdateMyProfile support - Backend: StorageService for pre-signed URLs - NestJS: Email consumer with Nodemailer and Handlebars - Frontend: Email Templates admin pages (list/edit) - Frontend: Updated profileApi.uploadAvatar with pre-signed URL flow - Frontend: New /post-job public page (company registration + job creation wizard) - Migrations: 027_create_email_system.sql, 028_add_avatar_url_to_users.sql
133 lines
5.3 KiB
TypeScript
133 lines
5.3 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 } from "@/lib/api"
|
|
import { toast } from "sonner"
|
|
import { Loader2, Check } from "lucide-react"
|
|
|
|
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)
|
|
|
|
const fetchSettings = async () => {
|
|
try {
|
|
const data = await settingsApi.get("theme")
|
|
if (data && Object.keys(data).length > 0) {
|
|
setConfig({ ...DEFAULT_THEME, ...data }) // Merge with defaults
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch theme settings", error)
|
|
// Accept default
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchSettings()
|
|
}, [])
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true)
|
|
try {
|
|
await settingsApi.save("theme", config)
|
|
toast.success("Theme settings saved successfully")
|
|
// Force reload to apply? Or use Context.
|
|
// Ideally Context updates. For now, reload works.
|
|
window.location.reload()
|
|
} catch (error) {
|
|
console.error("Failed to save settings", error)
|
|
toast.error("Failed to save settings")
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
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 configuration.</p>
|
|
</div>
|
|
<Separator />
|
|
|
|
<div className="grid gap-6">
|
|
<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 && (
|
|
<img src={config.logoUrl} alt="Preview" className="h-10 w-auto border rounded bg-muted p-1" onError={(e) => e.currentTarget.style.display = 'none'} />
|
|
)}
|
|
</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={handleSave} disabled={saving}>
|
|
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
Save Changes
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|