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:
54
spa/src/lib/api/goals.ts
Normal file
54
spa/src/lib/api/goals.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { z } from "zod"
|
||||
import { get, post, put, del } from "./client"
|
||||
import { goalDtoSchema } from "./users"
|
||||
|
||||
export const goalsResponseSchema = z.object({
|
||||
goals: z.array(goalDtoSchema),
|
||||
})
|
||||
export type GoalsResponse = z.infer<typeof goalsResponseSchema>
|
||||
|
||||
export type CreateGoalRequest = {
|
||||
year: number
|
||||
target_count: number
|
||||
}
|
||||
|
||||
export type UpdateGoalRequest = {
|
||||
target_count: number
|
||||
}
|
||||
|
||||
export const userSettingsDtoSchema = z.object({
|
||||
federate_goals: z.boolean(),
|
||||
})
|
||||
export type UserSettingsDto = z.infer<typeof userSettingsDtoSchema>
|
||||
|
||||
export type UpdateUserSettingsRequest = {
|
||||
federate_goals: boolean
|
||||
}
|
||||
|
||||
export function getGoals() {
|
||||
return get<GoalsResponse>("/goals")
|
||||
}
|
||||
|
||||
export function getUserGoals(userId: string) {
|
||||
return get<GoalsResponse>(`/users/${userId}/goals`)
|
||||
}
|
||||
|
||||
export function createGoal(data: CreateGoalRequest) {
|
||||
return post<z.infer<typeof goalDtoSchema>>("/goals", data)
|
||||
}
|
||||
|
||||
export function updateGoal(year: number, data: UpdateGoalRequest) {
|
||||
return put<z.infer<typeof goalDtoSchema>>(`/goals/${year}`, data)
|
||||
}
|
||||
|
||||
export function deleteGoal(year: number) {
|
||||
return del(`/goals/${year}`)
|
||||
}
|
||||
|
||||
export function getSettings() {
|
||||
return get<UserSettingsDto>("/settings")
|
||||
}
|
||||
|
||||
export function updateSettings(data: UpdateUserSettingsRequest) {
|
||||
return put("/settings", data)
|
||||
}
|
||||
@@ -63,6 +63,16 @@ export type MonthActivityDto = z.infer<typeof monthActivityDtoSchema>
|
||||
|
||||
const userDiaryResponseSchema = paginatedSchema(diaryEntryDtoSchema)
|
||||
|
||||
export const goalDtoSchema = z.object({
|
||||
year: z.number(),
|
||||
target_count: z.number(),
|
||||
current_count: z.number(),
|
||||
percentage: z.number(),
|
||||
is_complete: z.boolean(),
|
||||
goal_type: z.string(),
|
||||
})
|
||||
export type GoalDto = z.infer<typeof goalDtoSchema>
|
||||
|
||||
export const userProfileResponseSchema = z.object({
|
||||
user_id: z.string().uuid(),
|
||||
username: z.string(),
|
||||
@@ -74,6 +84,7 @@ export const userProfileResponseSchema = z.object({
|
||||
entries: userDiaryResponseSchema.optional(),
|
||||
history: z.array(monthActivityDtoSchema).optional(),
|
||||
trends: userTrendsDtoSchema.optional(),
|
||||
goals: z.array(goalDtoSchema).optional(),
|
||||
})
|
||||
export type UserProfileResponse = z.infer<typeof userProfileResponseSchema>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user