Add UI for articles.
This commit is contained in:
parent
7853ca4ee6
commit
8edab39363
26 changed files with 1282 additions and 79 deletions
|
@ -4,4 +4,6 @@ DB_HOST="localhost"
|
|||
DB_PORT=5432
|
||||
DB_USER="legi"
|
||||
# Change value of DB_PASSWORD!
|
||||
DB_PASSWORD="legi"
|
||||
DB_PASSWORD="legi"
|
||||
|
||||
TITLE="Tricoteuses Données légales (exemple)"
|
||||
|
|
140
package-lock.json
generated
140
package-lock.json
generated
|
@ -10,6 +10,8 @@
|
|||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@auditors/core": "^0.3.0",
|
||||
"@iconify-icons/codicon": "^1.2.15",
|
||||
"@iconify/svelte": "^2.2.1",
|
||||
"@playwright/test": "^1.22.2",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.86",
|
||||
"@sveltejs/kit": "next",
|
||||
|
@ -18,6 +20,7 @@
|
|||
"@types/he": "^1.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"augmented-data-viewer": "^0.1.1",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"daisyui": "^2.18.1",
|
||||
"dotenv": "^16.0.1",
|
||||
|
@ -118,6 +121,30 @@
|
|||
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@iconify-icons/codicon": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@iconify-icons/codicon/-/codicon-1.2.15.tgz",
|
||||
"integrity": "sha512-CrRvt+0kKM6LVWNFErvWFOZEFwRPnvW2HLpIab148WNECXfNgVdTYhZRwtClNq59pEZ13cv0lg+cQm/ZDGlm+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@iconify/types": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@iconify/svelte": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/svelte/-/svelte-2.2.1.tgz",
|
||||
"integrity": "sha512-eWZq8CRrr3WfnKAj8SWknfE3S/d+j/AzEcypeJaHurS1s4zTdFnkjATcFa8lerGtcX0PAtXiVL94tbIEd69N+w==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "http://github.com/sponsors/cyberalien"
|
||||
}
|
||||
},
|
||||
"node_modules/@iconify/types": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
|
||||
"integrity": "sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
|
@ -630,6 +657,16 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/augmented-data-viewer": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/augmented-data-viewer/-/augmented-data-viewer-0.1.1.tgz",
|
||||
"integrity": "sha512-Hn6MA2SOkiA028dhNtZnqozERvhPehWBaV8ebTtrB2wMozqfsUZlI+jNVKJKosMtyjduRi13Pv6RSzKFYPYp5Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@iconify-icons/codicon": "^1.2.15",
|
||||
"@iconify/svelte": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.8",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz",
|
||||
|
@ -756,9 +793,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001374",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001374.tgz",
|
||||
"integrity": "sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==",
|
||||
"version": "1.0.30001375",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz",
|
||||
"integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1029,9 +1066,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.212",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.212.tgz",
|
||||
"integrity": "sha512-LjQUg1SpLj2GfyaPDVBUHdhmlDU1vDB4f0mJWSGkISoXQrn5/lH3ECPCuo2Bkvf6Y30wO+b69te+rZK/llZmjg==",
|
||||
"version": "1.4.213",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.213.tgz",
|
||||
"integrity": "sha512-+3DbGHGOCHTVB/Ms63bGqbyC1b8y7Fk86+7ltssB8NQrZtSCvZG6eooSl9U2Q0yw++fL2DpHKOdTU0NVEkFObg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/es6-promise": {
|
||||
|
@ -1990,15 +2027,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh/node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
|
@ -2781,6 +2809,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
|
@ -3401,13 +3438,13 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.0.4.tgz",
|
||||
"integrity": "sha512-NU304nqnBeOx2MkQnskBQxVsa0pRAH5FphokTGmyy8M3oxbvw7qAXts2GORxs+h/2vKsD+osMhZ7An6yK6F1dA==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.0.5.tgz",
|
||||
"integrity": "sha512-bRvrt9Tw8EGW4jj64aYFTnVg134E8hgDxyl/eEHnxiGqYk7/pTPss6CWlurqPOUzqvEoZkZ58Ws+Iu8MB87iMA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.14.47",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss": "^8.4.16",
|
||||
"resolve": "^1.22.1",
|
||||
"rollup": "^2.75.6"
|
||||
},
|
||||
|
@ -3562,6 +3599,27 @@
|
|||
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
|
||||
"dev": true
|
||||
},
|
||||
"@iconify-icons/codicon": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@iconify-icons/codicon/-/codicon-1.2.15.tgz",
|
||||
"integrity": "sha512-CrRvt+0kKM6LVWNFErvWFOZEFwRPnvW2HLpIab148WNECXfNgVdTYhZRwtClNq59pEZ13cv0lg+cQm/ZDGlm+w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@iconify/types": "*"
|
||||
}
|
||||
},
|
||||
"@iconify/svelte": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/svelte/-/svelte-2.2.1.tgz",
|
||||
"integrity": "sha512-eWZq8CRrr3WfnKAj8SWknfE3S/d+j/AzEcypeJaHurS1s4zTdFnkjATcFa8lerGtcX0PAtXiVL94tbIEd69N+w==",
|
||||
"dev": true
|
||||
},
|
||||
"@iconify/types": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
|
||||
"integrity": "sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
|
@ -3903,6 +3961,16 @@
|
|||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true
|
||||
},
|
||||
"augmented-data-viewer": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/augmented-data-viewer/-/augmented-data-viewer-0.1.1.tgz",
|
||||
"integrity": "sha512-Hn6MA2SOkiA028dhNtZnqozERvhPehWBaV8ebTtrB2wMozqfsUZlI+jNVKJKosMtyjduRi13Pv6RSzKFYPYp5Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@iconify-icons/codicon": "^1.2.15",
|
||||
"@iconify/svelte": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"autoprefixer": {
|
||||
"version": "10.4.8",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz",
|
||||
|
@ -3979,9 +4047,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001374",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001374.tgz",
|
||||
"integrity": "sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==",
|
||||
"version": "1.0.30001375",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz",
|
||||
"integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
|
@ -4177,9 +4245,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.212",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.212.tgz",
|
||||
"integrity": "sha512-LjQUg1SpLj2GfyaPDVBUHdhmlDU1vDB4f0mJWSGkISoXQrn5/lH3ECPCuo2Bkvf6Y30wO+b69te+rZK/llZmjg==",
|
||||
"version": "1.4.213",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.213.tgz",
|
||||
"integrity": "sha512-+3DbGHGOCHTVB/Ms63bGqbyC1b8y7Fk86+7ltssB8NQrZtSCvZG6eooSl9U2Q0yw++fL2DpHKOdTU0NVEkFObg==",
|
||||
"dev": true
|
||||
},
|
||||
"es6-promise": {
|
||||
|
@ -4799,14 +4867,6 @@
|
|||
"requires": {
|
||||
"parent-module": "^1.0.0",
|
||||
"resolve-from": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"imurmurhash": {
|
||||
|
@ -5351,6 +5411,12 @@
|
|||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true
|
||||
},
|
||||
"reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
|
@ -5752,14 +5818,14 @@
|
|||
"dev": true
|
||||
},
|
||||
"vite": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.0.4.tgz",
|
||||
"integrity": "sha512-NU304nqnBeOx2MkQnskBQxVsa0pRAH5FphokTGmyy8M3oxbvw7qAXts2GORxs+h/2vKsD+osMhZ7An6yK6F1dA==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.0.5.tgz",
|
||||
"integrity": "sha512-bRvrt9Tw8EGW4jj64aYFTnVg134E8hgDxyl/eEHnxiGqYk7/pTPss6CWlurqPOUzqvEoZkZ58Ws+Iu8MB87iMA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild": "^0.14.47",
|
||||
"fsevents": "~2.3.2",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss": "^8.4.16",
|
||||
"resolve": "^1.22.1",
|
||||
"rollup": "^2.75.6"
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@auditors/core": "^0.3.0",
|
||||
"@iconify-icons/codicon": "^1.2.15",
|
||||
"@iconify/svelte": "^2.2.1",
|
||||
"@playwright/test": "^1.22.2",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.86",
|
||||
"@sveltejs/kit": "next",
|
||||
|
@ -31,6 +33,7 @@
|
|||
"@types/he": "^1.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"augmented-data-viewer": "^0.1.1",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"daisyui": "^2.18.1",
|
||||
"dotenv": "^16.0.1",
|
||||
|
|
4
src/app.d.ts
vendored
4
src/app.d.ts
vendored
|
@ -6,6 +6,8 @@ declare namespace App {
|
|||
// interface Platform {}
|
||||
// interface PrivateEnv {}
|
||||
// interface PublicEnv {}
|
||||
// interface Session {}
|
||||
interface Session {
|
||||
title: string
|
||||
}
|
||||
// interface Stuff {}
|
||||
}
|
||||
|
|
7
src/hooks.ts
Normal file
7
src/hooks.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import config from "$lib/server/config"
|
||||
|
||||
export function getSession(): App.Session {
|
||||
return {
|
||||
title: config.title,
|
||||
}
|
||||
}
|
171
src/lib/auditors/queries.ts
Normal file
171
src/lib/auditors/queries.ts
Normal file
|
@ -0,0 +1,171 @@
|
|||
import type { Audit, Auditor } from "@auditors/core"
|
||||
import {
|
||||
auditArray,
|
||||
auditChain,
|
||||
auditFunction,
|
||||
auditInteger,
|
||||
auditSetNullish,
|
||||
auditStringToNumber,
|
||||
auditTest,
|
||||
auditTrimString,
|
||||
} from "@auditors/core"
|
||||
|
||||
export function auditPaginationQuery(
|
||||
audit: Audit,
|
||||
query: URLSearchParams,
|
||||
): [unknown, unknown] {
|
||||
if (query == null) {
|
||||
return [query, null]
|
||||
}
|
||||
if (!(query instanceof URLSearchParams)) {
|
||||
return audit.unexpectedType(query, "URLSearchParams")
|
||||
}
|
||||
|
||||
const data: { [key: string]: unknown } = {}
|
||||
for (const [key, value] of query.entries()) {
|
||||
let values = data[key] as string[] | undefined
|
||||
if (values === undefined) {
|
||||
values = data[key] = []
|
||||
}
|
||||
values.push(value)
|
||||
}
|
||||
const errors: { [key: string]: unknown } = {}
|
||||
const remainingKeys = new Set(Object.keys(data))
|
||||
|
||||
audit.attribute(
|
||||
data,
|
||||
"limit",
|
||||
true,
|
||||
errors,
|
||||
remainingKeys,
|
||||
auditQuerySingleton(
|
||||
auditTrimString,
|
||||
auditStringToNumber,
|
||||
auditInteger,
|
||||
auditTest(
|
||||
(value: number) => value >= 1,
|
||||
"Value must be greater than or equal to 1",
|
||||
),
|
||||
auditTest(
|
||||
(value: number) => value <= 100,
|
||||
"Value must be less than or equal to 100",
|
||||
),
|
||||
auditSetNullish(10),
|
||||
),
|
||||
)
|
||||
audit.attribute(
|
||||
data,
|
||||
"offset",
|
||||
true,
|
||||
errors,
|
||||
remainingKeys,
|
||||
auditQuerySingleton(
|
||||
auditTrimString,
|
||||
auditStringToNumber,
|
||||
auditInteger,
|
||||
auditTest(
|
||||
(value: number) => value >= 0,
|
||||
"Value must be greater than or equal to 0",
|
||||
),
|
||||
auditSetNullish(0),
|
||||
),
|
||||
)
|
||||
|
||||
// Keep the remaining keys as is.
|
||||
remainingKeys.clear()
|
||||
|
||||
return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({}))
|
||||
}
|
||||
|
||||
export function auditQuerySingleton(...auditors: Auditor[]) {
|
||||
return auditChain(
|
||||
auditArray(),
|
||||
auditTest(
|
||||
(values) => values.length <= 1,
|
||||
"Parameter must be present only once in query",
|
||||
),
|
||||
auditFunction((value) => value[0]),
|
||||
...auditors,
|
||||
)
|
||||
}
|
||||
|
||||
export function auditSearchQuery(
|
||||
audit: Audit,
|
||||
query: URLSearchParams,
|
||||
): [unknown, unknown] {
|
||||
if (query == null) {
|
||||
return [query, null]
|
||||
}
|
||||
if (!(query instanceof URLSearchParams)) {
|
||||
return audit.unexpectedType(query, "URLSearchParams")
|
||||
}
|
||||
|
||||
const data: { [key: string]: unknown } = {}
|
||||
for (const [key, value] of query.entries()) {
|
||||
let values = data[key] as string[] | undefined
|
||||
if (values === undefined) {
|
||||
values = data[key] = []
|
||||
}
|
||||
values.push(value)
|
||||
}
|
||||
const errors: { [key: string]: unknown } = {}
|
||||
const remainingKeys = new Set(Object.keys(data))
|
||||
|
||||
auditSearchQueryContent(audit, data, errors, remainingKeys)
|
||||
|
||||
return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({}))
|
||||
}
|
||||
|
||||
export function auditSearchQueryContent(
|
||||
audit: Audit,
|
||||
data: { [key: string]: unknown },
|
||||
errors: { [key: string]: unknown },
|
||||
remainingKeys: Set<string>,
|
||||
) {
|
||||
audit.attribute(
|
||||
data,
|
||||
"limit",
|
||||
true,
|
||||
errors,
|
||||
remainingKeys,
|
||||
auditQuerySingleton(
|
||||
auditTrimString,
|
||||
auditStringToNumber,
|
||||
auditInteger,
|
||||
auditTest(
|
||||
(value: number) => value >= 1,
|
||||
"Value must be greater than or equal to 1",
|
||||
),
|
||||
auditTest(
|
||||
(value: number) => value <= 1000,
|
||||
"Value must be less than or equal to 1000",
|
||||
),
|
||||
auditSetNullish(20),
|
||||
),
|
||||
)
|
||||
audit.attribute(
|
||||
data,
|
||||
"offset",
|
||||
true,
|
||||
errors,
|
||||
remainingKeys,
|
||||
auditQuerySingleton(
|
||||
auditTrimString,
|
||||
auditStringToNumber,
|
||||
auditInteger,
|
||||
auditTest(
|
||||
(value: number) => value >= 0,
|
||||
"Value must be greater than or equal to 0",
|
||||
),
|
||||
auditSetNullish(0),
|
||||
),
|
||||
)
|
||||
audit.attribute(
|
||||
data,
|
||||
"q",
|
||||
true,
|
||||
errors,
|
||||
remainingKeys,
|
||||
auditQuerySingleton(auditTrimString),
|
||||
)
|
||||
}
|
125
src/lib/components/Pagination.svelte
Normal file
125
src/lib/components/Pagination.svelte
Normal file
|
@ -0,0 +1,125 @@
|
|||
<script lang="ts">
|
||||
import { cleanAudit } from "@auditors/core"
|
||||
|
||||
import { page } from "$app/stores"
|
||||
import { auditPaginationQuery } from "$lib/auditors/queries"
|
||||
import type { PaginationQuery } from "$lib/queries"
|
||||
|
||||
export let count: number | undefined | null = undefined // Count is not always known.
|
||||
export let currentPageCount: number
|
||||
|
||||
$: url = $page.url
|
||||
|
||||
$: query = ensureValidQuery(url.searchParams)
|
||||
|
||||
$: limit = query.limit
|
||||
|
||||
$: offset = query.offset
|
||||
|
||||
$: pathname = url.pathname
|
||||
|
||||
function ensureValidQuery(query: URLSearchParams): PaginationQuery {
|
||||
const [validQuery, queryError] = auditPaginationQuery(
|
||||
cleanAudit,
|
||||
query,
|
||||
) as [PaginationQuery, unknown]
|
||||
if (queryError !== null) {
|
||||
console.warn(
|
||||
`Query error at ${pathname}: ${JSON.stringify(
|
||||
queryError,
|
||||
null,
|
||||
2,
|
||||
)}\n\n${JSON.stringify(validQuery, null, 2)}`,
|
||||
)
|
||||
return { limit: 20, offset: 0 }
|
||||
}
|
||||
return validQuery
|
||||
}
|
||||
|
||||
function newPaginationUrl(
|
||||
pathname: string,
|
||||
query: Partial<PaginationQuery> | { [key: string]: string },
|
||||
{ limit, offset }: { limit?: number; offset?: number } = {},
|
||||
) {
|
||||
query = { ...query }
|
||||
if (limit != null && limit !== 20) {
|
||||
query.limit = limit
|
||||
} else {
|
||||
delete query.limit
|
||||
}
|
||||
if (offset) {
|
||||
query.offset = offset
|
||||
} else {
|
||||
delete query.offset
|
||||
}
|
||||
const search = new URLSearchParams(
|
||||
Object.entries(query).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === "number" ? value.toString() : value,
|
||||
]),
|
||||
).toString()
|
||||
return search ? `${pathname}?${search}` : pathname
|
||||
}
|
||||
|
||||
function pageNumber(limit: number, offset: number): number {
|
||||
return Math.floor(offset / limit) + 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="my-4 flex items-baseline justify-evenly">
|
||||
<p class="mr-4">
|
||||
{#if currentPageCount === 0}
|
||||
Aucun résultat
|
||||
{:else}
|
||||
Résultats {offset + 1} à {offset + currentPageCount}{#if count != null}
|
||||
sur {count}{/if}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="btn-group">
|
||||
{#if offset > 0}
|
||||
<a
|
||||
class="btn btn-outline"
|
||||
href={newPaginationUrl(pathname, query, {
|
||||
offset: Math.max(offset - limit, 0),
|
||||
})}
|
||||
>
|
||||
Précédent
|
||||
</a>
|
||||
{:else}
|
||||
<span class="btn btn-disabled">Précédent</span>
|
||||
{/if}
|
||||
{#each [offset - 3 * limit, offset - 2 * limit, offset - limit] as previousOffset (previousOffset)}
|
||||
{#if previousOffset >= 0 && (count == null || previousOffset < count)}
|
||||
<a
|
||||
class="btn btn-outline"
|
||||
href={newPaginationUrl(pathname, query, { offset: previousOffset })}
|
||||
>
|
||||
{pageNumber(limit, previousOffset)}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
<span class="btn btn-disabled">
|
||||
{pageNumber(limit, offset)}
|
||||
<span class="sr-only">(page actuelle)</span>
|
||||
</span>
|
||||
{#each [offset + limit, offset + 2 * limit, offset + 3 * limit] as nextOffset (nextOffset)}
|
||||
{#if nextOffset >= 0 && (count == null || nextOffset < count)}
|
||||
<a
|
||||
class="btn btn-outline"
|
||||
href={newPaginationUrl(pathname, query, { offset: nextOffset })}
|
||||
>
|
||||
{pageNumber(limit, nextOffset)}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if count == null || offset + limit < count}
|
||||
<a
|
||||
class="btn btn-outline"
|
||||
href={newPaginationUrl(pathname, query, { offset: offset + limit })}
|
||||
>Suivant</a
|
||||
>
|
||||
{:else}
|
||||
<span class="btn btn-disabled">Suivant</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
21
src/lib/components/errors/ErrorAlert.svelte
Normal file
21
src/lib/components/errors/ErrorAlert.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte"
|
||||
import errorIcon from "@iconify-icons/codicon/error"
|
||||
|
||||
import { realErrorFromError } from "./errors"
|
||||
import ErrorValue from "./ErrorValue.svelte"
|
||||
|
||||
export let error: unknown
|
||||
export let hideError = false
|
||||
|
||||
$: realError = realErrorFromError(error)
|
||||
</script>
|
||||
|
||||
{#if realError !== error || (!hideError && realError != null)}
|
||||
<div class="alert alert-error">
|
||||
<div class="flex-1">
|
||||
<Icon class="mr-1" icon={errorIcon} />
|
||||
<ErrorValue bind:error frame={false} {hideError} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
61
src/lib/components/errors/ErrorValue.svelte
Normal file
61
src/lib/components/errors/ErrorValue.svelte
Normal file
|
@ -0,0 +1,61 @@
|
|||
<script lang="ts">
|
||||
import type { AuditSwitchError } from "@auditors/core"
|
||||
|
||||
import { realErrorFromError } from "./errors"
|
||||
|
||||
export let error: unknown
|
||||
export let frame = true
|
||||
export let hideError = false
|
||||
|
||||
$: realError = realErrorFromError(error)
|
||||
|
||||
function asAuditSwitchError(value: unknown) {
|
||||
return value as AuditSwitchError
|
||||
}
|
||||
|
||||
function asObject(value: unknown) {
|
||||
return value as { [key: string]: unknown }
|
||||
}
|
||||
|
||||
function changeSwitchIndex({ target }: Event) {
|
||||
const { value } = target as HTMLSelectElement
|
||||
error = {
|
||||
...asAuditSwitchError(error),
|
||||
index: parseInt(value),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if realError !== error}
|
||||
{@const switchError = asAuditSwitchError(error)}
|
||||
<div class="form-control">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text">Alternative Errors:</span>
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered w-full max-w-xs"
|
||||
on:blur={changeSwitchIndex}
|
||||
on:change={changeSwitchIndex}
|
||||
value={switchError.index}
|
||||
>
|
||||
{#each switchError.errors as _, index}
|
||||
<option value={index}>Alternative {index + 1}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !hideError}
|
||||
{#if realError !== null && typeof realError === "object"}
|
||||
<ul class={frame ? "list-inside list-disc pl-4" : null}>
|
||||
{#each Object.entries(asObject(error)) as [itemKey, itemError]}
|
||||
<li>
|
||||
<span>{itemKey}:</span>
|
||||
<svelte:self error={itemError} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else if realError != null}
|
||||
<span>{realError}</span>
|
||||
{/if}
|
||||
{/if}
|
13
src/lib/components/errors/errors.ts
Normal file
13
src/lib/components/errors/errors.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import type { AuditSwitchError } from "@auditors/core"
|
||||
|
||||
export function realErrorFromError(error: unknown): unknown {
|
||||
if (
|
||||
error !== null &&
|
||||
typeof error === "object" &&
|
||||
(error as { [key: string]: unknown })["@auditors/error"] === "switch"
|
||||
) {
|
||||
const { errors, index } = error as AuditSwitchError
|
||||
return realErrorFromError(errors[index])
|
||||
}
|
||||
return error
|
||||
}
|
128
src/lib/data.ts
Normal file
128
src/lib/data.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
export interface Article {
|
||||
META: {
|
||||
META_COMMUN: MetaCommun
|
||||
META_SPEC: {
|
||||
META_ARTICLE: {
|
||||
NUM: string
|
||||
ETAT: Etat
|
||||
TYPE: "AUTONOME"
|
||||
DATE_FIN: string
|
||||
DATE_DEBUT: string
|
||||
}
|
||||
}
|
||||
}
|
||||
CONTEXTE: {
|
||||
TEXTE: {
|
||||
TITRE_TXT: TitreTexte | TitreTexte[]
|
||||
}
|
||||
}
|
||||
VERSIONS: {
|
||||
VERSION: Array<{
|
||||
"@etat": Etat
|
||||
LIEN_ART: {
|
||||
"@id": string
|
||||
"@fin": string
|
||||
"@num": string
|
||||
"@etat": Etat
|
||||
"@debut": string
|
||||
"@origine": "LEGI"
|
||||
}
|
||||
}>
|
||||
}
|
||||
BLOC_TEXTUEL: {
|
||||
CONTENU: string // HTML
|
||||
}
|
||||
}
|
||||
|
||||
export type EliId = string
|
||||
|
||||
export interface EliVersions {}
|
||||
|
||||
export type Etat = "MODIFIE" | "VIGUEUR"
|
||||
export type LegiObject =
|
||||
| Article
|
||||
| EliId
|
||||
| EliVersions
|
||||
| Section
|
||||
| Struct
|
||||
| Version
|
||||
|
||||
export type LegiObjectType =
|
||||
| "article"
|
||||
| "eli_id"
|
||||
| "eli_versions"
|
||||
| "sections"
|
||||
| "struct"
|
||||
| "version"
|
||||
|
||||
export interface MetaCommun {
|
||||
ID: string
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
ID: string
|
||||
}
|
||||
|
||||
export interface Struct {
|
||||
META: {
|
||||
META_COMMUN: MetaCommun
|
||||
}
|
||||
}
|
||||
|
||||
export interface TitreTexte {
|
||||
"#text": string
|
||||
}
|
||||
|
||||
export interface Version {
|
||||
META: {
|
||||
META_COMMUN: MetaCommun
|
||||
}
|
||||
}
|
||||
|
||||
export function assertNeverLegiObjectType(type: never): never {
|
||||
throw `Unexpected type for LEGI object: ${type}`
|
||||
}
|
||||
|
||||
export function pathnameFromLegiObject(
|
||||
type: LegiObjectType,
|
||||
object: LegiObject,
|
||||
): string {
|
||||
switch (type) {
|
||||
case "article":
|
||||
return `/articles/${(object as Article).META.META_COMMUN.ID}`
|
||||
case "eli_id":
|
||||
return `/eli/ids/TODO`
|
||||
case "eli_versions":
|
||||
return `/eli/ids/TODO`
|
||||
case "sections":
|
||||
return `/sections/${(object as Section).ID}`
|
||||
case "struct":
|
||||
return `/structs/${(object as Struct).META.META_COMMUN.ID}`
|
||||
case "version":
|
||||
return `/versions/${(object as Version).META.META_COMMUN.ID}`
|
||||
default:
|
||||
assertNeverLegiObjectType(type)
|
||||
}
|
||||
}
|
||||
|
||||
export function pathnameFromLegiObjectId(
|
||||
type: LegiObjectType,
|
||||
id: string,
|
||||
): string {
|
||||
switch (type) {
|
||||
case "article":
|
||||
return `/articles/${id}`
|
||||
case "eli_id":
|
||||
return `/eli/ids/TODO`
|
||||
case "eli_versions":
|
||||
return `/eli/ids/TODO`
|
||||
case "sections":
|
||||
return `/sections/${id}`
|
||||
case "struct":
|
||||
return `/structs/${id}`
|
||||
case "version":
|
||||
return `/versions/${id}`
|
||||
default:
|
||||
assertNeverLegiObjectType(type)
|
||||
}
|
||||
}
|
10
src/lib/queries.ts
Normal file
10
src/lib/queries.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export interface PaginationQuery {
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export interface SearchQuery {
|
||||
limit: number
|
||||
offset: number
|
||||
q?: string
|
||||
}
|
|
@ -6,42 +6,17 @@ import path from "path"
|
|||
import type { JSONValue } from "postgres"
|
||||
import sade from "sade"
|
||||
|
||||
import type {
|
||||
Article,
|
||||
EliId,
|
||||
EliVersions,
|
||||
Section,
|
||||
Struct,
|
||||
Version,
|
||||
} from "$lib/data"
|
||||
import { db } from "$lib/server/database"
|
||||
import { walkDir } from "$lib/server/file_systems"
|
||||
|
||||
interface Article {
|
||||
META: {
|
||||
META_COMMUN: MetaCommun
|
||||
}
|
||||
BLOC_TEXTUEL: {
|
||||
CONTENU: string // HTML
|
||||
}
|
||||
}
|
||||
|
||||
type EliId = string
|
||||
|
||||
interface EliVersions {}
|
||||
|
||||
interface MetaCommun {
|
||||
ID: string
|
||||
}
|
||||
|
||||
interface Section {
|
||||
ID: string
|
||||
}
|
||||
|
||||
interface Struct {
|
||||
META: {
|
||||
META_COMMUN: MetaCommun
|
||||
}
|
||||
}
|
||||
|
||||
interface Version {
|
||||
META: {
|
||||
META_COMMUN: MetaCommun
|
||||
}
|
||||
}
|
||||
|
||||
interface XmlHeader {
|
||||
"@encoding": "UTF-8"
|
||||
"@version": "1.0"
|
||||
|
|
|
@ -32,6 +32,15 @@ export function auditConfig(
|
|||
auditDb,
|
||||
auditRequire,
|
||||
)
|
||||
audit.attribute(
|
||||
data,
|
||||
"title",
|
||||
true,
|
||||
errors,
|
||||
remainingKeys,
|
||||
auditTrimString,
|
||||
auditRequire,
|
||||
)
|
||||
|
||||
return audit.reduceRemaining(data, errors, remainingKeys)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ export interface Config {
|
|||
user: string
|
||||
password: string
|
||||
}
|
||||
title: string
|
||||
}
|
||||
|
||||
const config = {
|
||||
|
@ -20,6 +21,7 @@ const config = {
|
|||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
},
|
||||
title: process.env.TITLE,
|
||||
}
|
||||
|
||||
const [validConfig, error] = validateConfig(config) as [Config, unknown]
|
||||
|
|
150
src/lib/summaries.ts
Normal file
150
src/lib/summaries.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
import type { Access, Summarizer, Summary } from "augmented-data-viewer"
|
||||
|
||||
import {
|
||||
type Article,
|
||||
assertNeverLegiObjectType,
|
||||
type LegiObject,
|
||||
type LegiObjectType,
|
||||
pathnameFromLegiObject,
|
||||
pathnameFromLegiObjectId,
|
||||
type Section,
|
||||
type Struct,
|
||||
type Version,
|
||||
} from "$lib/data"
|
||||
|
||||
export const summarizeArticleProperties: Summarizer = (access, value) => {
|
||||
if (access?.key === "article") {
|
||||
return summarizeLegiObject(access, "article", value)
|
||||
}
|
||||
if (access?.access?.key === "articles") {
|
||||
return summarizeLegiObjectToLink(access, "article", value)
|
||||
}
|
||||
|
||||
if (access?.key === "@id" && access?.access?.key === "LIEN_ART") {
|
||||
return {
|
||||
content: value as string,
|
||||
href: pathnameFromLegiObjectId("article", value as string),
|
||||
type: "link",
|
||||
}
|
||||
}
|
||||
if (access?.key === "CONTENU") {
|
||||
return { content: value as string, type: "html" }
|
||||
}
|
||||
if (access?.access?.key === "VERSION") {
|
||||
const version = value as Article["VERSIONS"]["VERSION"][0]
|
||||
const lienArt = version.LIEN_ART
|
||||
return {
|
||||
content: {
|
||||
items: [
|
||||
`Article ${lienArt["@num"]} en vigueur`,
|
||||
...(lienArt["@fin"] === "2999-01-01"
|
||||
? [
|
||||
" depuis le ",
|
||||
{
|
||||
value: lienArt["@debut"],
|
||||
type: "date",
|
||||
},
|
||||
]
|
||||
: [
|
||||
" du ",
|
||||
{
|
||||
value: lienArt["@debut"],
|
||||
type: "date",
|
||||
},
|
||||
" au ",
|
||||
{
|
||||
value: lienArt["@fin"],
|
||||
type: "date",
|
||||
},
|
||||
]),
|
||||
],
|
||||
type: "concatenation",
|
||||
},
|
||||
href: pathnameFromLegiObjectId("article", lienArt["@id"]),
|
||||
type: "link",
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function summarizeLegiObject(
|
||||
access: Access | undefined,
|
||||
type: LegiObjectType,
|
||||
value: unknown,
|
||||
): Summary | undefined {
|
||||
switch (type) {
|
||||
case "article": {
|
||||
const article = value as Article | undefined
|
||||
if (article === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const metaArticle = article.META.META_SPEC.META_ARTICLE
|
||||
const titreTxt = article.CONTEXTE.TEXTE.TITRE_TXT
|
||||
const titreTexte = (Array.isArray(titreTxt) ? titreTxt[0] : titreTxt)[
|
||||
"#text"
|
||||
]
|
||||
return {
|
||||
items: [
|
||||
`Article ${metaArticle.TYPE} ${metaArticle.ETAT} n° ${metaArticle.NUM}, en vigueur`,
|
||||
...(metaArticle.DATE_FIN === "2999-01-01"
|
||||
? [
|
||||
" depuis le ",
|
||||
{
|
||||
value: metaArticle.DATE_DEBUT,
|
||||
type: "date",
|
||||
},
|
||||
]
|
||||
: [
|
||||
" du ",
|
||||
{
|
||||
value: metaArticle.DATE_DEBUT,
|
||||
type: "date",
|
||||
},
|
||||
" au ",
|
||||
{
|
||||
value: metaArticle.DATE_FIN,
|
||||
type: "date",
|
||||
},
|
||||
]),
|
||||
" (",
|
||||
titreTexte,
|
||||
")",
|
||||
],
|
||||
type: "concatenation",
|
||||
}
|
||||
}
|
||||
case "eli_id":
|
||||
return `/eli/ids/TODO`
|
||||
case "eli_versions":
|
||||
return `/eli/ids/TODO`
|
||||
case "sections": {
|
||||
const section = value as Section | undefined
|
||||
return section?.ID
|
||||
}
|
||||
case "struct": {
|
||||
const struct = value as Struct | undefined
|
||||
return struct?.META.META_COMMUN.ID
|
||||
}
|
||||
case "version": {
|
||||
const version = value as Version | undefined
|
||||
return version?.META.META_COMMUN.ID
|
||||
}
|
||||
default:
|
||||
assertNeverLegiObjectType(type)
|
||||
}
|
||||
}
|
||||
|
||||
export function summarizeLegiObjectToLink(
|
||||
access: Access | undefined,
|
||||
type: LegiObjectType,
|
||||
value: unknown,
|
||||
): Summary | undefined {
|
||||
const objectSummary = summarizeLegiObject(access, type, value)
|
||||
return objectSummary === undefined
|
||||
? undefined
|
||||
: {
|
||||
content: objectSummary,
|
||||
href: pathnameFromLegiObject(type, value as LegiObject),
|
||||
type: "link",
|
||||
}
|
||||
}
|
|
@ -1,5 +1,107 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "../app.css"
|
||||
|
||||
import Icon from "@iconify/svelte"
|
||||
import chevronDown from "@iconify-icons/codicon/chevron-down"
|
||||
import chevronRight from "@iconify-icons/codicon/chevron-right"
|
||||
import threeBars from "@iconify-icons/codicon/three-bars"
|
||||
|
||||
import { session } from "$app/stores"
|
||||
|
||||
type MenuItem = MenuItemLink | MenuItemSubMenu
|
||||
|
||||
interface MenuItemBase {
|
||||
href?: string
|
||||
items?: MenuItemLink[]
|
||||
label: string
|
||||
}
|
||||
|
||||
interface MenuItemLink extends MenuItemBase {
|
||||
href: string
|
||||
}
|
||||
|
||||
interface MenuItemSubMenu extends MenuItemBase {
|
||||
items: MenuItemLink[]
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ href: "/recherche", label: "Recherche" },
|
||||
{
|
||||
label: "Données",
|
||||
items: [
|
||||
{ href: "/articles", label: "Articles" },
|
||||
{ href: "/eli/ids", label: "ELI ID" },
|
||||
{ href: "/eli/versions", label: "ELI versions" },
|
||||
{ href: "/sections", label: "Sections" },
|
||||
{ href: "/structs", label: "Structs" },
|
||||
{ href: "/versions", label: "Versions" },
|
||||
],
|
||||
},
|
||||
]
|
||||
const title = $session.title
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
<div class="navbar mb-2 bg-neutral text-neutral-content">
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<button tabindex="0" class="btn btn-ghost lg:hidden">
|
||||
<Icon class="h-5 w-5" icon={threeBars} />
|
||||
</button>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu rounded-box menu-compact mt-3 w-52 bg-neutral p-2 text-neutral-content shadow"
|
||||
>
|
||||
{#each menuItems as { href, items, label }}
|
||||
{#if href !== undefined}
|
||||
<li><a {href}>{label}</a></li>
|
||||
{:else if items !== undefined}
|
||||
<li tabindex="0">
|
||||
<span class="justify-between">
|
||||
{label}
|
||||
<Icon class="h-5 w-5" icon={chevronRight} />
|
||||
</span>
|
||||
<ul class="p-2 bg-neutral text-neutral-content">
|
||||
{#each items as { href, label }}
|
||||
<li><a {href}>{label}</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<a class="btn btn-ghost text-xl normal-case" href="/"> {title}</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<ul class="menu menu-horizontal p-0">
|
||||
{#each menuItems as { href, items, label }}
|
||||
{#if href !== undefined}
|
||||
<li><a {href}>{label}</a></li>
|
||||
{:else if items !== undefined}
|
||||
<li tabindex="0">
|
||||
<span class="justify-between">
|
||||
{label}
|
||||
<Icon class="h-5 w-5" icon={chevronDown} />
|
||||
</span>
|
||||
<ul class="p-2 bg-neutral text-neutral-content">
|
||||
{#each items as { href, label }}
|
||||
<li><a {href}>{label}</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<a
|
||||
class="btn"
|
||||
href="https://git.en-root.org/tricoteuses/tricoteuses-legal-explorer"
|
||||
>Contribuer</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto w-11/12">
|
||||
<slot />
|
||||
</div>
|
||||
|
|
38
src/routes/articles/[id].svelte
Normal file
38
src/routes/articles/[id].svelte
Normal file
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import { TreeView, SummaryView } from "augmented-data-viewer"
|
||||
|
||||
import ErrorAlert from "$lib/components/errors/ErrorAlert.svelte"
|
||||
import type { Article } from "$lib/data"
|
||||
import {
|
||||
summarizeArticleProperties,
|
||||
summarizeLegiObject,
|
||||
} from "$lib/summaries"
|
||||
|
||||
export let error: unknown
|
||||
export let article: Article
|
||||
|
||||
const summary = summarizeLegiObject({ key: "article" }, "article", article)
|
||||
</script>
|
||||
|
||||
<header class="prose my-6 max-w-full">
|
||||
<h2>Article</h2>
|
||||
{#if summary !== undefined}
|
||||
<h1>
|
||||
<SummaryView {summary} />
|
||||
</h1>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if error != null}
|
||||
<ErrorAlert {error} />
|
||||
{/if}
|
||||
|
||||
{#if error == null}
|
||||
<TreeView
|
||||
access={{ key: "article" }}
|
||||
frame={false}
|
||||
open
|
||||
summarize={summarizeArticleProperties}
|
||||
value={article}
|
||||
/>
|
||||
{/if}
|
19
src/routes/articles/[id].ts
Normal file
19
src/routes/articles/[id].ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import type { RequestHandler } from "@sveltejs/kit"
|
||||
import type { JSONObject } from "@sveltejs/kit/types/private"
|
||||
|
||||
import type { Article } from "$lib/data"
|
||||
import { db } from "$lib/server/database"
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url }) => {
|
||||
const article = (
|
||||
await db<{ data: Article }[]>`
|
||||
SELECT data FROM articles
|
||||
WHERE id = ${params.id}
|
||||
`
|
||||
).map(({ data }) => data)[0]
|
||||
|
||||
if (article === undefined) {
|
||||
return { status: 404 }
|
||||
}
|
||||
return { body: { article: article as unknown as JSONObject } }
|
||||
}
|
66
src/routes/articles/index.svelte
Normal file
66
src/routes/articles/index.svelte
Normal file
|
@ -0,0 +1,66 @@
|
|||
<script lang="ts">
|
||||
// import Icon from "@iconify/svelte"
|
||||
// import searchIcon from "@iconify-icons/codicon/search"
|
||||
import { TreeView } from "augmented-data-viewer"
|
||||
|
||||
// import { page } from "$app/stores"
|
||||
import ErrorAlert from "$lib/components/errors/ErrorAlert.svelte"
|
||||
import Pagination from "$lib/components/Pagination.svelte"
|
||||
import { summarizeArticleProperties } from "$lib/summaries"
|
||||
|
||||
export let error: unknown
|
||||
export let articles: unknown[]
|
||||
</script>
|
||||
|
||||
<header class="prose my-6 max-w-full">
|
||||
<h1>Articles</h1>
|
||||
</header>
|
||||
|
||||
<!-- <form action={$page.url.pathname} class="mx-auto max-w-sm" method="get">
|
||||
<div class="form-control">
|
||||
<div class="input-group">
|
||||
<input
|
||||
class="input input-bordered"
|
||||
name="q"
|
||||
type="search"
|
||||
bind:value={q}
|
||||
/>
|
||||
<button class="btn btn-square" type="submit">
|
||||
<Icon icon={searchIcon} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Législature</span>
|
||||
<select
|
||||
class="select select-bordered"
|
||||
name="legislature"
|
||||
bind:value={legislature}
|
||||
>
|
||||
<option value="">Toutes</option>
|
||||
{#each Object.entries(Legislature) as [key, value]}
|
||||
{#if value !== "*"}
|
||||
<option {value}>{key}</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</form> -->
|
||||
|
||||
{#if error != null}
|
||||
<ErrorAlert {error} />
|
||||
{/if}
|
||||
|
||||
{#if error == null}
|
||||
<TreeView
|
||||
access={{ key: "articles" }}
|
||||
frame={false}
|
||||
open
|
||||
summarize={summarizeArticleProperties}
|
||||
value={articles}
|
||||
/>
|
||||
|
||||
<Pagination currentPageCount={articles.length ?? 0} />
|
||||
{/if}
|
68
src/routes/articles/index.ts
Normal file
68
src/routes/articles/index.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core"
|
||||
import type { RequestHandler } from "@sveltejs/kit"
|
||||
import type { JSONObject } from "@sveltejs/kit/types/private"
|
||||
|
||||
import { auditSearchQueryContent } from "$lib/auditors/queries"
|
||||
import type { Article } from "$lib/data"
|
||||
import { db } from "$lib/server/database"
|
||||
|
||||
export function auditQuery(
|
||||
audit: Audit,
|
||||
query: URLSearchParams,
|
||||
): [unknown, unknown] {
|
||||
if (query == null) {
|
||||
return [query, null]
|
||||
}
|
||||
if (!(query instanceof URLSearchParams)) {
|
||||
return audit.unexpectedType(query, "URLSearchParams")
|
||||
}
|
||||
|
||||
const data: { [key: string]: unknown } = {}
|
||||
for (const [key, value] of query.entries()) {
|
||||
let values = data[key] as string[] | undefined
|
||||
if (values === undefined) {
|
||||
values = data[key] = []
|
||||
}
|
||||
values.push(value)
|
||||
}
|
||||
const errors: { [key: string]: unknown } = {}
|
||||
const remainingKeys = new Set(Object.keys(data))
|
||||
|
||||
auditSearchQueryContent(audit, data, errors, remainingKeys)
|
||||
|
||||
return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({}))
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [
|
||||
{ limit: number; offset: number; q?: string },
|
||||
unknown,
|
||||
]
|
||||
if (queryError !== null) {
|
||||
console.error(
|
||||
`Error in ${url.pathname} query:\n${JSON.stringify(
|
||||
query,
|
||||
null,
|
||||
2,
|
||||
)}\n\nError:\n${JSON.stringify(queryError, null, 2)}`,
|
||||
)
|
||||
return {
|
||||
// status: 400,
|
||||
body: {
|
||||
error: {
|
||||
query: queryError as unknown as JSONObject,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
const { limit, offset } = query
|
||||
|
||||
const articles = (
|
||||
await db<{ data: Article }[]>`
|
||||
SELECT data FROM articles
|
||||
OFFSET ${offset}
|
||||
LIMIT ${limit}
|
||||
`
|
||||
).map(({ data }) => data)
|
||||
return { body: { articles: articles as unknown as JSONObject[] } }
|
||||
}
|
|
@ -1,4 +1,33 @@
|
|||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>
|
||||
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
|
||||
</p>
|
||||
<script lang="ts">
|
||||
import { session } from "$app/stores"
|
||||
|
||||
const title = $session.title
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
<div class="hero bg-base-200">
|
||||
<div class="hero-content flex-col lg:flex-row">
|
||||
<img
|
||||
alt="Les Tricoteuses jacobines ou de Robespierre"
|
||||
class="max-w-sm rounded-lg shadow-2xl"
|
||||
src="/tricoteuses_jacobines_1301x1907.jpg"
|
||||
/>
|
||||
<div>
|
||||
<h1 class="text-5xl font-bold">Tricoteuses</h1>
|
||||
<p class="py-6">
|
||||
Un site et des API pour permettre aux développeurs d'utiliser plus
|
||||
facilement les données publiques légales de la <abbr
|
||||
title="Direction de l'information légale et administrative"
|
||||
>DILA</abbr
|
||||
>
|
||||
diffusées sur <a href="https://www.legifrance.gouv.fr/">Légifrance</a>
|
||||
</p>
|
||||
<a class="btn btn-primary" href="/recherche">Commencer</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
|
54
src/routes/recherche/index.svelte
Normal file
54
src/routes/recherche/index.svelte
Normal file
|
@ -0,0 +1,54 @@
|
|||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte"
|
||||
import searchIcon from "@iconify-icons/codicon/search"
|
||||
import { TreeView } from "augmented-data-viewer"
|
||||
|
||||
import { page } from "$app/stores"
|
||||
import ErrorAlert from "$lib/components/errors/ErrorAlert.svelte"
|
||||
import Pagination from "$lib/components/Pagination.svelte"
|
||||
import { summarizeArticleProperties } from "$lib/summaries"
|
||||
|
||||
export let articles: unknown[]
|
||||
export let error: unknown
|
||||
export let q: string | undefined
|
||||
</script>
|
||||
|
||||
<header class="prose my-6 max-w-full">
|
||||
<h1>Recherche</h1>
|
||||
</header>
|
||||
|
||||
<form action={$page.url.pathname} class="mx-auto max-w-sm" method="get">
|
||||
<div class="form-control">
|
||||
<div class="input-group">
|
||||
<input
|
||||
class="input input-bordered"
|
||||
name="q"
|
||||
type="search"
|
||||
bind:value={q}
|
||||
/>
|
||||
<button class="btn btn-square" type="submit">
|
||||
<Icon icon={searchIcon} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{#if error != null}
|
||||
<ErrorAlert {error} />
|
||||
{/if}
|
||||
|
||||
{#if error == null && q !== undefined}
|
||||
{#if articles === undefined}
|
||||
<p>Aucun article trouvé</p>
|
||||
{:else}
|
||||
<TreeView
|
||||
access={{ key: "articles" }}
|
||||
frame={false}
|
||||
open
|
||||
summarize={summarizeArticleProperties}
|
||||
value={articles}
|
||||
/>
|
||||
|
||||
<Pagination currentPageCount={articles.length ?? 0} />
|
||||
{/if}
|
||||
{/if}
|
79
src/routes/recherche/index.ts
Normal file
79
src/routes/recherche/index.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core"
|
||||
import type { RequestHandler } from "@sveltejs/kit"
|
||||
import type { JSONObject } from "@sveltejs/kit/types/private"
|
||||
|
||||
import { auditSearchQueryContent } from "$lib/auditors/queries"
|
||||
import type { Article } from "$lib/data"
|
||||
import { db } from "$lib/server/database"
|
||||
|
||||
export function auditQuery(
|
||||
audit: Audit,
|
||||
query: URLSearchParams,
|
||||
): [unknown, unknown] {
|
||||
if (query == null) {
|
||||
return [query, null]
|
||||
}
|
||||
if (!(query instanceof URLSearchParams)) {
|
||||
return audit.unexpectedType(query, "URLSearchParams")
|
||||
}
|
||||
|
||||
const data: { [key: string]: unknown } = {}
|
||||
for (const [key, value] of query.entries()) {
|
||||
let values = data[key] as string[] | undefined
|
||||
if (values === undefined) {
|
||||
values = data[key] = []
|
||||
}
|
||||
values.push(value)
|
||||
}
|
||||
const errors: { [key: string]: unknown } = {}
|
||||
const remainingKeys = new Set(Object.keys(data))
|
||||
|
||||
auditSearchQueryContent(audit, data, errors, remainingKeys)
|
||||
|
||||
return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({}))
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [
|
||||
{ limit: number; offset: number; q?: string },
|
||||
unknown,
|
||||
]
|
||||
if (queryError !== null) {
|
||||
console.error(
|
||||
`Error in ${url.pathname} query:\n${JSON.stringify(
|
||||
query,
|
||||
null,
|
||||
2,
|
||||
)}\n\nError:\n${JSON.stringify(queryError, null, 2)}`,
|
||||
)
|
||||
return {
|
||||
// status: 400,
|
||||
body: {
|
||||
error: {
|
||||
query: queryError as unknown as JSONObject,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
const { limit, offset, q } = query
|
||||
|
||||
if (q !== undefined) {
|
||||
// https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000006308296/
|
||||
// https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000006308296/1983-12-30/
|
||||
// https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000006308296/1984-12-30/
|
||||
// https://www.legifrance.gouv.fr/loda/article_lc/LEGIARTI000036456533
|
||||
// https://www.legifrance.gouv.fr/loda/article_lc/LEGIARTI000036456533/2018-01-01
|
||||
// https://www.legifrance.gouv.fr/loda/id/LEGIARTI000006317314/1983-12-30
|
||||
const articleId = q.match(/LEGIARTI\d+/)?.[0]
|
||||
if (articleId != null) {
|
||||
const articles = (
|
||||
await db<{ data: Article }[]>`
|
||||
SELECT data FROM articles
|
||||
WHERE id = ${articleId}
|
||||
`
|
||||
).map(({ data }) => data)
|
||||
return { body: { articles: articles as unknown as JSONObject[], q } }
|
||||
}
|
||||
}
|
||||
return { body: {} }
|
||||
}
|
BIN
static/tricoteuses_jacobines_1301x1907.jpg
Normal file
BIN
static/tricoteuses_jacobines_1301x1907.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1,009 KiB |
|
@ -2,7 +2,10 @@ const daisyui = require("daisyui")
|
|||
const typography = require("@tailwindcss/typography")
|
||||
|
||||
const config = {
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
content: [
|
||||
"./src/**/*.{html,js,svelte,ts}",
|
||||
"./node_modules/augmented-data-viewer/**/*.{html,js,svelte,ts}",
|
||||
],
|
||||
|
||||
theme: {
|
||||
extend: {},
|
||||
|
|
Loading…
Add table
Reference in a new issue