gohorsejobs/frontend/src/app/dashboard/settings/page.tsx
Tiago Yamamoto 841b1d780c feat: Email System, Avatar Upload, Email Templates UI, and Public Job Posting
- 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
2025-12-26 12:21:34 -03:00

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>
)
}