feat: user-chosen browse views with grid/list toggle and profile sync
Build & Push Docker Image / build (push) Successful in 2m25s

- view modes grid | table | rows + density cozy/compact, persisted via
  localStorage and shareable ?view=?density= URL params (URL wins)
- table view: sortable columns (name, rating, reviews), new sort options
  name_asc/name_desc/recently_updated (backend enum + handler)
- live debounced search, removable filter chips, '/' focuses search
- virtualization via @tanstack/react-virtual for table and rows views
- profile sync: users.preferences jsonb + GET/PUT /api/auth/me/preferences;
  preference precedence URL > server profile > localStorage > default
- add local rollup/lightningcss/tailwindcss oxide native binaries for macos dev
This commit is contained in:
opencode
2026-08-02 09:34:53 +02:00
parent bcc74328d6
commit 78244c3197
22 changed files with 1061 additions and 73 deletions
@@ -308,6 +308,28 @@ export interface AuthUser {
isLocal?: boolean;
}
export type UserPreferencesView = typeof UserPreferencesView[keyof typeof UserPreferencesView];
export const UserPreferencesView = {
grid: 'grid',
table: 'table',
rows: 'rows',
} as const;
export type UserPreferencesDensity = typeof UserPreferencesDensity[keyof typeof UserPreferencesDensity];
export const UserPreferencesDensity = {
cozy: 'cozy',
compact: 'compact',
} as const;
export interface UserPreferences {
view?: UserPreferencesView;
density?: UserPreferencesDensity;
}
export interface ErrorResponse {
error: string;
}
@@ -325,6 +347,9 @@ export const ListToolsSort = {
newest: 'newest',
top_rated: 'top_rated',
most_reviewed: 'most_reviewed',
name_asc: 'name_asc',
name_desc: 'name_desc',
recently_updated: 'recently_updated',
} as const;
export type ListTrashedToolsParams = {
+149
View File
@@ -47,6 +47,7 @@ import type {
TrashToolsInput,
User,
UserCreateInput,
UserPreferences,
UserRoleUpdate,
VersionInfo
} from './api.schemas';
@@ -1887,6 +1888,154 @@ export function useGetMe<TData = Awaited<ReturnType<typeof getMe>>, TError = Err
export const getGetMePreferencesUrl = () => {
return `/api/auth/me/preferences`
}
/**
* @summary Get current user's browse preferences
*/
export const getMePreferences = async ( options?: RequestInit): Promise<UserPreferences> => {
return customFetch<UserPreferences>(getGetMePreferencesUrl(),
{
...options,
method: 'GET'
}
);}
export const getGetMePreferencesQueryKey = () => {
return [
`/api/auth/me/preferences`
] as const;
}
export const getGetMePreferencesQueryOptions = <TData = Awaited<ReturnType<typeof getMePreferences>>, TError = ErrorType<ErrorResponse>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getMePreferences>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetMePreferencesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getMePreferences>>> = ({ signal }) => getMePreferences({ signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getMePreferences>>, TError, TData> & { queryKey: QueryKey }
}
export type GetMePreferencesQueryResult = NonNullable<Awaited<ReturnType<typeof getMePreferences>>>
export type GetMePreferencesQueryError = ErrorType<ErrorResponse>
/**
* @summary Get current user's browse preferences
*/
export function useGetMePreferences<TData = Awaited<ReturnType<typeof getMePreferences>>, TError = ErrorType<ErrorResponse>>(
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getMePreferences>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMePreferencesQueryOptions(options)
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
return { ...query, queryKey: queryOptions.queryKey };
}
export const getUpdateMePreferencesUrl = () => {
return `/api/auth/me/preferences`
}
/**
* @summary Update current user's browse preferences
*/
export const updateMePreferences = async (userPreferences: UserPreferences, options?: RequestInit): Promise<UserPreferences> => {
return customFetch<UserPreferences>(getUpdateMePreferencesUrl(),
{
...options,
method: 'PUT',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(
userPreferences,)
}
);}
export const getUpdateMePreferencesMutationOptions = <TError = ErrorType<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMePreferences>>, TError,{data: BodyType<UserPreferences>}, TContext>, request?: SecondParameter<typeof customFetch>}
): UseMutationOptions<Awaited<ReturnType<typeof updateMePreferences>>, TError,{data: BodyType<UserPreferences>}, TContext> => {
const mutationKey = ['updateMePreferences'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updateMePreferences>>, {data: BodyType<UserPreferences>}> = (props) => {
const {data} = props ?? {};
return updateMePreferences(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UpdateMePreferencesMutationResult = NonNullable<Awaited<ReturnType<typeof updateMePreferences>>>
export type UpdateMePreferencesMutationBody = BodyType<UserPreferences>
export type UpdateMePreferencesMutationError = ErrorType<ErrorResponse>
/**
* @summary Update current user's browse preferences
*/
export const useUpdateMePreferences = <TError = ErrorType<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMePreferences>>, TError,{data: BodyType<UserPreferences>}, TContext>, request?: SecondParameter<typeof customFetch>}
): UseMutationResult<
Awaited<ReturnType<typeof updateMePreferences>>,
TError,
{data: BodyType<UserPreferences>},
TContext
> => {
return useMutation(getUpdateMePreferencesMutationOptions(options));
}
export const getListUsersUrl = () => {