feat: watchlist (premium), mobile bottom nav
Build & Push Docker Image / build (push) Successful in 2m35s

This commit is contained in:
opencode
2026-08-02 13:00:10 +02:00
parent c77610786b
commit d033b20dfb
14 changed files with 383 additions and 12 deletions
@@ -335,6 +335,7 @@ export const UserPreferencesDensity = {
export interface UserPreferences {
view?: UserPreferencesView;
density?: UserPreferencesDensity;
watchlist?: number[];
}
export interface ErrorResponse {
+77
View File
@@ -2199,6 +2199,83 @@ export const useUpdateMePreferences = <TError = ErrorType<ErrorResponse>,
return useMutation(getUpdateMePreferencesMutationOptions(options));
}
export const getGetMeWatchlistUrl = () => {
return `/api/auth/me/watchlist`
}
/**
* @summary Get current user's watchlist tools (premium)
*/
export const getMeWatchlist = async ( options?: RequestInit): Promise<ToolWithStats[]> => {
return customFetch<ToolWithStats[]>(getGetMeWatchlistUrl(),
{
...options,
method: 'GET'
}
);}
export const getGetMeWatchlistQueryKey = () => {
return [
`/api/auth/me/watchlist`
] as const;
}
export const getGetMeWatchlistQueryOptions = <TData = Awaited<ReturnType<typeof getMeWatchlist>>, TError = ErrorType<ErrorResponse>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getMeWatchlist>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetMeWatchlistQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getMeWatchlist>>> = ({ signal }) => getMeWatchlist({ signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getMeWatchlist>>, TError, TData> & { queryKey: QueryKey }
}
export type GetMeWatchlistQueryResult = NonNullable<Awaited<ReturnType<typeof getMeWatchlist>>>
export type GetMeWatchlistQueryError = ErrorType<ErrorResponse>
/**
* @summary Get current user's watchlist tools (premium)
*/
export function useGetMeWatchlist<TData = Awaited<ReturnType<typeof getMeWatchlist>>, TError = ErrorType<ErrorResponse>>(
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getMeWatchlist>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMeWatchlistQueryOptions(options)
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
return { ...query, queryKey: queryOptions.queryKey };
}
export const getListUsersUrl = () => {
+31
View File
@@ -656,6 +656,33 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"
/auth/me/watchlist:
get:
operationId: getMeWatchlist
tags: [auth]
summary: Get current user's watchlist tools (premium)
responses:
"200":
description: Watchlist tools in saved order
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ToolWithStats"
"401":
description: Not authenticated
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"403":
description: Premium feature required
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/users:
get:
operationId: listUsers
@@ -1193,6 +1220,10 @@ components:
density:
type: string
enum: [cozy, compact]
watchlist:
type: array
items:
type: integer
ErrorResponse:
type: object
+29 -3
View File
@@ -485,7 +485,8 @@ export const GetMeResponse = zod.object({
*/
export const GetMePreferencesResponse = zod.object({
"view": zod.enum(['grid', 'table', 'rows']).optional(),
"density": zod.enum(['cozy', 'compact']).optional()
"density": zod.enum(['cozy', 'compact']).optional(),
"watchlist": zod.array(zod.number()).optional()
})
@@ -494,15 +495,40 @@ export const GetMePreferencesResponse = zod.object({
*/
export const UpdateMePreferencesBody = zod.object({
"view": zod.enum(['grid', 'table', 'rows']).optional(),
"density": zod.enum(['cozy', 'compact']).optional()
"density": zod.enum(['cozy', 'compact']).optional(),
"watchlist": zod.array(zod.number()).optional()
})
export const UpdateMePreferencesResponse = zod.object({
"view": zod.enum(['grid', 'table', 'rows']).optional(),
"density": zod.enum(['cozy', 'compact']).optional()
"density": zod.enum(['cozy', 'compact']).optional(),
"watchlist": zod.array(zod.number()).optional()
})
/**
* @summary Get current user's watchlist tools (premium)
*/
export const GetMeWatchlistResponseItem = zod.object({
"id": zod.number(),
"name": zod.string(),
"description": zod.string(),
"category": zod.string(),
"websiteUrl": zod.string().nullish(),
"iconUrl": zod.string().nullish(),
"createdBy": zod.string().nullish(),
"features": zod.array(zod.string()).optional(),
"tags": zod.array(zod.string()).optional(),
"createdAt": zod.coerce.date(),
"updatedAt": zod.coerce.date(),
"ratingCount": zod.number(),
"avgUsefulness": zod.number().nullable(),
"avgUsability": zod.number().nullable(),
"avgCombined": zod.number().nullable()
})
export const GetMeWatchlistResponse = zod.array(GetMeWatchlistResponseItem)
/**
* @summary List all local users (admin only)
*/
@@ -11,4 +11,5 @@ import type { UserPreferencesView } from './userPreferencesView';
export interface UserPreferences {
view?: UserPreferencesView;
density?: UserPreferencesDensity;
watchlist?: number[];
}
+1
View File
@@ -5,6 +5,7 @@ import { z } from "zod/v4";
export type UserPreferences = {
view?: "grid" | "table" | "rows";
density?: "cozy" | "compact";
watchlist?: number[];
};
export const usersTable = pgTable("users", {