Compare commits
No commits in common. "e3f7341a0e575a9b0cb922c9b697c21f6f6875c6" and "cfe5fc2d9f012aafc8aa470a5b2f8d1366d29dcc" have entirely different histories.
e3f7341a0e
...
cfe5fc2d9f
16 changed files with 992 additions and 852 deletions
|
@ -1,5 +1,6 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
|
/build
|
||||||
/.svelte-kit
|
/.svelte-kit
|
||||||
/package
|
/package
|
||||||
.env
|
.env
|
||||||
|
|
29
Dockerfile
29
Dockerfile
|
@ -1,17 +1,36 @@
|
||||||
|
# stage build
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# copy everything to the container
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# clean install all dependencies
|
||||||
|
RUN corepack enable && pnpm i
|
||||||
|
|
||||||
|
# remove potential security issues
|
||||||
|
RUN pnpm audit fix
|
||||||
|
|
||||||
|
# build SvelteKit appmakeAuthjsRequest
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
|
||||||
|
# stage run
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# copy dependency list
|
# copy dependency list
|
||||||
COPY package.json pnpm-lock.yaml run/entrypoint.sh ./
|
COPY --from=0 /app/package*.json /app/pnpm-lock.yaml ./
|
||||||
COPY patches ./patches
|
COPY --from=0 /app/patches ./patches
|
||||||
COPY prisma ./prisma
|
COPY --from=0 /app/prisma ./prisma
|
||||||
|
|
||||||
# Setup pnpm, install Prisma CLI (for generating client) and install dependencies
|
# Setup pnpm, install Prisma CLI (for generating client) and install dependencies
|
||||||
RUN corepack enable && pnpm i --prod && pnpm audit fix && npx prisma generate
|
RUN corepack enable && pnpm i --prod && pnpm audit fix && npx prisma generate
|
||||||
|
|
||||||
# copy built SvelteKit app to /app
|
# copy built SvelteKit app to /app
|
||||||
COPY build ./
|
COPY --from=0 /app/build ./
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["./entrypoint.sh"]
|
CMD ["node", "./index.js"]
|
||||||
|
|
42
package.json
42
package.json
|
@ -18,56 +18,56 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.30.0",
|
"@auth/core": "^0.30.0",
|
||||||
"@floating-ui/core": "^1.6.1",
|
"@floating-ui/core": "^1.6.0",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@prisma/client": "^5.13.0",
|
"@prisma/client": "^5.12.1",
|
||||||
"carta-md": "4.0.2",
|
"carta-md": "4.0.2",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
"isomorphic-dompurify": "^2.9.0",
|
"isomorphic-dompurify": "^2.7.0",
|
||||||
"prisma": "^5.13.0",
|
"prisma": "^5.12.1",
|
||||||
"qs": "^6.12.1",
|
"qs": "^6.12.1",
|
||||||
"set-cookie-parser": "^2.6.0",
|
"set-cookie-parser": "^2.6.0",
|
||||||
"svelte-floating-ui": "^1.5.8",
|
"svelte-floating-ui": "^1.5.8",
|
||||||
"zod": "^3.23.6",
|
"zod": "^3.22.4",
|
||||||
"zod-form-data": "^2.0.2"
|
"zod-form-data": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@playwright/test": "^1.43.1",
|
"@playwright/test": "^1.43.1",
|
||||||
"@stylistic/eslint-plugin": "^1.8.0",
|
"@stylistic/eslint-plugin": "^1.7.2",
|
||||||
"@sveltejs/adapter-node": "^5.0.1",
|
"@sveltejs/adapter-node": "^5.0.1",
|
||||||
"@sveltejs/kit": "^2.5.7",
|
"@sveltejs/kit": "^2.5.6",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.12",
|
||||||
"@trpc/client": "^10.45.2",
|
"@trpc/client": "^10.45.2",
|
||||||
"@trpc/server": "^10.45.2",
|
"@trpc/server": "^10.45.2",
|
||||||
"@types/diff": "^5.2.0",
|
"@types/diff": "^5.0.9",
|
||||||
"@types/node": "^20.12.8",
|
"@types/node": "^20.12.7",
|
||||||
"@types/qs": "^6.9.15",
|
"@types/qs": "^6.9.15",
|
||||||
"@types/set-cookie-parser": "^2.4.7",
|
"@types/set-cookie-parser": "^2.4.7",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"daisyui": "^4.10.5",
|
"daisyui": "^4.10.2",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-no-relative-import-paths": "^1.5.4",
|
"eslint-plugin-no-relative-import-paths": "^1.5.4",
|
||||||
"eslint-plugin-svelte": "^2.38.0",
|
"eslint-plugin-svelte": "^2.37.0",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
"eslint-plugin-unused-imports": "^3.1.0",
|
||||||
"globals": "^15.1.0",
|
"globals": "^15.0.0",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"postcss-nesting": "^12.1.2",
|
"postcss-nesting": "^12.1.1",
|
||||||
"svelte": "^4.2.15",
|
"svelte": "^4.2.15",
|
||||||
"svelte-check": "^3.7.1",
|
"svelte-check": "^3.6.9",
|
||||||
"sveltekit-superforms": "^2.13.0",
|
"sveltekit-superforms": "^2.12.5",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"trpc-sveltekit": "^3.6.1",
|
"trpc-sveltekit": "^3.6.1",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"tsx": "^4.9.1",
|
"tsx": "^4.7.2",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"typescript-eslint": "^7.8.0",
|
"typescript-eslint": "^7.7.0",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.9",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.5.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|
1030
pnpm-lock.yaml
1030
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,5 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
# Migrate database before starting server
|
|
||||||
npx prisma migrate deploy
|
|
||||||
node ./index.js
|
|
|
@ -10,7 +10,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a class="card2 card-animation" href="/entry/{entry.id}">
|
<a class="card2 card-animation" href="/entry/{entry.id}">
|
||||||
<div class="row items-center flex-wrap gap-2 text-sm">
|
<div class="row items-center gap-2 text-sm">
|
||||||
<span>{formatDate(entry.current_version.date)}</span>
|
<span>{formatDate(entry.current_version.date)}</span>
|
||||||
{#if entry.current_version.category}
|
{#if entry.current_version.category}
|
||||||
<CategoryField category={entry.current_version.category} nolink />
|
<CategoryField category={entry.current_version.category} nolink />
|
||||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
||||||
PaginationRequest,
|
PaginationRequest,
|
||||||
SortRequest,
|
SortRequest,
|
||||||
} from "$lib/shared/model";
|
} from "$lib/shared/model";
|
||||||
import { DateRange, utcDateToYMD } from "$lib/shared/util";
|
import { DateRange, dateToYMD } from "$lib/shared/util";
|
||||||
import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error";
|
import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error";
|
||||||
|
|
||||||
import { prisma } from "$lib/server/prisma";
|
import { prisma } from "$lib/server/prisma";
|
||||||
|
@ -365,7 +365,7 @@ left join stations s on s.id = r.station_id`,
|
||||||
current_version: {
|
current_version: {
|
||||||
id: item.version_id,
|
id: item.version_id,
|
||||||
text: item.text,
|
text: item.text,
|
||||||
date: utcDateToYMD(item.date),
|
date: dateToYMD(item.date),
|
||||||
category: item.category_id
|
category: item.category_id
|
||||||
? {
|
? {
|
||||||
id: item.category_id,
|
id: item.category_id,
|
||||||
|
|
|
@ -19,7 +19,7 @@ import type {
|
||||||
EntryExecution,
|
EntryExecution,
|
||||||
UserTagNameNonnull,
|
UserTagNameNonnull,
|
||||||
} from "$lib/shared/model";
|
} from "$lib/shared/model";
|
||||||
import { utcDateToYMD } from "$lib/shared/util";
|
import { dateToYMD } from "$lib/shared/util";
|
||||||
import { ErrorNotFound } from "$lib/shared/util/error";
|
import { ErrorNotFound } from "$lib/shared/util/error";
|
||||||
|
|
||||||
type DbRoomLn = DbRoom & { station: DbStation };
|
type DbRoomLn = DbRoom & { station: DbStation };
|
||||||
|
@ -73,7 +73,7 @@ export function mapVersion(version: DbEntryVersionLn): EntryVersion {
|
||||||
return {
|
return {
|
||||||
id: version.id,
|
id: version.id,
|
||||||
text: version.text,
|
text: version.text,
|
||||||
date: utcDateToYMD(version.date),
|
date: dateToYMD(version.date),
|
||||||
category: version.category,
|
category: version.category,
|
||||||
priority: version.priority,
|
priority: version.priority,
|
||||||
author: version.author,
|
author: version.author,
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
import { expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import {
|
|
||||||
DateRange, dateFromYMD, dateToYMD, formatDate, humanDate, utcDateToYMD,
|
|
||||||
} from "./date";
|
|
||||||
|
|
||||||
const MINUTE = 60000;
|
|
||||||
const HOUR = 3_600_000;
|
|
||||||
const DAY = 24 * HOUR;
|
|
||||||
|
|
||||||
it("formatDate", () => {
|
|
||||||
const date = new Date(2024, 0, 2);
|
|
||||||
expect(formatDate(date)).toBe("02.01.2024");
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
{ s: "2024-01-02", exp: new Date(2024, 0, 2) },
|
|
||||||
{ s: "24-01-02", exp: NaN },
|
|
||||||
])("dateFromYMD", ({ s, exp }) => {
|
|
||||||
expect(dateFromYMD(s)).toStrictEqual(exp);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("utcDateToYMD", () => {
|
|
||||||
const utcDate = new Date(Date.UTC(2024, 0, 2));
|
|
||||||
expect(utcDateToYMD(utcDate)).toBe("2024-01-02");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("dateToYMD", () => {
|
|
||||||
const date = new Date(2024, 0, 2);
|
|
||||||
expect(dateToYMD(date)).toBe("2024-01-02");
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
{ s: 0, txt: "jetzt gerade" },
|
|
||||||
{ s: -30 * MINUTE, txt: "vor 30 Minuten" },
|
|
||||||
{ s: -11 * HOUR, txt: "vor 11 Stunden" },
|
|
||||||
{ s: -DAY, txt: "gestern" },
|
|
||||||
{ s: -2.5 * DAY, txt: "vor 2 Tagen" },
|
|
||||||
{ s: -2.6 * DAY, txt: "vor 3 Tagen" },
|
|
||||||
{ s: -3.5 * DAY, txt: "vor 3 Tagen" },
|
|
||||||
{ s: -4 * DAY, txt: "am 28.12.2023, 12:00" },
|
|
||||||
|
|
||||||
{ s: 28 * MINUTE, txt: "in 28 Minuten" },
|
|
||||||
{ s: 8 * HOUR, txt: "in 8 Stunden" },
|
|
||||||
{ s: DAY, txt: "morgen" },
|
|
||||||
{ s: 2.6 * DAY, txt: "in 3 Tagen" },
|
|
||||||
{ s: 4 * DAY, txt: "am 05.01.2024, 12:00" },
|
|
||||||
])("humanDate", ({ s, txt }) => {
|
|
||||||
const mockDate = new Date(2024, 0, 1, 12, 0);
|
|
||||||
vi.setSystemTime(mockDate);
|
|
||||||
|
|
||||||
const dt = new Date(Number(mockDate) + s);
|
|
||||||
const res = humanDate(dt, true);
|
|
||||||
expect(res).toBe(txt);
|
|
||||||
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
{ s: "", exp: null },
|
|
||||||
{ s: "..", exp: null },
|
|
||||||
{ s: "foo..bar", exp: null },
|
|
||||||
{ s: "2024-04-15", exp: new DateRange(dateFromYMD("2024-04-15"), dateFromYMD("2024-04-15")) },
|
|
||||||
{ s: "2024-04-13..2024-04-20", exp: new DateRange(dateFromYMD("2024-04-13"), dateFromYMD("2024-04-20")) },
|
|
||||||
{ s: "2024-04-13..", exp: new DateRange(dateFromYMD("2024-04-13"), null) },
|
|
||||||
{ s: "..2024-04-20", exp: new DateRange(null, dateFromYMD("2024-04-20")) },
|
|
||||||
])("parse daterange $s", ({ s, exp }) => {
|
|
||||||
const res = DateRange.parse(s);
|
|
||||||
expect(res).toStrictEqual(exp);
|
|
||||||
|
|
||||||
if (res && s !== "2024-04-15") {
|
|
||||||
expect(res.toString()).toBe(s);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,172 +0,0 @@
|
||||||
const LOCALE = "de-DE";
|
|
||||||
const MS_PER_DAY = 86400000;
|
|
||||||
|
|
||||||
function coerceDate(date: Date | string): Date {
|
|
||||||
if (!(date instanceof Date)) {
|
|
||||||
const d1 = dateFromYMD(date);
|
|
||||||
if (d1) return d1;
|
|
||||||
return new Date(date);
|
|
||||||
}
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDate(date: Date | string, time = false): string {
|
|
||||||
const dt = coerceDate(date);
|
|
||||||
if (time) {
|
|
||||||
return dt.toLocaleString(LOCALE, {
|
|
||||||
day: "2-digit",
|
|
||||||
month: "2-digit",
|
|
||||||
year: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return dt.toLocaleDateString(LOCALE, {
|
|
||||||
day: "2-digit",
|
|
||||||
month: "2-digit",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dateFromYMD(s: string): Date {
|
|
||||||
const re = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
||||||
const match = s.match(re);
|
|
||||||
if (match) {
|
|
||||||
return new Date(
|
|
||||||
parseInt(match[1]),
|
|
||||||
parseInt(match[2]) - 1,
|
|
||||||
parseInt(match[3]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// @ts-expect-error emulate behavior of date constructor
|
|
||||||
return NaN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convert the given date to a string (using the internal UTC format) */
|
|
||||||
export function utcDateToYMD(date: Date): string {
|
|
||||||
return date.toISOString().slice(0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convert the given date to a string (using the local timezone) */
|
|
||||||
export function dateToYMD(date: Date): string {
|
|
||||||
return date.getFullYear().toString().padStart(4, "0") + "-"
|
|
||||||
+ (date.getMonth() + 1).toString().padStart(2, "0") + "-"
|
|
||||||
+ date.getDate().toString().padStart(2, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
function dateDiffInDays(a: Date, b: Date): number {
|
|
||||||
const ts1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
|
|
||||||
const ts2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
|
|
||||||
|
|
||||||
return Math.round((ts2 - ts1) / MS_PER_DAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function humanDate(date: Date | string, time = false): string {
|
|
||||||
const now = new Date();
|
|
||||||
const dt = coerceDate(date);
|
|
||||||
const threshold = 302400000; // 3.5 * 24 * 3_600_000
|
|
||||||
const diff = Number(dt) - Number(now); // pos: Future, neg: Past
|
|
||||||
if (Math.abs(diff) > threshold) return `am ${formatDate(date, time)}`;
|
|
||||||
|
|
||||||
const intl = new Intl.RelativeTimeFormat(LOCALE);
|
|
||||||
|
|
||||||
const diffDays = dateDiffInDays(now, dt);
|
|
||||||
if (diffDays !== 0) {
|
|
||||||
if (diffDays === 1) return "morgen";
|
|
||||||
if (diffDays === -1) return "gestern";
|
|
||||||
return intl.format(diffDays, "day");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (time) {
|
|
||||||
const diffHours = Math.round(diff / 3_600_000);
|
|
||||||
if (diffHours !== 0) return intl.format(diffHours, "hour");
|
|
||||||
|
|
||||||
const diffMinutes = Math.round(diff / 60_000);
|
|
||||||
if (diffMinutes !== 0) return intl.format(diffMinutes, "minute");
|
|
||||||
}
|
|
||||||
|
|
||||||
return time ? "jetzt gerade" : "heute";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DateRange {
|
|
||||||
start: Date | null;
|
|
||||||
|
|
||||||
end: Date | null;
|
|
||||||
|
|
||||||
constructor(start: Date | null, end: Date | null) {
|
|
||||||
if (start === null && end == null) throw Error("this is not a range");
|
|
||||||
this.start = start;
|
|
||||||
this.end = end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Construct a new DateRange with the given length in days */
|
|
||||||
static withLength(start: Date, nDays: number): DateRange {
|
|
||||||
const end = new Date(start);
|
|
||||||
end.setDate(start.getDate() + nDays);
|
|
||||||
return new DateRange(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a date range of the current calendar week */
|
|
||||||
static thisWeek(): DateRange {
|
|
||||||
const dayStart = new Date();
|
|
||||||
// Correct for timezone
|
|
||||||
dayStart.setMinutes(dayStart.getMinutes() - dayStart.getTimezoneOffset());
|
|
||||||
|
|
||||||
const todayWd = dayStart.getDay();
|
|
||||||
// Day starts at Sunday (0)
|
|
||||||
const daysMinus = todayWd === 0 ? 6 : todayWd - 1;
|
|
||||||
dayStart.setDate(dayStart.getDate() - daysMinus);
|
|
||||||
return DateRange.withLength(dayStart, 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parse a date range from a string
|
|
||||||
*
|
|
||||||
* Accepted formats:
|
|
||||||
* - Single date `2024-04-15` => `2024-04-15..2024-04-15`
|
|
||||||
* - Range with 2 ends: `2024-04-13..2024-04-20`
|
|
||||||
* - Range with 1 end: `2024-04-13..`; `..2024-04-20`
|
|
||||||
*/
|
|
||||||
static parse(s: string): DateRange | null {
|
|
||||||
const parts = s.split("..", 2);
|
|
||||||
const parsed = parts.map((p) => {
|
|
||||||
if (p.length === 0) return null;
|
|
||||||
return dateFromYMD(p);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parsed.length === 0
|
|
||||||
// @ts-expect-error Parsed dates can be NaN
|
|
||||||
|| parsed.findIndex(isNaN) !== -1
|
|
||||||
|| parsed.every((e) => e === null)
|
|
||||||
) return null;
|
|
||||||
|
|
||||||
if (parsed.length === 2) {
|
|
||||||
return new DateRange(parsed[0], parsed[1]);
|
|
||||||
} else {
|
|
||||||
return new DateRange(parsed[0], parsed[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Shift the range by the given number of days. This modifies the range in-place */
|
|
||||||
addDays(n: number) {
|
|
||||||
this.start?.setDate(this.start.getDate() + n);
|
|
||||||
this.end?.setDate(this.end.getDate() + n);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return a parsable string representation */
|
|
||||||
toString(): string {
|
|
||||||
let res = "";
|
|
||||||
if (this.start) res += dateToYMD(this.start);
|
|
||||||
res += "..";
|
|
||||||
if (this.end) res += dateToYMD(this.end);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return a string representation for display purposes */
|
|
||||||
format(): string {
|
|
||||||
let res = "";
|
|
||||||
if (this.start) res += formatDate(this.start);
|
|
||||||
res += " \u2013 ";
|
|
||||||
if (this.end) res += formatDate(this.end);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
74
src/lib/shared/util/index.test.ts
Normal file
74
src/lib/shared/util/index.test.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { EntityQuery } from "$lib/shared/model";
|
||||||
|
import { ZEntriesQuery } from "$lib/shared/model/validation";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getQueryUrl, humanDate, DateRange, parseQueryUrl,
|
||||||
|
} from ".";
|
||||||
|
|
||||||
|
const MINUTE = 60000;
|
||||||
|
const HOUR = 3_600_000;
|
||||||
|
const DAY = 24 * HOUR;
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ s: 0, txt: "jetzt gerade" },
|
||||||
|
{ s: -30 * MINUTE, txt: "vor 30 Minuten" },
|
||||||
|
{ s: -11 * HOUR, txt: "vor 11 Stunden" },
|
||||||
|
{ s: -DAY, txt: "gestern" },
|
||||||
|
{ s: -2.5 * DAY, txt: "vor 2 Tagen" },
|
||||||
|
{ s: -2.6 * DAY, txt: "vor 3 Tagen" },
|
||||||
|
{ s: -3.5 * DAY, txt: "vor 3 Tagen" },
|
||||||
|
{ s: -4 * DAY, txt: "am 28.12.2023, 12:00" },
|
||||||
|
|
||||||
|
{ s: 28 * MINUTE, txt: "in 28 Minuten" },
|
||||||
|
{ s: 8 * HOUR, txt: "in 8 Stunden" },
|
||||||
|
{ s: DAY, txt: "morgen" },
|
||||||
|
{ s: 2.6 * DAY, txt: "in 3 Tagen" },
|
||||||
|
{ s: 4 * DAY, txt: "am 05.01.2024, 12:00" },
|
||||||
|
])("humanDate", ({ s, txt }) => {
|
||||||
|
const mockDate = new Date(2024, 0, 1, 12, 0);
|
||||||
|
vi.setSystemTime(mockDate);
|
||||||
|
|
||||||
|
const dt = new Date(Number(mockDate) + s);
|
||||||
|
const res = humanDate(dt, true);
|
||||||
|
expect(res).toBe(txt);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getQueryUrl", () => {
|
||||||
|
const query: EntityQuery = {
|
||||||
|
filter: {
|
||||||
|
author: [{ id: 2, name: "Max" }],
|
||||||
|
category: [{ id: 1, name: "Blutabnahme" }, { id: 2, name: "Labortests" }],
|
||||||
|
done: true,
|
||||||
|
search: "Hello World",
|
||||||
|
},
|
||||||
|
pagination: { limit: 10, offset: 20 },
|
||||||
|
sort: { field: "room", asc: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryUrl = getQueryUrl(query, "");
|
||||||
|
expect(queryUrl).toBe("?filter%5Bauthor%5D%5B0%5D%5Bid%5D=2&filter%5Bauthor%5D%5B0%5D%5Bname%5D=Max&filter%5Bcategory%5D%5B0%5D%5Bid%5D=1&filter%5Bcategory%5D%5B0%5D%5Bname%5D=Blutabnahme&filter%5Bcategory%5D%5B1%5D%5Bid%5D=2&filter%5Bcategory%5D%5B1%5D%5Bname%5D=Labortests&filter%5Bdone%5D=true&filter%5Bsearch%5D=Hello%20World&pagination%5Blimit%5D=10&pagination%5Boffset%5D=20&sort%5Bfield%5D=room&sort%5Basc%5D=true");
|
||||||
|
|
||||||
|
const decoded = ZEntriesQuery.parse(parseQueryUrl(queryUrl));
|
||||||
|
expect(decoded).toStrictEqual(query);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ s: "", exp: null },
|
||||||
|
{ s: "..", exp: null },
|
||||||
|
{ s: "foo..bar", exp: null },
|
||||||
|
{ s: "2024-04-15", exp: new DateRange(new Date("2024-04-15"), new Date("2024-04-15")) },
|
||||||
|
{ s: "2024-04-13..2024-04-20", exp: new DateRange(new Date("2024-04-13"), new Date("2024-04-20")) },
|
||||||
|
{ s: "2024-04-13..", exp: new DateRange(new Date("2024-04-13"), null) },
|
||||||
|
{ s: "..2024-04-20", exp: new DateRange(null, new Date("2024-04-20")) },
|
||||||
|
])("parse date range $s", ({ s, exp }) => {
|
||||||
|
const res = DateRange.parse(s);
|
||||||
|
expect(res).toStrictEqual(exp);
|
||||||
|
|
||||||
|
if (res && s !== "2024-04-15") {
|
||||||
|
expect(res.toString()).toBe(s);
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,2 +1,251 @@
|
||||||
export * from "./util";
|
import { goto } from "$app/navigation";
|
||||||
export * from "./date";
|
|
||||||
|
import { isRedirect, error } from "@sveltejs/kit";
|
||||||
|
import { TRPCClientError } from "@trpc/client";
|
||||||
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
|
import qs from "qs";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import type { EntityQuery, Patient } from "$lib/shared/model";
|
||||||
|
import type { RouterOutput } from "../trpc";
|
||||||
|
|
||||||
|
const LOCALE = "de-DE";
|
||||||
|
|
||||||
|
function coerceDate(date: Date | string): Date {
|
||||||
|
if (!(date instanceof Date)) {
|
||||||
|
return new Date(date);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(date: Date | string, time = false): string {
|
||||||
|
const dt = coerceDate(date);
|
||||||
|
if (time) {
|
||||||
|
return dt.toLocaleString(LOCALE, {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dt.toLocaleDateString(LOCALE, {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const MS_PER_DAY = 86400000;
|
||||||
|
|
||||||
|
function dateDiffInDays(a: Date, b: Date): number {
|
||||||
|
const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
|
||||||
|
const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
|
||||||
|
|
||||||
|
return Math.round((utc2 - utc1) / MS_PER_DAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function humanDate(date: Date | string, time = false): string {
|
||||||
|
const now = new Date();
|
||||||
|
const dt = coerceDate(date);
|
||||||
|
const threshold = 302400000; // 3.5 * 24 * 3_600_000
|
||||||
|
const diff = Number(dt) - Number(now); // pos: Future, neg: Past
|
||||||
|
if (Math.abs(diff) > threshold) return `am ${formatDate(date, time)}`;
|
||||||
|
|
||||||
|
const intl = new Intl.RelativeTimeFormat(LOCALE);
|
||||||
|
|
||||||
|
const diffDays = dateDiffInDays(now, dt);
|
||||||
|
if (diffDays !== 0) {
|
||||||
|
if (diffDays === 1) return "morgen";
|
||||||
|
if (diffDays === -1) return "gestern";
|
||||||
|
return intl.format(diffDays, "day");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time) {
|
||||||
|
const diffHours = Math.round(diff / 3_600_000);
|
||||||
|
if (diffHours !== 0) return intl.format(diffHours, "hour");
|
||||||
|
|
||||||
|
const diffMinutes = Math.round(diff / 60_000);
|
||||||
|
if (diffMinutes !== 0) return intl.format(diffMinutes, "minute");
|
||||||
|
}
|
||||||
|
|
||||||
|
return time ? "jetzt gerade" : "heute";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBool(val: boolean): string {
|
||||||
|
return val ? "Ja" : "Nein";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encode an entity query (search and filter) into an URL */
|
||||||
|
export function getQueryUrl(query: EntityQuery, basePath: string): string {
|
||||||
|
if (Object.values(query).filter((q) => q !== undefined).length === 0) return basePath;
|
||||||
|
return basePath + "?" + qs.stringify(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function parseQueryUrl(search: string): any {
|
||||||
|
return qs.parse(search, { ignoreQueryPrefix: true, allowDots: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gotoEntityQuery(query: EntityQuery, basePath: string) {
|
||||||
|
if (window && window.location.pathname.startsWith(`${basePath}/`)) {
|
||||||
|
if (window.location.search) {
|
||||||
|
const oldQuery: EntityQuery = parseQueryUrl(window.location.search);
|
||||||
|
const newQuery: EntityQuery = {
|
||||||
|
filter: { ...oldQuery.filter, ...query.filter },
|
||||||
|
sort: query.sort,
|
||||||
|
};
|
||||||
|
goto(getQueryUrl(newQuery, basePath));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goto(getQueryUrl(query, basePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wrap a page load query to handle occuring errors
|
||||||
|
*
|
||||||
|
* Converts TRPC errors to SvelteKit ones
|
||||||
|
*/
|
||||||
|
export async function loadWrap<T>(f: () => Promise<T>) {
|
||||||
|
try {
|
||||||
|
return await f();
|
||||||
|
} catch (e) {
|
||||||
|
if (isRedirect(e)) {
|
||||||
|
throw e;
|
||||||
|
} else if (e instanceof TRPCClientError) {
|
||||||
|
error(e.data?.httpStatus ?? 500, e.message);
|
||||||
|
} else if (e instanceof ZodError) {
|
||||||
|
error(400, e.message);
|
||||||
|
} else if (e instanceof Error) {
|
||||||
|
error(500, e.message);
|
||||||
|
} else {
|
||||||
|
error(500, "unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseUrl(url: URL): string {
|
||||||
|
return `${url.protocol}//${url.host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateToYMD(date: Date): string {
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Debouncer {
|
||||||
|
private delay: number;
|
||||||
|
|
||||||
|
private handler: () => unknown;
|
||||||
|
|
||||||
|
private timeout: number | null = null;
|
||||||
|
|
||||||
|
constructor(delay: number, handler: () => unknown) {
|
||||||
|
this.delay = delay;
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
if (this.timeout) window.clearTimeout(this.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger() {
|
||||||
|
this.clear();
|
||||||
|
this.timeout = window.setTimeout(this.handler, this.delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
now() {
|
||||||
|
this.clear();
|
||||||
|
this.handler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeHtml(s: string): string {
|
||||||
|
return DOMPurify.sanitize(s, { FORBID_TAGS: ["img"] });
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DateRange {
|
||||||
|
start: Date | null;
|
||||||
|
|
||||||
|
end: Date | null;
|
||||||
|
|
||||||
|
constructor(start: Date | null, end: Date | null) {
|
||||||
|
if (start === null && end == null) throw Error("this is not a range");
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Construct a new DateRange with the given length in days */
|
||||||
|
static withLength(start: Date, nDays: number): DateRange {
|
||||||
|
const end = new Date(start);
|
||||||
|
end.setDate(start.getDate() + nDays);
|
||||||
|
return new DateRange(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a date range of the current calendar week */
|
||||||
|
static thisWeek(): DateRange {
|
||||||
|
const dayStart = new Date();
|
||||||
|
// Correct for timezone
|
||||||
|
dayStart.setMinutes(dayStart.getMinutes() - dayStart.getTimezoneOffset());
|
||||||
|
|
||||||
|
const todayWd = dayStart.getDay();
|
||||||
|
// Day starts at Sunday (0)
|
||||||
|
const daysMinus = todayWd === 0 ? 6 : todayWd - 1;
|
||||||
|
dayStart.setDate(dayStart.getDate() - daysMinus);
|
||||||
|
return DateRange.withLength(dayStart, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse a date range from a string
|
||||||
|
*
|
||||||
|
* Accepted formats:
|
||||||
|
* - Single date `2024-04-15` => `2024-04-15..2024-04-15`
|
||||||
|
* - Range with 2 ends: `2024-04-13..2024-04-20`
|
||||||
|
* - Range with 1 end: `2024-04-13..`; `..2024-04-20`
|
||||||
|
*/
|
||||||
|
static parse(s: string): DateRange | null {
|
||||||
|
const parts = s.split("..", 2);
|
||||||
|
const parsed = parts.map((p) => {
|
||||||
|
if (p.length === 0) return null;
|
||||||
|
return new Date(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsed.length === 0
|
||||||
|
// @ts-expect-error Parsed dates can be NaN
|
||||||
|
|| parsed.findIndex(isNaN) !== -1
|
||||||
|
|| parsed.every((e) => e === null)
|
||||||
|
) return null;
|
||||||
|
|
||||||
|
if (parsed.length === 2) {
|
||||||
|
return new DateRange(parsed[0], parsed[1]);
|
||||||
|
} else {
|
||||||
|
return new DateRange(parsed[0], parsed[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shift the range by the given number of days. This modifies the range in-place */
|
||||||
|
addDays(n: number) {
|
||||||
|
this.start?.setDate(this.start.getDate() + n);
|
||||||
|
this.end?.setDate(this.end.getDate() + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return a parsable string representation */
|
||||||
|
toString(): string {
|
||||||
|
let res = "";
|
||||||
|
if (this.start) res += dateToYMD(this.start);
|
||||||
|
res += "..";
|
||||||
|
if (this.end) res += dateToYMD(this.end);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return a string representation for display purposes */
|
||||||
|
format(): string {
|
||||||
|
let res = "";
|
||||||
|
if (this.start) res += formatDate(this.start);
|
||||||
|
res += " \u2013 ";
|
||||||
|
if (this.end) res += formatDate(this.end);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPatientName(patient: RouterOutput["patient"]["list"]["items"][0]): string {
|
||||||
|
return `${patient.first_name} ${patient.last_name} (${patient.age})`;
|
||||||
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { expect, it } from "vitest";
|
|
||||||
|
|
||||||
import type { EntityQuery } from "$lib/shared/model";
|
|
||||||
import { ZEntriesQuery } from "$lib/shared/model/validation";
|
|
||||||
|
|
||||||
import {
|
|
||||||
getQueryUrl, parseQueryUrl,
|
|
||||||
} from ".";
|
|
||||||
|
|
||||||
it("getQueryUrl", () => {
|
|
||||||
const query: EntityQuery = {
|
|
||||||
filter: {
|
|
||||||
author: [{ id: 2, name: "Max" }],
|
|
||||||
category: [{ id: 1, name: "Blutabnahme" }, { id: 2, name: "Labortests" }],
|
|
||||||
done: true,
|
|
||||||
search: "Hello World",
|
|
||||||
},
|
|
||||||
pagination: { limit: 10, offset: 20 },
|
|
||||||
sort: { field: "room", asc: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryUrl = getQueryUrl(query, "");
|
|
||||||
expect(queryUrl).toBe("?filter%5Bauthor%5D%5B0%5D%5Bid%5D=2&filter%5Bauthor%5D%5B0%5D%5Bname%5D=Max&filter%5Bcategory%5D%5B0%5D%5Bid%5D=1&filter%5Bcategory%5D%5B0%5D%5Bname%5D=Blutabnahme&filter%5Bcategory%5D%5B1%5D%5Bid%5D=2&filter%5Bcategory%5D%5B1%5D%5Bname%5D=Labortests&filter%5Bdone%5D=true&filter%5Bsearch%5D=Hello%20World&pagination%5Blimit%5D=10&pagination%5Boffset%5D=20&sort%5Bfield%5D=room&sort%5Basc%5D=true");
|
|
||||||
|
|
||||||
const decoded = ZEntriesQuery.parse(parseQueryUrl(queryUrl));
|
|
||||||
expect(decoded).toStrictEqual(query);
|
|
||||||
});
|
|
|
@ -1,105 +0,0 @@
|
||||||
import { goto } from "$app/navigation";
|
|
||||||
|
|
||||||
import { isRedirect, error } from "@sveltejs/kit";
|
|
||||||
import { TRPCClientError } from "@trpc/client";
|
|
||||||
import DOMPurify from "isomorphic-dompurify";
|
|
||||||
import qs from "qs";
|
|
||||||
import { ZodError } from "zod";
|
|
||||||
|
|
||||||
import type { EntityQuery } from "$lib/shared/model";
|
|
||||||
import type { RouterOutput } from "$lib/shared/trpc";
|
|
||||||
|
|
||||||
export function formatBool(val: boolean): string {
|
|
||||||
return val ? "Ja" : "Nein";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Encode an entity query (search and filter) into an URL */
|
|
||||||
export function getQueryUrl(query: EntityQuery, basePath: string): string {
|
|
||||||
if (Object.values(query).filter((q) => q !== undefined).length === 0) return basePath;
|
|
||||||
return basePath + "?" + qs.stringify(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function parseQueryUrl(search: string): any {
|
|
||||||
return qs.parse(search, { ignoreQueryPrefix: true, allowDots: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function gotoEntityQuery(query: EntityQuery, basePath: string) {
|
|
||||||
if (window && window.location.pathname.startsWith(`${basePath}/`)) {
|
|
||||||
if (window.location.search) {
|
|
||||||
const oldQuery: EntityQuery = parseQueryUrl(window.location.search);
|
|
||||||
const newQuery: EntityQuery = {
|
|
||||||
filter: { ...oldQuery.filter, ...query.filter },
|
|
||||||
sort: query.sort,
|
|
||||||
};
|
|
||||||
goto(getQueryUrl(newQuery, basePath));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
goto(getQueryUrl(query, basePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Wrap a page load query to handle occuring errors
|
|
||||||
*
|
|
||||||
* Converts TRPC errors to SvelteKit ones
|
|
||||||
*/
|
|
||||||
export async function loadWrap<T>(f: () => Promise<T>) {
|
|
||||||
try {
|
|
||||||
return await f();
|
|
||||||
} catch (e) {
|
|
||||||
if (isRedirect(e)) {
|
|
||||||
throw e;
|
|
||||||
} else if (e instanceof TRPCClientError) {
|
|
||||||
error(e.data?.httpStatus ?? 500, e.message);
|
|
||||||
} else if (e instanceof ZodError) {
|
|
||||||
error(400, e.message);
|
|
||||||
} else if (e instanceof Error) {
|
|
||||||
error(500, e.message);
|
|
||||||
} else {
|
|
||||||
error(500, "unknown error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function baseUrl(url: URL): string {
|
|
||||||
return `${url.protocol}//${url.host}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Debouncer {
|
|
||||||
private delay: number;
|
|
||||||
|
|
||||||
private handler: () => unknown;
|
|
||||||
|
|
||||||
private timeout: number | null = null;
|
|
||||||
|
|
||||||
constructor(delay: number, handler: () => unknown) {
|
|
||||||
this.delay = delay;
|
|
||||||
this.handler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
if (this.timeout) window.clearTimeout(this.timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
trigger() {
|
|
||||||
this.clear();
|
|
||||||
this.timeout = window.setTimeout(this.handler, this.delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
now() {
|
|
||||||
this.clear();
|
|
||||||
this.handler();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sanitizeHtml(s: string): string {
|
|
||||||
return DOMPurify.sanitize(s, { FORBID_TAGS: ["img"] });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatPatientName(patient: RouterOutput["patient"]["list"]["items"][0]): string {
|
|
||||||
return `${patient.first_name} ${patient.last_name} (${patient.age})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function divFloor(a: number, b: number): number {
|
|
||||||
return Math.floor(a / b);
|
|
||||||
}
|
|
|
@ -53,7 +53,7 @@
|
||||||
filter: {
|
filter: {
|
||||||
done: false,
|
done: false,
|
||||||
station: selection ? [selection] : undefined,
|
station: selection ? [selection] : undefined,
|
||||||
date: dateRange ? [{ id: dateRange.toString() }] : undefined,
|
date: dateRange ? [{ id: dateRange.toString(), name: dateRange.format() }] : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
import { redirect } from "@sveltejs/kit";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { URL_VISIT } from "$lib/shared/constants";
|
|
||||||
import { ZEntriesQuery } from "$lib/shared/model/validation";
|
import { ZEntriesQuery } from "$lib/shared/model/validation";
|
||||||
import { trpc } from "$lib/shared/trpc";
|
import { trpc } from "$lib/shared/trpc";
|
||||||
import {
|
import { DateRange, loadWrap, parseQueryUrl } from "$lib/shared/util";
|
||||||
DateRange, getQueryUrl, loadWrap, parseQueryUrl,
|
|
||||||
} from "$lib/shared/util";
|
|
||||||
|
|
||||||
export const load: PageLoad = async (event) => {
|
export const load: PageLoad = async (event) => {
|
||||||
return loadWrap(async () => {
|
return loadWrap(async () => {
|
||||||
|
@ -19,17 +15,13 @@ export const load: PageLoad = async (event) => {
|
||||||
query = ZEntriesQuery.parse(decoded);
|
query = ZEntriesQuery.parse(decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default filter (current week)
|
||||||
|
const drange = DateRange.thisWeek();
|
||||||
if (!query.filter) {
|
if (!query.filter) {
|
||||||
const url = getQueryUrl({
|
query.filter = {
|
||||||
filter: {
|
date: [{ id: drange.toString() }],
|
||||||
done: false,
|
};
|
||||||
date: [{ id: DateRange.thisWeek().toString() }],
|
|
||||||
},
|
|
||||||
}, URL_VISIT);
|
|
||||||
redirect(302, url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort entries by date
|
|
||||||
if (!query.sort) {
|
if (!query.sort) {
|
||||||
query.sort = { field: "date" };
|
query.sort = { field: "date" };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue