feat: compare (premium), rating-history trend, hover previews
Build & Push Docker Image / build (push) Successful in 2m47s

This commit is contained in:
opencode
2026-08-02 12:48:02 +02:00
parent 0c27982098
commit c77610786b
18 changed files with 911 additions and 13 deletions
@@ -145,6 +145,13 @@ export interface Tool {
deletedBy?: string | null;
}
export interface RatingHistoryItem {
date: string;
usefulness: number;
usability: number;
combined: number;
}
export interface ToolWithStats {
id: number;
name: string;
@@ -366,6 +373,13 @@ export const ListToolsSort = {
recently_updated: 'recently_updated',
} as const;
export type ListCompareToolsParams = {
/**
* Comma-separated tool ids
*/
ids: string;
};
export type ListTrashedToolsParams = {
search?: string;
};
+163
View File
@@ -31,11 +31,13 @@ import type {
GetTopToolsParams,
HealthStatus,
ListAuditLogsParams,
ListCompareToolsParams,
ListToolsParams,
ListTrashedToolsParams,
LocalLoginInput,
Rating,
RatingDistribution,
RatingHistoryItem,
RatingInput,
RestoreTools200,
Tool,
@@ -375,6 +377,167 @@ export const useCreateTool = <TError = ErrorType<ErrorResponse>,
return useMutation(getCreateToolMutationOptions(options));
}
export const getListCompareToolsUrl = (params: ListCompareToolsParams,) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? 'null' : value.toString())
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0 ? `/api/compare?${stringifiedParams}` : `/api/compare`
}
/**
* @summary Compare tools side by side (premium)
*/
export const listCompareTools = async (params: ListCompareToolsParams, options?: RequestInit): Promise<ToolWithStats[]> => {
return customFetch<ToolWithStats[]>(getListCompareToolsUrl(params),
{
...options,
method: 'GET'
}
);}
export const getListCompareToolsQueryKey = (params?: ListCompareToolsParams,) => {
return [
`/api/compare`, ...(params ? [params] : [])
] as const;
}
export const getListCompareToolsQueryOptions = <TData = Awaited<ReturnType<typeof listCompareTools>>, TError = ErrorType<ErrorResponse>>(params: ListCompareToolsParams, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listCompareTools>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListCompareToolsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listCompareTools>>> = ({ signal }) => listCompareTools(params, { signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listCompareTools>>, TError, TData> & { queryKey: QueryKey }
}
export type ListCompareToolsQueryResult = NonNullable<Awaited<ReturnType<typeof listCompareTools>>>
export type ListCompareToolsQueryError = ErrorType<ErrorResponse>
/**
* @summary Compare tools side by side (premium)
*/
export function useListCompareTools<TData = Awaited<ReturnType<typeof listCompareTools>>, TError = ErrorType<ErrorResponse>>(
params: ListCompareToolsParams, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listCompareTools>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListCompareToolsQueryOptions(params,options)
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
return { ...query, queryKey: queryOptions.queryKey };
}
export const getGetToolRatingHistoryUrl = (id: number,) => {
return `/api/tools/${id}/rating-history`
}
/**
* @summary Get a tool's rating history over time
*/
export const getToolRatingHistory = async (id: number, options?: RequestInit): Promise<RatingHistoryItem[]> => {
return customFetch<RatingHistoryItem[]>(getGetToolRatingHistoryUrl(id),
{
...options,
method: 'GET'
}
);}
export const getGetToolRatingHistoryQueryKey = (id: number,) => {
return [
`/api/tools/${id}/rating-history`
] as const;
}
export const getGetToolRatingHistoryQueryOptions = <TData = Awaited<ReturnType<typeof getToolRatingHistory>>, TError = ErrorType<ErrorResponse>>(id: number, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getToolRatingHistory>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetToolRatingHistoryQueryKey(id);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getToolRatingHistory>>> = ({ signal }) => getToolRatingHistory(id, { signal, ...requestOptions });
return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getToolRatingHistory>>, TError, TData> & { queryKey: QueryKey }
}
export type GetToolRatingHistoryQueryResult = NonNullable<Awaited<ReturnType<typeof getToolRatingHistory>>>
export type GetToolRatingHistoryQueryError = ErrorType<ErrorResponse>
/**
* @summary Get a tool's rating history over time
*/
export function useGetToolRatingHistory<TData = Awaited<ReturnType<typeof getToolRatingHistory>>, TError = ErrorType<ErrorResponse>>(
id: number, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getToolRatingHistory>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetToolRatingHistoryQueryOptions(id,options)
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
return { ...query, queryKey: queryOptions.queryKey };
}
export const getGetToolUrl = (id: number,) => {
+75
View File
@@ -126,6 +126,67 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"
/compare:
get:
operationId: listCompareTools
tags: [tools]
summary: Compare tools side by side (premium)
parameters:
- name: ids
in: query
required: true
schema:
type: string
description: Comma-separated tool ids
responses:
"200":
description: Tools in requested order
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ToolWithStats"
"401":
description: Authentication required
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"403":
description: Premium feature required
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/tools/{id}/rating-history:
get:
operationId: getToolRatingHistory
tags: [tools]
summary: Get a tool's rating history over time
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Rating history
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/RatingHistoryItem"
"400":
description: Invalid id
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/tools/{id}:
get:
operationId: getTool
@@ -875,6 +936,20 @@ components:
deletedBy:
type: ["string", "null"]
RatingHistoryItem:
type: object
required: [date, usefulness, usability, combined]
properties:
date:
type: string
format: date-time
usefulness:
type: number
usability:
type: number
combined:
type: number
ToolWithStats:
type: object
required: [id, name, description, category, createdAt, updatedAt, ratingCount, avgUsefulness, avgUsability, avgCombined]
+43
View File
@@ -85,6 +85,49 @@ export const CreateToolBody = zod.object({
})
/**
* @summary Compare tools side by side (premium)
*/
export const ListCompareToolsQueryParams = zod.object({
"ids": zod.coerce.string().describe('Comma-separated tool ids')
})
export const ListCompareToolsResponseItem = 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 ListCompareToolsResponse = zod.array(ListCompareToolsResponseItem)
/**
* @summary Get a tool's rating history over time
*/
export const GetToolRatingHistoryParams = zod.object({
"id": zod.coerce.number()
})
export const GetToolRatingHistoryResponseItem = zod.object({
"date": zod.coerce.date(),
"usefulness": zod.number(),
"usability": zod.number(),
"combined": zod.number()
})
export const GetToolRatingHistoryResponse = zod.array(GetToolRatingHistoryResponseItem)
/**
* @summary Get a tool by ID
*/
+2
View File
@@ -21,12 +21,14 @@ export * from './getTopToolsMetric';
export * from './getTopToolsParams';
export * from './healthStatus';
export * from './listAuditLogsParams';
export * from './listCompareToolsParams';
export * from './listToolsParams';
export * from './listToolsSort';
export * from './listTrashedToolsParams';
export * from './localLoginInput';
export * from './rating';
export * from './ratingDistribution';
export * from './ratingHistoryItem';
export * from './ratingInput';
export * from './restoreTools200';
export * from './scoreBucket';
@@ -0,0 +1,14 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export type ListCompareToolsParams = {
/**
* Comma-separated tool ids
*/
ids: string;
};
@@ -0,0 +1,14 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export interface RatingHistoryItem {
date: Date;
usefulness: number;
usability: number;
combined: number;
}