feat: goals — "watch N movies in YEAR" with progress bar

Domain: Goal entity, UserSettings (federation toggle), RemoteGoalEntry.
Ports: GoalRepository, UserSettingsRepository, RemoteGoalRepository.
Adapters: sqlite + postgres repos, migrations, AP content query extensions.
Application: CRUD use cases (create/update/delete/get/list), settings use cases.
API: 7 endpoints (/goals CRUD, /users/{id}/goals, /settings) with utoipa docs.
Federation: GoalObject (Note + goal discriminator), outbound broadcast with
per-user toggle, inbound GoalObjectHandler in CompositeObjectHandler.
SPA: API client + hooks, GoalCard (shadcn Card+Progress+DropdownMenu),
GoalSheet (Drawer), profile integration (editable own, read-only others),
federation toggle in settings (Switch).
Classic HTML: glassmorphic goal card on profile, Frutiger Aero styling.
Progress computed from existing reviews — backwards compatible.
This commit is contained in:
2026-06-08 22:37:52 +02:00
parent 213f9a2433
commit fff5f4af2f
67 changed files with 2747 additions and 28 deletions

View File

@@ -10,11 +10,14 @@ import {
RefreshCw,
ShieldBan,
Sparkles,
Target,
User,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { useAuth, useIsAdmin } from "@/components/auth-provider"
import { reindexSearch } from "@/lib/api/users"
import { useSettings, useUpdateSettings } from "@/hooks/use-goals"
export const Route = createFileRoute("/_app/settings/")({
component: SettingsPage,
@@ -94,6 +97,8 @@ function SettingsPage() {
<SettingsGroup label={t("settings.integrations")} items={integrations} />
<SettingsGroup label={t("settings.socialGroup")} items={social} />
<PrivacySection />
{isAdmin && <AdminActions />}
<button
@@ -109,6 +114,40 @@ function SettingsPage() {
)
}
function PrivacySection() {
const { t } = useTranslation()
const { data: settings } = useSettings()
const updateMutation = useUpdateSettings()
return (
<div>
<p className="mb-1.5 px-1 text-xs font-medium text-muted-foreground">
{t("settings.privacy")}
</p>
<div className="divide-y divide-border rounded-xl bg-card">
<div className="flex items-center gap-3 p-3">
<span className="text-muted-foreground">
<Target className="size-4" />
</span>
<div className="flex-1">
<p className="text-sm font-medium">{t("settings.federateGoals")}</p>
<p className="text-xs text-muted-foreground">
{t("settings.federateGoalsDesc")}
</p>
</div>
<Switch
checked={settings?.federate_goals ?? false}
onCheckedChange={(checked) =>
updateMutation.mutate({ federate_goals: checked })
}
disabled={updateMutation.isPending}
/>
</div>
</div>
</div>
)
}
function AdminActions() {
const { t } = useTranslation()
const reindex = useMutation({