@datagouv/components-next 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Control-b-43UT2B.js → Control-B9WQxlpJ.js} +10 -10
- package/dist/{Datafair.client-BrjISlaZ.js → Datafair.client-D_6yzJUG.js} +9 -9
- package/dist/{Event-py_AAwJY.js → Event-iXDrp-fy.js} +15 -15
- package/dist/{Image-BHQYZWzH.js → Image-mNMBOofs.js} +22 -22
- package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
- package/dist/{JsonPreview.client-DivFAnbN.js → JsonPreview.client-DSl1kiwC.js} +2 -2
- package/dist/{Map-CtP7leHl.js → Map-BfqKMw0S.js} +165 -165
- package/dist/{MapContainer.client-Cq8tNLZZ.js → MapContainer.client-BSfpk_rT.js} +17 -17
- package/dist/{OSM-gdDSIsDA.js → OSM-CcrUhItr.js} +1 -1
- package/dist/{PdfPreview.client-BNBgLsK2.js → PdfPreview.client-BANFeM6K.js} +438 -427
- package/dist/{Pmtiles.client-CQi5Rw8W.js → Pmtiles.client-DH9ZrsZR.js} +6177 -5967
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
- package/dist/{ScaleLine-hG-FhNqy.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-D6rFonpT.js → Tile-CSpn0Ca8.js} +47 -47
- package/dist/{TileImage-cfrEgz5y.js → TileImage-CpbaEFwd.js} +44 -44
- package/dist/{View-vp7vDb9q.js → View-BYBpt-AU.js} +58 -58
- package/dist/{XmlPreview.client-XCsNvnsZ.js → XmlPreview.client-KCB5mwVf.js} +3 -3
- package/dist/{common-C8efyyuc.js → common-BoeTUnDZ.js} +58 -58
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +199 -172
- package/dist/components.css +1 -1
- package/dist/{index-CXUamc8Z.js → index-DHWzX5vK.js} +4668 -4466
- package/dist/{leaflet-src-BRKv9Ksv.js → leaflet-src-pVQfonBI.js} +1 -1
- package/dist/{main-DKu5SOAl.js → main-BDcYkhOc.js} +41684 -33432
- package/dist/{proj-CsjVeojN.js → proj-DxGbobp7.js} +110 -110
- package/dist/{tilecoord-CPcQJnrF.js → tilecoord-DvX7WWiA.js} +94 -94
- package/dist/{vue3-xml-viewer.common-BIaEIFYG.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
- package/package.json +28 -27
- package/src/components/ActivityList/ActivityList.vue +21 -12
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +111 -17
- package/src/components/Chart/ChartViewerWrapper.vue +28 -16
- package/src/components/ChartCard.vue +112 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +23 -6
- package/src/components/DataserviceQuality.vue +82 -0
- package/src/components/DataserviceQualityInline.vue +31 -0
- package/src/components/DataserviceQualityTooltipContent.vue +79 -0
- package/src/components/DatasetCard.vue +17 -6
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
- package/src/components/DatasetQuality.vue +11 -11
- package/src/components/DatasetQualityInline.vue +2 -2
- package/src/components/DatasetQualityTooltipContent.vue +1 -1
- package/src/components/DiscussionMessageCard.vue +14 -15
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +69 -0
- package/src/components/LoadingBlock.vue +4 -1
- package/src/components/ObjectCard.vue +4 -1
- package/src/components/ObjectCardOwner.vue +13 -1
- package/src/components/Pagination.vue +18 -58
- package/src/components/Placeholder.vue +3 -2
- package/src/components/PostCard.vue +13 -15
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +19 -7
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
- package/src/components/ResourceAccordion/Metadata.vue +21 -5
- package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +27 -18
- package/src/components/ResourceAccordion/Preview.vue +8 -4
- package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
- package/src/components/ResourceAccordion/ResourceAccordion.vue +29 -9
- package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -209
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +136 -183
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +18 -6
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +4 -3
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +12 -8
- package/src/components/Search/SearchInput.vue +1 -1
- package/src/components/Search/Sidemenu.vue +4 -3
- package/src/components/SmallChart.vue +11 -10
- package/src/components/StatBox.vue +13 -5
- package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
- package/src/components/TabularExplorer/TabularCell.vue +7 -8
- package/src/components/TabularExplorer/TabularCellPopover.vue +19 -3
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularDateField.vue +41 -0
- package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +122 -913
- package/src/components/TabularExplorer/TabularFilterContent.vue +33 -25
- package/src/components/TabularExplorer/TabularFilterPopover.vue +7 -7
- package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
- package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
- package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
- package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
- package/src/components/TabularExplorer/TabularTable.vue +484 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +32 -20
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +15 -14
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +2 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +47 -39
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +1 -0
- package/src/functions/charts.ts +32 -1
- package/src/functions/dates.ts +26 -7
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +57 -56
- package/src/functions/owned.ts +21 -0
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +83 -0
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +119 -4
- package/src/functions/tabularApi.ts +1 -43
- package/src/main.ts +32 -6
- package/src/types/dataservices.ts +15 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +2 -2
- package/src/types/visualizations.ts +5 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-C3dDzrIr.js +0 -61
- /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
- /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
- /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
class="-mr-0.5 flex-initial truncate"
|
|
5
5
|
>
|
|
6
6
|
<AppLink
|
|
7
|
+
v-if="organizationUrl !== null"
|
|
7
8
|
class="link text-sm overflow-hidden flex items-center relative z-[2] truncate"
|
|
8
9
|
:to="organizationUrl || organization.page"
|
|
9
10
|
>
|
|
@@ -12,6 +13,16 @@
|
|
|
12
13
|
size="sm"
|
|
13
14
|
/>
|
|
14
15
|
</AppLink>
|
|
16
|
+
<span
|
|
17
|
+
v-else
|
|
18
|
+
class="text-sm overflow-hidden flex items-center truncate"
|
|
19
|
+
>
|
|
20
|
+
<OrganizationNameWithCertificate
|
|
21
|
+
:organization
|
|
22
|
+
size="sm"
|
|
23
|
+
color-class="text-gray-title"
|
|
24
|
+
/>
|
|
25
|
+
</span>
|
|
15
26
|
</div>
|
|
16
27
|
<div
|
|
17
28
|
v-else-if="ownerName"
|
|
@@ -33,7 +44,8 @@ import OrganizationNameWithCertificate from './OrganizationNameWithCertificate.v
|
|
|
33
44
|
const props = defineProps<{
|
|
34
45
|
organization?: OrganizationReference | null
|
|
35
46
|
owner?: UserReference | null
|
|
36
|
-
|
|
47
|
+
// undefined falls back to organization.page; pass null to render plain, non-linked text
|
|
48
|
+
organizationUrl?: RouteLocationRaw | null
|
|
37
49
|
}>()
|
|
38
50
|
|
|
39
51
|
const ownerName = computed(() => {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
</li>
|
|
20
20
|
<li>
|
|
21
21
|
<a
|
|
22
|
-
:href="getHref(
|
|
22
|
+
:href="getHref(currentPage - 1)"
|
|
23
23
|
class="fr-pagination__link fr-pagination__link--prev fr-pagination__link--lg-label"
|
|
24
24
|
data-testid="previous-page"
|
|
25
25
|
@click.prevent.stop="previousPage"
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
</li>
|
|
30
30
|
<li>
|
|
31
31
|
<a
|
|
32
|
-
:aria-current="
|
|
32
|
+
:aria-current="currentPage === 1 ? 'page' : undefined"
|
|
33
33
|
:href="getHref(1)"
|
|
34
34
|
class="fr-pagination__link"
|
|
35
|
-
:class="{ 'fr-hidden fr-unhidden-sm':
|
|
35
|
+
:class="{ 'fr-hidden fr-unhidden-sm': currentPage > 1 }"
|
|
36
36
|
:title="t('Page {nb}', { nb: 1 })"
|
|
37
37
|
:data-testid="1"
|
|
38
38
|
@click.prevent.stop="onClick(1)"
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
<a
|
|
48
48
|
v-if="index"
|
|
49
49
|
class="fr-pagination__link"
|
|
50
|
-
:class="{ 'fr-hidden fr-unhidden-lg': index <
|
|
51
|
-
:aria-current="
|
|
50
|
+
:class="{ 'fr-hidden fr-unhidden-lg': index < currentPage - 1 || index > currentPage + 1 }"
|
|
51
|
+
:aria-current="currentPage === index ? 'page' : undefined"
|
|
52
52
|
:href="getHref(index)"
|
|
53
53
|
:title="t('Page {nb}', { nb: index })"
|
|
54
54
|
:data-testid="index"
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
<li>
|
|
67
67
|
<a
|
|
68
68
|
class="fr-pagination__link"
|
|
69
|
-
:aria-current="
|
|
69
|
+
:aria-current="currentPage === pageCount ? 'page' : undefined"
|
|
70
70
|
:href="getHref(pageCount)"
|
|
71
71
|
:title="t('Page {nb}', { nb: pageCount })"
|
|
72
72
|
:data-testid="pageCount"
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
<li>
|
|
79
79
|
<a
|
|
80
80
|
class="fr-pagination__link fr-pagination__link--next fr-pagination__link--lg-label"
|
|
81
|
-
:href="getHref(
|
|
81
|
+
:href="getHref(currentPage + 1)"
|
|
82
82
|
data-testid="next-page"
|
|
83
83
|
@click.prevent.stop="nextPage"
|
|
84
84
|
>
|
|
@@ -103,6 +103,8 @@
|
|
|
103
103
|
import { computed, useTemplateRef } from 'vue'
|
|
104
104
|
import { useRoute } from 'vue-router'
|
|
105
105
|
import { useTranslation } from '../composables/useTranslation'
|
|
106
|
+
import { clampPage, getVisiblePages } from '../functions/paginate'
|
|
107
|
+
import { scrollToBlockTop } from '../functions/scroll'
|
|
106
108
|
|
|
107
109
|
type Props = {
|
|
108
110
|
/**
|
|
@@ -128,78 +130,36 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
128
130
|
pageSize: 20,
|
|
129
131
|
})
|
|
130
132
|
|
|
131
|
-
const PAGES_AROUND = 3
|
|
132
|
-
|
|
133
|
-
function range(size: number, startAt = 1) {
|
|
134
|
-
return [...Array(size).keys()].map(i => i + startAt)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getPages(pageCount: number) {
|
|
138
|
-
return range(pageCount)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function getPagesShown(pages: { length: number }, currentPage: number) {
|
|
142
|
-
return Math.min(
|
|
143
|
-
PAGES_AROUND * 2 + 1,
|
|
144
|
-
pages.length - 2,
|
|
145
|
-
PAGES_AROUND + currentPage - 1,
|
|
146
|
-
PAGES_AROUND + pages.length - currentPage,
|
|
147
|
-
)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function getStartPage(currentPage: number) {
|
|
151
|
-
return Math.max(
|
|
152
|
-
currentPage - PAGES_AROUND, // we want to start 3 pages before the current one
|
|
153
|
-
2, // we don't want to start below page 2
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function getVisiblePages(currentPage: number, pageCount: number) {
|
|
158
|
-
const pages = getPages(pageCount)
|
|
159
|
-
const start = getStartPage(currentPage)
|
|
160
|
-
if (pageCount <= 2) {
|
|
161
|
-
return []
|
|
162
|
-
}
|
|
163
|
-
const pagination: Array<number | null> = range(getPagesShown(pages, currentPage), start)
|
|
164
|
-
if (!pagination.includes(2)) {
|
|
165
|
-
pagination.unshift(null)
|
|
166
|
-
}
|
|
167
|
-
if (!pagination.includes(pageCount - 1)) {
|
|
168
|
-
pagination.push(null)
|
|
169
|
-
}
|
|
170
|
-
return pagination
|
|
171
|
-
}
|
|
172
|
-
|
|
173
133
|
const { t } = useTranslation()
|
|
174
134
|
const route = useRoute()
|
|
175
135
|
const pageCount = computed(() => Math.ceil(props.totalResults / props.pageSize))
|
|
176
|
-
|
|
136
|
+
// The `page` prop often comes straight from a query string, so it is normalised
|
|
137
|
+
// once here and every link below builds on an existing page.
|
|
138
|
+
const currentPage = computed(() => clampPage(props.page, pageCount.value))
|
|
139
|
+
const visiblePages = computed(() => getVisiblePages(currentPage.value, pageCount.value))
|
|
177
140
|
|
|
178
141
|
const nav = useTemplateRef('navRef')
|
|
179
142
|
function change(index: number) {
|
|
180
143
|
emit('change', index)
|
|
181
144
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
nav.value.parentElement.style.scrollMarginTop = '100px'
|
|
185
|
-
nav.value.parentElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
145
|
+
scrollToBlockTop(nav.value?.parentElement)
|
|
186
146
|
}
|
|
187
147
|
|
|
188
148
|
function onClick(index: number) {
|
|
189
|
-
if (index !==
|
|
149
|
+
if (index !== currentPage.value) {
|
|
190
150
|
change(index)
|
|
191
151
|
}
|
|
192
152
|
}
|
|
193
153
|
|
|
194
154
|
function nextPage() {
|
|
195
|
-
const index =
|
|
155
|
+
const index = currentPage.value + 1
|
|
196
156
|
if (index <= pageCount.value) {
|
|
197
157
|
change(index)
|
|
198
158
|
}
|
|
199
159
|
}
|
|
200
160
|
|
|
201
161
|
function previousPage() {
|
|
202
|
-
const index =
|
|
162
|
+
const index = currentPage.value - 1
|
|
203
163
|
if (index > 0) {
|
|
204
164
|
change(index)
|
|
205
165
|
}
|
|
@@ -209,7 +169,7 @@ function getHref(forPage: number) {
|
|
|
209
169
|
if (forPage < 1 || forPage > pageCount.value) {
|
|
210
170
|
return undefined
|
|
211
171
|
}
|
|
212
|
-
if (
|
|
172
|
+
if (currentPage.value === forPage) {
|
|
213
173
|
return undefined
|
|
214
174
|
}
|
|
215
175
|
const search = new URLSearchParams(route.query as Record<string, string>)
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
<script setup lang="ts">
|
|
12
12
|
import { computed } from 'vue'
|
|
13
|
-
import { RiBookShelfLine, RiBuilding2Line, RiDatabase2Line, RiLineChartLine, RiTerminalLine } from '@remixicon/vue'
|
|
13
|
+
import { RiBarChartBoxLine, RiBookShelfLine, RiBuilding2Line, RiDatabase2Line, RiLineChartLine, RiTerminalLine } from '@remixicon/vue'
|
|
14
14
|
|
|
15
15
|
const props = defineProps<{
|
|
16
|
-
type: 'Dataset' | 'Dataservice' | 'Reuse' | 'Organization' | 'Topic'
|
|
16
|
+
type: 'Dataset' | 'Dataservice' | 'Reuse' | 'Organization' | 'Topic' | 'Chart'
|
|
17
17
|
}>()
|
|
18
18
|
|
|
19
19
|
const icon = computed(() => {
|
|
@@ -23,6 +23,7 @@ const icon = computed(() => {
|
|
|
23
23
|
Reuse: RiLineChartLine,
|
|
24
24
|
Organization: RiBuilding2Line,
|
|
25
25
|
Topic: RiBookShelfLine,
|
|
26
|
+
Chart: RiBarChartBoxLine,
|
|
26
27
|
}[props.type]
|
|
27
28
|
})
|
|
28
29
|
</script>
|
|
@@ -23,12 +23,20 @@
|
|
|
23
23
|
|
|
24
24
|
<ObjectCardShortDescription :text="post.headline || post.content" />
|
|
25
25
|
|
|
26
|
-
<
|
|
26
|
+
<TranslationT
|
|
27
27
|
v-if="post.published || post.created_at"
|
|
28
|
+
tag="div"
|
|
28
29
|
class="text-sm text-gray-medium mt-1"
|
|
30
|
+
keypath="Publié {date}"
|
|
29
31
|
>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
<template #date>
|
|
33
|
+
<FormattedDate
|
|
34
|
+
:date="post.published || post.created_at"
|
|
35
|
+
format="relative"
|
|
36
|
+
:options="{ dateStyle: 'long', timeStyle: 'short' }"
|
|
37
|
+
/>
|
|
38
|
+
</template>
|
|
39
|
+
</TranslationT>
|
|
32
40
|
|
|
33
41
|
<slot />
|
|
34
42
|
</ObjectCard>
|
|
@@ -37,26 +45,16 @@
|
|
|
37
45
|
<script setup lang="ts">
|
|
38
46
|
import { RiArticleLine } from '@remixicon/vue'
|
|
39
47
|
import type { RouteLocationRaw } from 'vue-router'
|
|
40
|
-
import { useFormatDate } from '../functions/dates'
|
|
41
|
-
import { useTranslation } from '../composables/useTranslation'
|
|
42
48
|
import type { Post } from '../types/posts'
|
|
43
49
|
import Placeholder from './Placeholder.vue'
|
|
44
50
|
import ObjectCard from './ObjectCard.vue'
|
|
45
51
|
import ObjectCardHeader from './ObjectCardHeader.vue'
|
|
46
52
|
import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
|
|
53
|
+
import FormattedDate from './FormattedDate.vue'
|
|
54
|
+
import TranslationT from './TranslationT.vue'
|
|
47
55
|
|
|
48
56
|
defineProps<{
|
|
49
57
|
post: Post
|
|
50
58
|
postUrl?: RouteLocationRaw
|
|
51
59
|
}>()
|
|
52
|
-
|
|
53
|
-
const { t } = useTranslation()
|
|
54
|
-
const { formatRelativeIfRecentDate } = useFormatDate()
|
|
55
|
-
|
|
56
|
-
const formatDate = (dateString: string) => {
|
|
57
|
-
return formatRelativeIfRecentDate(dateString, {
|
|
58
|
-
dateStyle: 'long',
|
|
59
|
-
timeStyle: 'short',
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
60
|
</script>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/>
|
|
11
11
|
</div>
|
|
12
12
|
<PreviewUnavailable v-else>
|
|
13
|
-
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le
|
|
13
|
+
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le pour le consulter.") }}
|
|
14
14
|
</PreviewUnavailable>
|
|
15
15
|
</div>
|
|
16
16
|
</template>
|
|
@@ -47,9 +47,21 @@
|
|
|
47
47
|
/>
|
|
48
48
|
</dd>
|
|
49
49
|
<template v-if="generatedFormats.length">
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
<TranslationT
|
|
51
|
+
tag="dt"
|
|
52
|
+
class="font-bold fr-text--sm fr-mb-0"
|
|
53
|
+
keypath="Formats générés automatiquement par {platform} (dernière mise à jour {date})"
|
|
54
|
+
>
|
|
55
|
+
<template #platform>
|
|
56
|
+
{{ config.name }}
|
|
57
|
+
</template>
|
|
58
|
+
<template #date>
|
|
59
|
+
<FormattedDate
|
|
60
|
+
:date="conversionsFinishedAt"
|
|
61
|
+
format="relative"
|
|
62
|
+
/>
|
|
63
|
+
</template>
|
|
64
|
+
</TranslationT>
|
|
53
65
|
<dd
|
|
54
66
|
v-for="generatedFormat in generatedFormats"
|
|
55
67
|
:key="generatedFormat.format"
|
|
@@ -132,7 +144,8 @@ import { filesize } from '../../functions/helpers'
|
|
|
132
144
|
import { getResourceFilesize } from '../../functions/resources'
|
|
133
145
|
import { trackEvent } from '../../functions/matomo'
|
|
134
146
|
import { useComponentsConfig } from '../../config'
|
|
135
|
-
import
|
|
147
|
+
import FormattedDate from '../FormattedDate.vue'
|
|
148
|
+
import TranslationT from '../TranslationT.vue'
|
|
136
149
|
import { useTranslation } from '../../composables/useTranslation'
|
|
137
150
|
import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
|
|
138
151
|
import type { Resource } from '../../types/resources'
|
|
@@ -145,7 +158,6 @@ const props = defineProps<{
|
|
|
145
158
|
|
|
146
159
|
const { t } = useTranslation()
|
|
147
160
|
const config = useComponentsConfig()
|
|
148
|
-
const { formatRelativeIfRecentDate } = useFormatDate()
|
|
149
161
|
|
|
150
162
|
const { generatedFormats, wfsFormats, defaultWfsProjection } = useResourceCapabilities(
|
|
151
163
|
() => props.resource,
|
|
@@ -154,7 +166,7 @@ const { generatedFormats, wfsFormats, defaultWfsProjection } = useResourceCapabi
|
|
|
154
166
|
|
|
155
167
|
const resourceFilesize = computed(() => getResourceFilesize(props.resource))
|
|
156
168
|
|
|
157
|
-
const
|
|
158
|
-
|
|
169
|
+
const conversionsFinishedAt = computed(() =>
|
|
170
|
+
props.resource.extras['analysis:parsing:finished_at'] as string | undefined,
|
|
159
171
|
)
|
|
160
172
|
</script>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="text-xs">
|
|
3
|
+
<PreviewUnavailable v-if="!isSizeAllowed">
|
|
4
|
+
{{ fileSizeBytes
|
|
5
|
+
? t("Le fichier {fileType} est trop volumineux pour être prévisualisé. Téléchargez-le pour le consulter.", { fileType })
|
|
6
|
+
: t("La taille du fichier est inconnue, l'aperçu n'est pas disponible. Téléchargez-le pour le consulter.")
|
|
7
|
+
}}
|
|
8
|
+
</PreviewUnavailable>
|
|
9
|
+
<PreviewUnavailable v-else-if="hasError">
|
|
10
|
+
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le pour le consulter.") }}
|
|
11
|
+
</PreviewUnavailable>
|
|
12
|
+
<img
|
|
13
|
+
v-else
|
|
14
|
+
:src="resource.url"
|
|
15
|
+
alt=""
|
|
16
|
+
class="block w-full h-auto max-w-[800px]"
|
|
17
|
+
@error="hasError = true"
|
|
18
|
+
>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup lang="ts">
|
|
23
|
+
import { computed, ref } from 'vue'
|
|
24
|
+
import PreviewUnavailable from './PreviewUnavailable.vue'
|
|
25
|
+
import type { Resource } from '../../types/resources'
|
|
26
|
+
import { getResourceFilesize } from '../../functions/resources'
|
|
27
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
28
|
+
import { useComponentsConfig } from '../../config'
|
|
29
|
+
|
|
30
|
+
const props = defineProps<{
|
|
31
|
+
resource: Resource
|
|
32
|
+
}>()
|
|
33
|
+
|
|
34
|
+
const { t } = useTranslation()
|
|
35
|
+
const config = useComponentsConfig()
|
|
36
|
+
|
|
37
|
+
const hasError = ref(false)
|
|
38
|
+
|
|
39
|
+
const fileType = computed(() => props.resource.format?.toUpperCase() ?? t('Fichier'))
|
|
40
|
+
const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
|
|
41
|
+
|
|
42
|
+
const isSizeAllowed = computed(() => {
|
|
43
|
+
const size = fileSizeBytes.value
|
|
44
|
+
const max = config.maxImagePreviewByteSize
|
|
45
|
+
if (!size || !max) return false
|
|
46
|
+
return size <= max
|
|
47
|
+
})
|
|
48
|
+
</script>
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
</PreviewUnavailable>
|
|
5
5
|
<div
|
|
6
6
|
v-else
|
|
7
|
-
id="map"
|
|
8
7
|
ref="mapRef"
|
|
8
|
+
class="size-full"
|
|
9
9
|
/>
|
|
10
10
|
</template>
|
|
11
11
|
|
|
@@ -128,10 +128,3 @@ onMounted(() => {
|
|
|
128
128
|
displayMap()
|
|
129
129
|
})
|
|
130
130
|
</script>
|
|
131
|
-
|
|
132
|
-
<style>
|
|
133
|
-
#map {
|
|
134
|
-
width: 100%;
|
|
135
|
-
height: 500px;
|
|
136
|
-
}
|
|
137
|
-
</style>
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
+
import { RiInformationLine } from '@remixicon/vue'
|
|
3
4
|
import type { Resource } from '../../types/resources'
|
|
4
5
|
import CopyButton from '../CopyButton.vue'
|
|
5
6
|
import DescriptionDetails from '../DescriptionDetails.vue'
|
|
6
7
|
import DescriptionTerm from '../DescriptionTerm.vue'
|
|
7
|
-
import
|
|
8
|
+
import FormattedDate from '../FormattedDate.vue'
|
|
8
9
|
import { filesize } from '../../functions/helpers'
|
|
9
10
|
import ExtraAccordion from '../ExtraAccordion.vue'
|
|
11
|
+
import Toggletip from '../Toggletip.vue'
|
|
10
12
|
import { getResourceTitleId, getResourceLabel, getResourceFilesize } from '../../functions/resources'
|
|
11
13
|
import { useTranslation } from '../../composables/useTranslation'
|
|
12
14
|
|
|
@@ -19,7 +21,6 @@ const resourceTitleId = computed(() => getResourceTitleId(props.resource))
|
|
|
19
21
|
const resourceFilesize = computed(() => getResourceFilesize(props.resource))
|
|
20
22
|
|
|
21
23
|
const { t } = useTranslation()
|
|
22
|
-
const { formatDate } = useFormatDate()
|
|
23
24
|
</script>
|
|
24
25
|
|
|
25
26
|
<template>
|
|
@@ -42,8 +43,23 @@ const { formatDate } = useFormatDate()
|
|
|
42
43
|
</a>
|
|
43
44
|
</code>
|
|
44
45
|
</DescriptionDetails>
|
|
45
|
-
<DescriptionTerm>
|
|
46
|
+
<DescriptionTerm class="flex items-center">
|
|
46
47
|
{{ t('URL stable') }}
|
|
48
|
+
<Toggletip
|
|
49
|
+
:styled-button="false"
|
|
50
|
+
button-class="ml-1 border-transparent -outline-offset-2 inline-flex items-center justify-center rounded-sm p-1 text-gray-medium hover:bg-gray-lower transition-colors"
|
|
51
|
+
>
|
|
52
|
+
<RiInformationLine
|
|
53
|
+
class="size-4"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
/>
|
|
56
|
+
<span class="sr-only">{{ t(`Qu'est-ce qu'une URL stable ?`) }}</span>
|
|
57
|
+
<template #toggletip>
|
|
58
|
+
<p class="fr-text--sm m-0">
|
|
59
|
+
{{ t(`Cette URL redirige toujours vers la dernière version du fichier. L'URL du fichier, elle, change à chaque mise à jour : privilégiez l'URL stable pour partager la ressource ou l'utiliser dans un script.`) }}
|
|
60
|
+
</p>
|
|
61
|
+
</template>
|
|
62
|
+
</Toggletip>
|
|
47
63
|
<CopyButton
|
|
48
64
|
:label="t(`Copier l'URL stable`)"
|
|
49
65
|
:copied-label="t('URL stable copiée !')"
|
|
@@ -94,11 +110,11 @@ const { formatDate } = useFormatDate()
|
|
|
94
110
|
>
|
|
95
111
|
<DescriptionTerm>{{ t('Créée le') }}</DescriptionTerm>
|
|
96
112
|
<DescriptionDetails>
|
|
97
|
-
|
|
113
|
+
<FormattedDate :date="resource.created_at" />
|
|
98
114
|
</DescriptionDetails>
|
|
99
115
|
<DescriptionTerm>{{ t('Modifiée le') }}</DescriptionTerm>
|
|
100
116
|
<DescriptionDetails>
|
|
101
|
-
|
|
117
|
+
<FormattedDate :date="resource.last_modified" />
|
|
102
118
|
</DescriptionDetails>
|
|
103
119
|
</dl>
|
|
104
120
|
<dl
|
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<PreviewWrapper
|
|
3
|
-
v-slot="{ data }"
|
|
4
3
|
file-type="PDF"
|
|
5
4
|
:resource="resource"
|
|
6
5
|
:max-size="config.maxPdfPreviewByteSize"
|
|
7
6
|
:load="load"
|
|
7
|
+
:fill="fill"
|
|
8
8
|
@loaded="renderAllPages"
|
|
9
9
|
>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
<!-- While pdfjs downloads/parses the document, show the A4 skeleton instead of a
|
|
11
|
+
bare "loading" line — it matches the reader framing below seamlessly. -->
|
|
12
|
+
<template #loading>
|
|
13
|
+
<PdfSkeleton :fill="fill" />
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<!-- Cap the page width (A4 never needs more) and center on a light backdrop so
|
|
17
|
+
it reads like a document reader. The width cap applies in both modes since
|
|
18
|
+
the render width follows the container; `fill` only drives the height
|
|
19
|
+
(fullscreen fills down, inline stays capped). The scroll container is a plain
|
|
20
|
+
block (not a flex) so the trailing `pb` is reliably scrollable — flex + overflow
|
|
21
|
+
drops the end padding of flex items. -->
|
|
22
|
+
<template #default="{ data }">
|
|
23
|
+
<div
|
|
24
|
+
class="overflow-y-auto bg-gray-100"
|
|
25
|
+
:class="fill ? 'min-h-0 flex-1' : 'max-h-[80vh]'"
|
|
26
|
+
>
|
|
27
|
+
<div class="flex justify-center p-4 pb-16">
|
|
28
|
+
<div
|
|
29
|
+
ref="containerRef"
|
|
30
|
+
class="w-[800px] max-w-full space-y-3"
|
|
31
|
+
>
|
|
32
|
+
<canvas
|
|
33
|
+
v-for="page in (data as PDFDocumentProxy).numPages"
|
|
34
|
+
:key="page"
|
|
35
|
+
:ref="(el) => setCanvasRef(el as HTMLCanvasElement, page)"
|
|
36
|
+
class="w-full bg-white shadow-sm"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
21
42
|
</PreviewWrapper>
|
|
22
43
|
</template>
|
|
23
44
|
|
|
@@ -27,6 +48,7 @@ import * as pdfjsLib from 'pdfjs-dist'
|
|
|
27
48
|
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
|
|
28
49
|
import type { PDFDocumentProxy } from 'pdfjs-dist'
|
|
29
50
|
import PreviewWrapper from './PreviewWrapper.vue'
|
|
51
|
+
import PdfSkeleton from './PdfSkeleton.vue'
|
|
30
52
|
import { useComponentsConfig } from '../../config'
|
|
31
53
|
import type { Resource } from '../../types/resources'
|
|
32
54
|
|
|
@@ -34,6 +56,8 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
|
|
|
34
56
|
|
|
35
57
|
const props = defineProps<{
|
|
36
58
|
resource: Resource
|
|
59
|
+
// Fill the available height (fullscreen) instead of the default capped height.
|
|
60
|
+
fill?: boolean
|
|
37
61
|
}>()
|
|
38
62
|
|
|
39
63
|
const config = useComponentsConfig()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- An A4 sheet on the reader backdrop with a few pulsing text lines, matching the
|
|
3
|
+
real PdfPreview framing (centered, capped ~800px, gray backdrop). -->
|
|
4
|
+
<div
|
|
5
|
+
class="overflow-hidden bg-gray-100"
|
|
6
|
+
:class="fill ? 'min-h-0 flex-1' : 'max-h-[80vh]'"
|
|
7
|
+
role="status"
|
|
8
|
+
:aria-label="t('Chargement de l\'aperçu…')"
|
|
9
|
+
>
|
|
10
|
+
<div class="flex justify-center p-4">
|
|
11
|
+
<div class="aspect-[210/297] w-[800px] max-w-full bg-white p-10 shadow-sm">
|
|
12
|
+
<div class="animate-pulse-placeholder space-y-6">
|
|
13
|
+
<div class="space-y-3">
|
|
14
|
+
<div class="h-5 w-1/2 rounded bg-gray-200" />
|
|
15
|
+
<div class="h-3 w-full rounded bg-gray-200" />
|
|
16
|
+
<div class="h-3 w-full rounded bg-gray-200" />
|
|
17
|
+
<div class="h-3 w-5/6 rounded bg-gray-200" />
|
|
18
|
+
</div>
|
|
19
|
+
<div class="space-y-3">
|
|
20
|
+
<div class="h-3 w-full rounded bg-gray-200" />
|
|
21
|
+
<div class="h-3 w-11/12 rounded bg-gray-200" />
|
|
22
|
+
<div class="h-3 w-full rounded bg-gray-200" />
|
|
23
|
+
<div class="h-3 w-3/4 rounded bg-gray-200" />
|
|
24
|
+
</div>
|
|
25
|
+
<div class="space-y-3">
|
|
26
|
+
<div class="h-3 w-full rounded bg-gray-200" />
|
|
27
|
+
<div class="h-3 w-5/6 rounded bg-gray-200" />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup lang="ts">
|
|
36
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
37
|
+
|
|
38
|
+
defineProps<{
|
|
39
|
+
// Fill the available height (fullscreen) instead of the default capped height.
|
|
40
|
+
fill?: boolean
|
|
41
|
+
}>()
|
|
42
|
+
|
|
43
|
+
const { t } = useTranslation()
|
|
44
|
+
</script>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="flex h-full flex-col">
|
|
3
3
|
<PreviewUnavailable v-if="hasError">
|
|
4
4
|
{{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}
|
|
5
5
|
</PreviewUnavailable>
|
|
6
6
|
<div
|
|
7
7
|
v-else
|
|
8
|
-
class="-
|
|
8
|
+
class="flex min-h-0 flex-1 flex-col"
|
|
9
9
|
>
|
|
10
10
|
<div
|
|
11
11
|
v-if="pmtilesViewerUrl"
|
|
12
|
-
class="bg-blue-100 text-datagouv fr-hidden fr-unhidden-md p-4"
|
|
12
|
+
class="bg-blue-100 text-datagouv fr-hidden fr-unhidden-md p-4 shrink-0"
|
|
13
13
|
>
|
|
14
14
|
<div class="fr-grid-row fr-grid-row--middle fr-grid-row--gutters">
|
|
15
15
|
<div
|
|
@@ -35,12 +35,17 @@
|
|
|
35
35
|
</p>
|
|
36
36
|
</div>
|
|
37
37
|
</div>
|
|
38
|
-
<div
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
<div class="relative min-h-0 flex-1">
|
|
39
|
+
<div
|
|
40
|
+
ref="containerRef"
|
|
41
|
+
class="size-full"
|
|
42
|
+
/>
|
|
43
|
+
<!-- Freshness caption overlaid inside the map (bottom-left, the corner left
|
|
44
|
+
free by the navigation/geolocate controls top-right and the attribution
|
|
45
|
+
bottom-right) so the map itself can use the whole height. -->
|
|
46
|
+
<div class="pointer-events-none absolute bottom-2 left-2 z-10 rounded bg-white/90 px-2 py-1 text-[12px] leading-4 text-gray-medium shadow-sm">
|
|
47
|
+
{{ t("Aperçu de la carte mis à jour le {date}", { date: lastUpdate }) }}
|
|
48
|
+
</div>
|
|
44
49
|
</div>
|
|
45
50
|
</div>
|
|
46
51
|
</div>
|
|
@@ -62,7 +67,7 @@ import styleVector from '../../../assets/json/vector.json'
|
|
|
62
67
|
import PreviewUnavailable from './PreviewUnavailable.vue'
|
|
63
68
|
import { useTranslation } from '../../composables/useTranslation'
|
|
64
69
|
import franceSvg from './france.svg?raw'
|
|
65
|
-
import {
|
|
70
|
+
import { getMapAttribution } from '../../functions/owned'
|
|
66
71
|
|
|
67
72
|
const props = defineProps<{ resource: Resource, dataset: Dataset | DatasetV2 }>()
|
|
68
73
|
|
|
@@ -89,11 +94,7 @@ const lastUpdate = computed(() => {
|
|
|
89
94
|
|
|
90
95
|
const container = useTemplateRef('containerRef')
|
|
91
96
|
|
|
92
|
-
const attributions = computed(() =>
|
|
93
|
-
if (!props.dataset.organization && !props.dataset.owner)
|
|
94
|
-
return ''
|
|
95
|
-
return `© <a href="${getOwnerPage(props.dataset)}" target="_blank">${getOwnerName(props.dataset)}</a>`
|
|
96
|
-
})
|
|
97
|
+
const attributions = computed(() => getMapAttribution(props.dataset))
|
|
97
98
|
|
|
98
99
|
async function displayMap() {
|
|
99
100
|
await import('maplibre-gl/dist/maplibre-gl.css')
|
|
@@ -104,14 +105,22 @@ async function displayMap() {
|
|
|
104
105
|
const p = new PMTiles(pmtilesUrl.value)
|
|
105
106
|
protocol.add(p)
|
|
106
107
|
|
|
107
|
-
p.getHeader().then((h: {
|
|
108
|
+
p.getHeader().then((h: { minLon: number, minLat: number, maxLon: number, maxLat: number, centerZoom: number, centerLon: number, centerLat: number }) => {
|
|
109
|
+
// Frame the data's bounding box (carried by the archive header) so we land on
|
|
110
|
+
// the data. Fall back to the header center when the bbox is degenerate — an
|
|
111
|
+
// archive with a single feature, or bounds left unset (all zeroes).
|
|
112
|
+
const hasBounds = h.minLon < h.maxLon && h.minLat < h.maxLat
|
|
108
113
|
const map = new maplibregl.Map({
|
|
109
114
|
container: container.value!,
|
|
110
115
|
// JSON import is inferred too loosely (e.g. center as number[]) to match
|
|
111
116
|
// maplibre's strict StyleSpecification, so cast through unknown.
|
|
112
117
|
style: styleVector as unknown as maplibregl.StyleSpecification,
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
...(hasBounds
|
|
119
|
+
? {
|
|
120
|
+
bounds: [[h.minLon, h.minLat], [h.maxLon, h.maxLat]] as [[number, number], [number, number]],
|
|
121
|
+
fitBoundsOptions: { padding: 24 },
|
|
122
|
+
}
|
|
123
|
+
: { center: [h.centerLon, h.centerLat] as [number, number], zoom: h.centerZoom }),
|
|
115
124
|
})
|
|
116
125
|
map.addControl(new maplibregl.NavigationControl())
|
|
117
126
|
map.addControl(
|