Skip to content

Commit

Permalink
feat: update deps and tests, replace route announcer, use valibot (#21)
Browse files Browse the repository at this point in the history
- updates packages, and removes unused packages
- aligns tests with template repository
- enables nuxt v4 compatibility flag
- uses new built-in route announcer
- uses recommended naming convention for runtime config
- uses valibot instead of zod for validation
  • Loading branch information
stefanprobst authored Jun 23, 2024
2 parents 4b0e5cc + 7fb3c1f commit a97da83
Show file tree
Hide file tree
Showing 38 changed files with 4,389 additions and 3,747 deletions.
1 change: 1 addition & 0 deletions app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
<NuxtLayout>
<NuxtPage />
<NuxtLoadingIndicator />
<NuxtRouteAnnouncer />
</NuxtLayout>
</template>
6 changes: 1 addition & 5 deletions components/app-footer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,7 @@ const links = computed(() => {

<div class="sm:justify-self-end sm:text-right">
Version:
{{
[env.public.NUXT_PUBLIC_GIT_TAG, env.public.NUXT_PUBLIC_GIT_BRANCH_NAME]
.filter(isNonEmptyString)
.join(" - ")
}}
{{ [env.public.gitTag, env.public.gitBranchName].filter(isNonEmptyString).join(" - ") }}
</div>
</div>
</footer>
Expand Down
12 changes: 6 additions & 6 deletions components/data-map-view.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { keyByToMap } from "@acdh-oeaw/lib";
import * as turf from "@turf/turf";
import type { MapGeoJSONFeature } from "maplibre-gl";
import { z } from "zod";
import * as v from "valibot";
import type { SearchFormData } from "@/components/search-form.vue";
import type { EntityFeature } from "@/composables/use-create-entity";
Expand All @@ -15,16 +15,16 @@ const router = useRouter();
const route = useRoute();
const t = useTranslations();
const searchFiltersSchema = z.object({
category: z.enum(categories).catch("entityName"),
search: z.string().catch(""),
const searchFiltersSchema = v.object({
category: v.fallback(v.picklist(categories), "entityName"),
search: v.fallback(v.string(), ""),
});
const searchFilters = computed(() => {
return searchFiltersSchema.parse(route.query);
return v.parse(searchFiltersSchema, route.query);
});
type SearchFilters = z.infer<typeof searchFiltersSchema>;
type SearchFilters = v.InferOutput<typeof searchFiltersSchema>;
function setSearchFilters(query: Partial<SearchFilters>) {
void router.push({ query });
Expand Down
33 changes: 23 additions & 10 deletions components/data-view.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { SortingState } from "@tanstack/vue-table";
import { z } from "zod";
import * as v from "valibot";
import type { SearchFormData } from "@/components/search-form.vue";
import {
Expand All @@ -18,17 +18,30 @@ const router = useRouter();
const route = useRoute();
const t = useTranslations();
const searchFiltersSchema = z.object({
category: z.enum(categories).catch("entityName"),
search: z.string().catch(""),
column: z.enum(columns).catch("name"),
sort: z.enum(["asc", "desc"]).catch("asc"),
page: z.coerce.number().int().positive().catch(1),
limit: z.coerce.number().int().positive().max(100).catch(20),
const searchFiltersSchema = v.object({
category: v.fallback(v.picklist(categories), "entityName"),
search: v.fallback(v.string(), ""),
column: v.fallback(v.picklist(columns), "name"),
sort: v.fallback(v.picklist(["asc", "desc"]), "asc"),
page: v.fallback(
v.pipe(v.unknown(), v.transform(Number), v.number(), v.integer(), v.minValue(1)),
1,
),
limit: v.fallback(
v.pipe(
v.unknown(),
v.transform(Number),
v.number(),
v.integer(),
v.minValue(1),
v.maxValue(100),
),
20,
),
});
const searchFilters = computed(() => {
return searchFiltersSchema.parse(route.query);
return v.parse(searchFiltersSchema, route.query);
});
const sortingState = computed(() => {
Expand All @@ -37,7 +50,7 @@ const sortingState = computed(() => {
] as SortingState;
});
type SearchFilters = z.infer<typeof searchFiltersSchema>;
type SearchFilters = v.InferOutput<typeof searchFiltersSchema>;
function setSearchFilters(query: Partial<SearchFilters>) {
void router.push({ query });
Expand Down
4 changes: 1 addition & 3 deletions components/geo-map.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ const colors = {
};
const mapStyle = computed(() => {
return theme.value === "dark"
? env.public.NUXT_PUBLIC_MAP_BASELAYER_URL_DARK
: env.public.NUXT_PUBLIC_MAP_BASELAYER_URL_LIGHT;
return theme.value === "dark" ? env.public.mapBaselayerUrlDark : env.public.mapBaselayerUrlLight;
});
const elementRef = ref<HTMLElement | null>(null);
Expand Down
2 changes: 1 addition & 1 deletion components/imprint-acdh-ch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const env = useRuntimeConfig();
const locale = useLocale();
const t = useTranslations();
const redmineId = env.public.NUXT_PUBLIC_REDMINE_ID;
const redmineId = env.public.redmineId;
const {
data: imprint,
Expand Down
47 changes: 0 additions & 47 deletions components/route-announcer.vue

This file was deleted.

2 changes: 1 addition & 1 deletion composables/use-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { paths } from "@/lib/api-client/api";

export function useApiClient() {
const env = useRuntimeConfig();
const baseUrl = env.public.NUXT_PUBLIC_API_BASE_URL;
const baseUrl = env.public.apiBaseUrl;
const client = createApiClient<paths>({ baseUrl });

return client;
Expand Down
56 changes: 28 additions & 28 deletions composables/use-get-search-results.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/vue-query";
import { z } from "zod";
import * as v from "valibot";

import { type Entity, useCreateEntity } from "@/composables/use-create-entity";
import type { components, operations } from "@/lib/api-client/api";
Expand Down Expand Up @@ -63,39 +63,39 @@ export const logicalOperators = ["and", "or"] as const;

export type LogicalOperator = (typeof logicalOperators)[number];

export const searchFilter = z
.record(
z.enum(categories),
z.object({
operator: z.enum(operators),
values: z.array(z.union([z.string(), z.number()])),
logicalOperator: z.enum(logicalOperators).default("and"),
export const searchFilter = v.pipe(
v.record(
v.picklist(categories),
v.object({
operator: v.picklist(operators),
values: v.array(v.union([v.string(), v.number()])),
logicalOperator: v.optional(v.picklist(logicalOperators), "and"),
}),
)
.refine(
(value) => {
return Object.keys(value).length > 0;
},
{ message: "At least one search filter category required" },
);

export const searchFilterString = z
.string()
.transform((value, context) => {
),
v.check((input) => {
return Object.keys(input).length > 0;
}, "At least one search filter category required"),
);

export const searchFilterString = v.pipe(
v.string(),
v.transform((input) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(value);
return JSON.parse(input);
} catch {
context.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid JSON passed as search filter",
});
return z.NEVER;
// FIXME: currently not possible, @see https://github.com/fabian-hiller/valibot/issues/182
// info.issues?.push({
// message: "Invalid JSON passed as search filter",
// });
// throw new v.ValiError()
return v.never();
}
})
.pipe(searchFilter);
}),
searchFilter,
);

export type SearchFilter = z.infer<typeof searchFilter>;
export type SearchFilter = v.InferOutput<typeof searchFilter>;

export interface GetSearchResultsParams
extends Omit<NonNullable<operations["GetQuery"]["parameters"]["query"]>, "format" | "search"> {
Expand Down
4 changes: 1 addition & 3 deletions composables/use-id-prefix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { createUrl } from "@acdh-oeaw/lib";
export function useIdPrefix() {
const env = useRuntimeConfig();

const prefix = String(
createUrl({ baseUrl: env.public.NUXT_PUBLIC_API_BASE_URL, pathname: "/api/entity/" }),
);
const prefix = String(createUrl({ baseUrl: env.public.apiBaseUrl, pathname: "/api/entity/" }));

function getUnprefixedId(id: string) {
/**
Expand Down
82 changes: 42 additions & 40 deletions config/project.config.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
import { log } from "@acdh-oeaw/lib";
import { ColorSpace, getLuminance, HSL, OKLCH, parse, sRGB, to as convert } from "colorjs.io/fn";
import { z } from "zod";
import * as v from "valibot";

import projectConfig from "../project.config.json" assert { type: "json" };

ColorSpace.register(sRGB);
ColorSpace.register(HSL);
ColorSpace.register(OKLCH);

const schema = z.object({
colors: z
.object({
brand: z.string().min(1),
geojsonPoints: z.string().min(1),
geojsonAreaCenterPoints: z.string().min(1),
entityColors: z.object({
place: z.string().min(1),
source: z.string().min(1),
person: z.string().min(1),
group: z.string().min(1),
move: z.string().min(1),
event: z.string().min(1),
activity: z.string().min(1),
acquisition: z.string().min(1),
feature: z.string().min(1),
artifact: z.string().min(1),
file: z.string().min(1),
human_remains: z.string().min(1),
stratigraphic_unit: z.string().min(1),
type: z.string().min(1),
const schema = v.object({
colors: v.pipe(
v.object({
brand: v.pipe(v.string(), v.nonEmpty()),
disabledNodeColor: v.pipe(v.string(), v.nonEmpty()),
entityColors: v.object({
acquisition: v.pipe(v.string(), v.nonEmpty()),
activity: v.pipe(v.string(), v.nonEmpty()),
artifact: v.pipe(v.string(), v.nonEmpty()),
event: v.pipe(v.string(), v.nonEmpty()),
feature: v.pipe(v.string(), v.nonEmpty()),
file: v.pipe(v.string(), v.nonEmpty()),
group: v.pipe(v.string(), v.nonEmpty()),
human_remains: v.pipe(v.string(), v.nonEmpty()),
move: v.pipe(v.string(), v.nonEmpty()),
person: v.pipe(v.string(), v.nonEmpty()),
place: v.pipe(v.string(), v.nonEmpty()),
source: v.pipe(v.string(), v.nonEmpty()),
stratigraphic_unit: v.pipe(v.string(), v.nonEmpty()),
type: v.pipe(v.string(), v.nonEmpty()),
}),
entityDefaultColor: z.string().min(1),
disabledNodeColor: z.string().min(1),
})
.transform((values) => {
entityDefaultColor: v.pipe(v.string(), v.nonEmpty()),
geojsonPoints: v.pipe(v.string(), v.nonEmpty()),
geojsonAreaCenterPoints: v.pipe(v.string(), v.nonEmpty()),
}),
v.transform((values) => {
const color = parse(values.brand);
const luminance = getLuminance(convert(color, OKLCH));
const [h, s, l] = convert(color, HSL).coords;
Expand All @@ -44,29 +44,31 @@ const schema = z.object({
brandContrast: luminance > 0.5 ? "hsl(0deg 0% 0%)" : "hsl(0deg 0% 100%)",
};
}),
fullscreen: z.boolean(),
map: z.object({
startPage: z.boolean(),
),
defaultLocale: v.picklist(["de", "en"]),
fullscreen: v.boolean(),
imprint: v.picklist(["acdh-ch", "custom", "none"]),
logos: v.object({
light: v.string(),
dark: v.string(),
withTextLight: v.string(),
withTextDark: v.string(),
}),
defaultLocale: z.enum(["de", "en"]),
logos: z.object({
light: z.string(),
dark: z.string(),
withTextLight: z.string(),
withTextDark: z.string(),
map: v.object({
startPage: v.boolean(),
}),
imprint: z.enum(["acdh-ch", "custom", "none"]),
twitter: z.string().optional(),
twitter: v.optional(v.string()),
});

const result = schema.safeParse(projectConfig);
const result = v.safeParse(schema, projectConfig);

if (!result.success) {
const message = "Invalid project configuration.";
log.error(message, result.error.flatten().fieldErrors);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
log.error(message, v.flatten<any>(result.issues).nested);
const error = new Error(message);
delete error.stack;
throw error;
}

export const project = result.data;
export const project = result.output;
Loading

0 comments on commit a97da83

Please sign in to comment.