diff --git a/package-lock.json b/package-lock.json index 2206dbf7b0ec938717b97926f1cc4af5512f08a3..1b18042d48472e1ec4f4d72e7bbb8ef684a920c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,8 @@ "@sveltejs/adapter-node": "^1.0.0-next.85", "@sveltejs/kit": "^1.0.0-next.428", "@tailwindcss/typography": "^0.5.4", + "@tricoteuses/explorer-tools": "^0.1.12", + "@tricoteuses/legal-explorer": "^0.0.1", "@types/cookie": "^0.5.0", "@types/fs-extra": "^9.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -1960,6 +1962,15 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@iconify-icons/codicon": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@iconify-icons/codicon/-/codicon-1.2.16.tgz", + "integrity": "sha512-85rBsFEhhq2qSBfIEF0hzUk31i4GjeRzNyd0DZGFWo5v+PgAeTBiv8ftsTDf8d2fxy9F5kesT/R7bOtRy1xKmw==", + "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", @@ -1969,6 +1980,12 @@ "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/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -2195,6 +2212,23 @@ "tailwindcss": ">=3.0.0 || insiders" } }, + "node_modules/@tricoteuses/explorer-tools": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@tricoteuses/explorer-tools/-/explorer-tools-0.1.12.tgz", + "integrity": "sha512-Aachs9YOz1R624H3gl1WuMkdbqhBPmjKIogfO3iukjVH4MBZ7g1jN2a8q4mRj0c9v82fwiKKBdcCz0FLi4S8Cw==", + "dev": true, + "dependencies": { + "@iconify-icons/codicon": "^1.2.15", + "@iconify/svelte": "^2.2.1", + "augmented-data-viewer": "^0.1.5" + } + }, + "node_modules/@tricoteuses/legal-explorer": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@tricoteuses/legal-explorer/-/legal-explorer-0.0.1.tgz", + "integrity": "sha512-EILVjFAlQBff4ysltQqwSUlwUKtVzomfyClITNMMYRGEdrJBthL9zqsGLJexK5D4WEbFv+ZLpRKgq5GEpdKz6g==", + "dev": true + }, "node_modules/@types/cookie": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz", @@ -2563,6 +2597,16 @@ "node": ">=8" } }, + "node_modules/augmented-data-viewer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/augmented-data-viewer/-/augmented-data-viewer-0.1.5.tgz", + "integrity": "sha512-LuzvtNsO3rxfl9PbASCJiptr7svRyUhaoHi2PPcD+OqGqgrMd7kuEbeNDAtl7PC/cU2eIQPvjPnN9QO3lLlU5w==", + "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", @@ -7935,12 +7979,27 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@iconify-icons/codicon": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@iconify-icons/codicon/-/codicon-1.2.16.tgz", + "integrity": "sha512-85rBsFEhhq2qSBfIEF0hzUk31i4GjeRzNyd0DZGFWo5v+PgAeTBiv8ftsTDf8d2fxy9F5kesT/R7bOtRy1xKmw==", + "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/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -8101,6 +8160,23 @@ "lodash.merge": "^4.6.2" } }, + "@tricoteuses/explorer-tools": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@tricoteuses/explorer-tools/-/explorer-tools-0.1.12.tgz", + "integrity": "sha512-Aachs9YOz1R624H3gl1WuMkdbqhBPmjKIogfO3iukjVH4MBZ7g1jN2a8q4mRj0c9v82fwiKKBdcCz0FLi4S8Cw==", + "dev": true, + "requires": { + "@iconify-icons/codicon": "^1.2.15", + "@iconify/svelte": "^2.2.1", + "augmented-data-viewer": "^0.1.5" + } + }, + "@tricoteuses/legal-explorer": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@tricoteuses/legal-explorer/-/legal-explorer-0.0.1.tgz", + "integrity": "sha512-EILVjFAlQBff4ysltQqwSUlwUKtVzomfyClITNMMYRGEdrJBthL9zqsGLJexK5D4WEbFv+ZLpRKgq5GEpdKz6g==", + "dev": true + }, "@types/cookie": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz", @@ -8349,6 +8425,16 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "augmented-data-viewer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/augmented-data-viewer/-/augmented-data-viewer-0.1.5.tgz", + "integrity": "sha512-LuzvtNsO3rxfl9PbASCJiptr7svRyUhaoHi2PPcD+OqGqgrMd7kuEbeNDAtl7PC/cU2eIQPvjPnN9QO3lLlU5w==", + "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", diff --git a/package.json b/package.json index e82791a0a5a34d0403147de4d533f1ff82891d1c..f47f7ac3aa8b5207816a730d691c753dd676e28c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@sveltejs/adapter-node": "^1.0.0-next.85", "@sveltejs/kit": "^1.0.0-next.428", "@tailwindcss/typography": "^0.5.4", + "@tricoteuses/explorer-tools": "^0.1.12", + "@tricoteuses/legal-explorer": "^0.0.1", "@types/cookie": "^0.5.0", "@types/fs-extra": "^9.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", diff --git a/src/lib/components/legifrance/ArticleView.svelte b/src/lib/components/legifrance/ArticleView.svelte new file mode 100644 index 0000000000000000000000000000000000000000..dd69f0edd246b4fea890b4db1075ff3188c67794 --- /dev/null +++ b/src/lib/components/legifrance/ArticleView.svelte @@ -0,0 +1,92 @@ +<script lang="ts"> + import Icon from "@iconify/svelte" + import { + firstValueOfArrayOrSingleton, + iterArrayOrSingleton, + } from "@tricoteuses/explorer-tools" + import type { + Article, + LegalObject, + LegalObjectType, + } from "@tricoteuses/legal-explorer" + + import LienView from "$lib/components/legifrance/LienView.svelte" + + export let article: Article + export let level = 1 + + const dateFormatter = new Intl.DateTimeFormat("fr-FR", { + dateStyle: "medium", + }) + + $: metaArticle = article.META.META_SPEC.META_ARTICLE + + $: legifranceUrl = legifranceUrlFromLegalObject("article", article) + + $: liens = [...iterArrayOrSingleton(article.LIENS?.LIEN)] + $: ciblesCreation = liens.filter( + (lien) => lien["@sens"] === "cible" && lien["@typelien"] === "CREATION", + ) + $: titreTexte = firstValueOfArrayOrSingleton( + article.CONTEXTE.TEXTE.TITRE_TXT, + )?.["#text"] + + export function legifranceUrlFromLegalObject( + type: LegalObjectType, + object: LegalObject, + ): string | undefined { + switch (type) { + case "article": + return `https://www.legifrance.gouv.fr/codes/article_lc/${ + (object as Article).META.META_COMMUN.ID + }` + default: + console.warn( + `ArticleView.legifranceUrlFromLegalObject: TODO Handle legal object type: ${type}`, + ) + return undefined + } + } +</script> + +<h4 class="mb-4 font-serif text-2xl italic text-gray-700 md:text-3xl"> + <Icon + class="mr-1 inline-flex h-7 w-7 place-self-center text-le-gris-dispositif-light" + icon="ri-map-pin-2-fill" + /> + <span class="font-bold">Article {metaArticle.NUM}</span> + - {titreTexte} +</h4> + +<div class="prose"> + {@html article.BLOC_TEXTUEL.CONTENU} +</div> + +{#if ciblesCreation.length > 0} + <ul> + {#each liens as lien} + <li> + <LienView level={level + 1} {lien} /> + </li> + {/each} + </ul> +{/if} + +<div class="mt-4 text-right text-sm text-gray-500"> + {#if metaArticle.DATE_DEBUT !== "2999-01-01"} + <p> + Article en vigueur {#if metaArticle.DATE_FIN === "2999-01-01"}depuis le {dateFormatter.format( + new Date(metaArticle.DATE_DEBUT), + )}{:else}du {dateFormatter.format(new Date(metaArticle.DATE_DEBUT))} au {dateFormatter.format( + new Date(metaArticle.DATE_FIN), + )}{/if} + </p> + {/if} + {#if legifranceUrl !== undefined} + <span class="text-sm text-gray-500 md:text-base"> + <a class="link" href={legifranceUrl} target="_blank" + >Voir l'article sur Légifrance.fr</a + > + </span> + {/if} +</div> diff --git a/src/lib/components/legifrance/LienView.svelte b/src/lib/components/legifrance/LienView.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b287f65c6a3438623b11de0cfaf989c805195e6a --- /dev/null +++ b/src/lib/components/legifrance/LienView.svelte @@ -0,0 +1,101 @@ +<script lang="ts"> + import Icon from "@iconify/svelte" + import { + type Aggregate, + type Article, + type LegalObject, + type LegalObjectType, + type Lien, + pathnameFromLegalId, + rootTypeFromLegalId, + } from "@tricoteuses/legal-explorer" + + import { page } from "$app/stores" + import ArticleView from "$lib/components/legifrance/ArticleView.svelte" + + export let level = 1 + export let lien: Lien + + let open = false + + $: id = lien["@id"] + $: rootType = rootTypeFromLegalId(id) + $: componentAndPropertiesPromise = + componentAndPropertiesFromTypeAndTarget(rootType) + + async function componentAndPropertiesFromTypeAndTarget( + type: LegalObjectType | undefined, + ) { + if (type === undefined) { + return undefined + } + switch (type) { + case "article": + const article = await retrieveLegifranceArticle(id) + if (article === undefined) { + return undefined + } + return { + component: ArticleView, + properties: { article, level }, + } + default: + console.log( + `LienView.componentAndPropertiesFromTypeAndTarget: Document type ${type} not handled yet`, + ) + return undefined + } + } + + async function retrieveLegifranceArticle( + id: string, + ): Promise<Article | undefined> { + const response = await fetch( + new URL(`api/articles/${encodeURIComponent(id)}`, $page.data.legalUrl), + { headers: { Accept: "application/json" } }, + ) + if (!response.ok) { + console.error( + `LienView: Error retrieving article ${id}:\n${response.status} ${response.statusText}`, + ) + console.error(await response.text()) + return undefined + } + const data = (await response.json()) as Aggregate + return data.id === undefined ? undefined : data.article?.[data.id] + } + + function toggle() { + open = !open + } +</script> + +{#if componentAndPropertiesPromise === undefined} + <div class="inline-flex align-top" on:click|stopPropagation={toggle}> + <Icon class="mt-1 inline-block shrink-0" icon="codicon:dash" inline /> + <a class="link link-hover link-primary" href={pathnameFromLegalId(id)}> + {lien["#text"]} + </a> + </div> +{:else} + <div + class="inline-flex cursor-pointer align-top" + on:click|stopPropagation={toggle} + > + <Icon + class="mt-1 inline-block shrink-0" + icon={open ? "codicon:triangle-down" : "codicon:triangle-right"} + inline + /> + {lien["#text"]} + </div> + {#if open} + {#await componentAndPropertiesPromise} + <p>Récupération du lien Légifrance en cours…</p> + {:then { component, properties }} + <div class="ml-2 border-l-4 pl-2"> + <svelte:component this={component} {...properties} /> + </div> + {/await} + {/if} +{/if} diff --git a/src/lib/components/parameters/ArticleModal.svelte b/src/lib/components/parameters/ArticleModal.svelte index cbc067687c171069f85bb37c57f5713e38e81864..554425a0f034bd398f6e7e89f1caa4aa5cdb84fc 100644 --- a/src/lib/components/parameters/ArticleModal.svelte +++ b/src/lib/components/parameters/ArticleModal.svelte @@ -15,30 +15,27 @@ } from "@rgossiaux/svelte-headlessui" import { page } from "$app/stores" + import ArticleView from "$lib/components/legifrance/ArticleView.svelte" export let billLegalReferences: Reference[] | undefined export let billParameter: ValueParameter | ScaleParameter export let isOpen = false - const dateFormatter = new Intl.DateTimeFormat("fr-FR", { - dateStyle: "medium", - }) - $: firstLegalReference = billLegalReferences?.[0] $: articlePromise = !isOpen || firstLegalReference === undefined ? undefined - : retrieveLegalArticle(firstLegalReference.href) + : retrieveLegifranceArticle(firstLegalReference.href) function closeModal() { isOpen = false } - async function retrieveLegalArticle(url: string) { + async function retrieveLegifranceArticle(url: string) { const response = await fetch( new URL( - `api/recherche?q=${decodeURIComponent(url)}`, + `api/recherche?q=${encodeURIComponent(url)}`, $page.data.legalUrl, ), { headers: { Accept: "application/json" } }, @@ -921,47 +918,7 @@ Récupération de l'article légal ou règlementaire en cours… </p> {:then article} - {@const metaArticle = article.META.META_SPEC.META_ARTICLE} - {@const titreTxt = article.CONTEXTE.TEXTE.TITRE_TXT} - {@const titreTexte = ( - Array.isArray(titreTxt) ? titreTxt[0] : titreTxt - )["#text"]} - <h4 - class="mb-4 font-serif text-2xl italic text-gray-700 md:text-3xl" - > - <Icon - class="mr-1 inline-flex h-7 w-7 place-self-center text-le-gris-dispositif-light" - icon="ri-map-pin-2-fill" - /> - <span class="font-bold">Article {metaArticle.NUM}</span> - - {titreTexte} - </h4> - - <div class="prose"> - {@html article.BLOC_TEXTUEL.CONTENU} - </div> - - <div class="mt-4 text-right text-sm text-gray-500"> - {#if metaArticle.DATE_DEBUT !== "2999-01-01"} - <p> - Article en vigueur {#if metaArticle.DATE_FIN === "2999-01-01"}depuis - le {dateFormatter.format( - new Date(metaArticle.DATE_DEBUT), - )}{:else}du {dateFormatter.format( - new Date(metaArticle.DATE_DEBUT), - )} au {dateFormatter.format( - new Date(metaArticle.DATE_FIN), - )}{/if} - </p> - {/if} - <span class="text-sm text-gray-500 md:text-base"> - <a - class="link" - href={firstLegalReference.href} - target="_blank">Voir l'article sur Légifrance.fr</a - > - </span> - </div> + <ArticleView {article} /> {/await} {:else} <p>Aucune référence légale ou règlementaire trouvée</p> diff --git a/src/lib/users.ts b/src/lib/users.ts new file mode 100644 index 0000000000000000000000000000000000000000..19b723dc090dc704a879a7164dbce2c061d9638f --- /dev/null +++ b/src/lib/users.ts @@ -0,0 +1,12 @@ +export interface User { + email: string // "john@doe.com" + email_verified: boolean + family_name: string // "Doe" + given_name: string // "John" + last_name: string // "Doe" + locale: string // "fr" + name: string // "John Doe" + preferred_username: string // "jdoe", + roles?: string[] // [ 'offline_access', 'default-roles-leximpact', 'uma_authorization' ], + sub: string // "12345678-9abc-def0-1234-56789abcdef0" +}