From 81a463a741f48e2c2a22ddc8361c6e0abb8b4140 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Fri, 30 Jan 2026 12:14:23 +0800 Subject: [PATCH] filter stuff --- .../src/composables/useRouteQuerySchema.ts | 41 +++++++++++++++++++ next-ui/src/functions/filter.ts | 22 ++++++++++ next-ui/src/types/filter.test.ts | 32 +++++++++++++++ next-ui/src/types/filter.ts | 24 +++++++++++ 4 files changed, 119 insertions(+) create mode 100644 next-ui/src/composables/useRouteQuerySchema.ts create mode 100644 next-ui/src/functions/filter.ts create mode 100644 next-ui/src/types/filter.test.ts create mode 100644 next-ui/src/types/filter.ts diff --git a/next-ui/src/composables/useRouteQuerySchema.ts b/next-ui/src/composables/useRouteQuerySchema.ts new file mode 100644 index 00000000..3884663a --- /dev/null +++ b/next-ui/src/composables/useRouteQuerySchema.ts @@ -0,0 +1,41 @@ +import * as v from 'valibot' +import { useRouteQuery } from '@vueuse/router' +import { syncRef } from '@vueuse/core' + +/** + * Reactive `route.query` with schema validation. + * If value is not valid, the `schema` default values are used. + * + * @param queryName the query parameter name + * @param schema valibot schema to validate against + */ +export function useRouteQuerySchema(queryName: string, schema: T) { + const queryString = useRouteQuery(queryName, '{}') + + const defaults = v.getDefaults(schema) + + function getInitialValue(stringValue: string) { + try { + return v.parse(schema, JSON.parse(stringValue)) + } catch { + return defaults + } + } + + const data = ref(getInitialValue(String(queryString.value))) + + syncRef(data, queryString, { + direction: 'ltr', + deep: true, + transform: { + ltr: (left) => { + if (JSON.stringify(left) !== JSON.stringify(defaults)) return JSON.stringify(left) + return undefined + }, + }, + }) + + return { + data: data as v.InferOutput, + } +} diff --git a/next-ui/src/functions/filter.ts b/next-ui/src/functions/filter.ts new file mode 100644 index 00000000..70906565 --- /dev/null +++ b/next-ui/src/functions/filter.ts @@ -0,0 +1,22 @@ +import type { SchemaFilterSeriesStatus } from '@/types/filter' +import type { InferOutput } from 'valibot' + +export function schemaFilterSeriesStatusToConditions( + filter: MaybeRefOrGetter>, +) { + const list = toValue(filter).v.map((it) => ({ + seriesStatus: { + operator: it.i === 'e' ? 'isNot' : 'is', + value: it.v, + }, + })) + + if (toValue(filter).m === 'allOf') + return { + allOf: list, + } + else + return { + anyOf: list, + } +} diff --git a/next-ui/src/types/filter.test.ts b/next-ui/src/types/filter.test.ts new file mode 100644 index 00000000..031df192 --- /dev/null +++ b/next-ui/src/types/filter.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from 'vitest' +import * as v from 'valibot' +import { SchemaAnyAll } from '@/types/filter' + +describe('schema any all', () => { + test('anyOf', () => { + const input = { m: 'anyOf' } + const result = v.parse(SchemaAnyAll, input) + + expect(result).toStrictEqual(input) + }) + + test('allOf', () => { + const input = { m: 'allOf' } + const result = v.parse(SchemaAnyAll, input) + + expect(result).toStrictEqual(input) + }) + + test('other value throws error', () => { + const input = { m: 'other' } + + expect(() => v.parse(SchemaAnyAll, input)).toThrowError() + }) + + test('defaults to anyOf', () => { + const input = { m: 'anyOf' } + const defaults = v.getDefaults(SchemaAnyAll) + + expect(defaults).toStrictEqual(input) + }) +}) diff --git a/next-ui/src/types/filter.ts b/next-ui/src/types/filter.ts new file mode 100644 index 00000000..9ac305e4 --- /dev/null +++ b/next-ui/src/types/filter.ts @@ -0,0 +1,24 @@ +import * as v from 'valibot' + +export type AnyAll = 'anyOf' | 'allOf' + +export const SchemaAnyAll = v.object({ + m: v.optional(v.picklist(['anyOf', 'allOf']), 'anyOf'), +}) + +const SchemaIncludeExclude = v.object({ i: v.optional(v.picklist(['i', 'e'])) }) + +const SchemaSeriesStatus = v.object({ + ...SchemaIncludeExclude.entries, + v: v.optional(v.picklist(['ENDED', 'ONGOING', 'ABANDONED', 'HIATUS'])), +}) + +export const SchemaFilterSeriesStatus = v.object({ + ...SchemaAnyAll.entries, + v: v.fallback(v.optional(v.array(SchemaSeriesStatus), []), []), +}) + +export const FilterSeriesGenre = v.object({ + ...SchemaAnyAll.entries, + value: v.array(v.string()), +})