@datagouv/components-next 1.0.2-dev.1 → 1.0.2-dev.100

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.
Files changed (108) hide show
  1. package/assets/main.css +4 -0
  2. package/dist/{Control-DuZJdKV_.js → Control-ZFh5ta_U.js} +1 -1
  3. package/dist/Datafair.client-C2j760M5.js +30 -0
  4. package/dist/{Event--kp8kMdJ.js → Event-DSQcW7OF.js} +24 -24
  5. package/dist/{Image-34hvypZI.js → Image-BijNEG0p.js} +6 -6
  6. package/dist/JsonPreview.client-PFfBR4J8.js +40 -0
  7. package/dist/{Map-BjUnLyj8.js → Map-BUtPf5GN.js} +756 -756
  8. package/dist/MapContainer.client-CGrS2baS.js +101 -0
  9. package/dist/{OSM-s40W6sQ2.js → OSM-D4MTdBtk.js} +2 -2
  10. package/dist/{PdfPreview.client-C-w6-w44.js → PdfPreview.client-DU36UBGQ.js} +822 -865
  11. package/dist/{Pmtiles.client-BR7_ldHY.js → Pmtiles.client-DuTezcn5.js} +574 -579
  12. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-ProPRqX6.js +61 -0
  13. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-hJQIqcZm.js} +2 -2
  14. package/dist/{Tile-DbNFNPfU.js → Tile-Dcl7oIVu.js} +35 -35
  15. package/dist/{TileImage-BsXBxMtq.js → TileImage-BJeHipMX.js} +4 -4
  16. package/dist/{View-BR92hTWP.js → View-xp_P_OHw.js} +412 -401
  17. package/dist/XmlPreview.client-Bcq2Ye14.js +34 -0
  18. package/dist/{common-PJfpC179.js → common-BjQlan3k.js} +36 -36
  19. package/dist/components-next.css +6 -6
  20. package/dist/components-next.js +175 -149
  21. package/dist/components.css +1 -1
  22. package/dist/{index-SrYZwgCT.js → index-BJ-zwAF5.js} +32886 -27183
  23. package/dist/{main-B2kXxWRG.js → main-TqHFAOCi.js} +92255 -76685
  24. package/dist/{proj-DsetBcW7.js → proj-CsNo9yH1.js} +532 -512
  25. package/dist/{tilecoord-Db24Px13.js → tilecoord-A0fLnBZr.js} +28 -28
  26. package/dist/{vue3-xml-viewer.common-BRxsqI9j.js → vue3-xml-viewer.common-BnJTx_B7.js} +1 -1
  27. package/package.json +18 -11
  28. package/src/components/ActivityList/ActivityList.vue +0 -2
  29. package/src/components/Chart/ChartViewer.vue +226 -0
  30. package/src/components/Chart/ChartViewerWrapper.vue +170 -0
  31. package/src/components/DataserviceCard.vue +3 -0
  32. package/src/components/DatasetCard.vue +9 -4
  33. package/src/components/Form/Listbox.vue +101 -0
  34. package/src/components/Form/SearchableSelect.vue +2 -1
  35. package/src/components/InfiniteLoader.vue +53 -0
  36. package/src/components/ObjectCardHeader.vue +11 -4
  37. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  38. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  39. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  40. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  41. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  42. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  43. package/src/components/OpenApiViewer/openapi.ts +150 -0
  44. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  45. package/src/components/Pagination.vue +8 -5
  46. package/src/components/ReadMore.vue +1 -1
  47. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  48. package/src/components/ResourceAccordion/Datafair.client.vue +4 -10
  49. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  50. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -121
  51. package/src/components/ResourceAccordion/MapContainer.client.vue +5 -11
  52. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  53. package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -103
  54. package/src/components/ResourceAccordion/Pmtiles.client.vue +5 -10
  55. package/src/components/ResourceAccordion/Preview.vue +16 -21
  56. package/src/components/ResourceAccordion/PreviewLoader.vue +1 -2
  57. package/src/components/ResourceAccordion/PreviewUnavailable.vue +22 -0
  58. package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
  59. package/src/components/ResourceAccordion/ResourceAccordion.vue +10 -109
  60. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -115
  61. package/src/components/ResourceExplorer/ResourceExplorer.vue +35 -20
  62. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
  63. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +56 -146
  64. package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
  65. package/src/components/ReuseCard.vue +12 -4
  66. package/src/components/Search/GlobalSearch.vue +191 -110
  67. package/src/components/Search/SearchInput.vue +5 -4
  68. package/src/components/TabularExplorer/TabularCell.vue +51 -0
  69. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  70. package/src/components/TabularExplorer/TabularExplorer.vue +973 -0
  71. package/src/components/TabularExplorer/TabularFilterContent.vue +351 -0
  72. package/src/components/TabularExplorer/TabularFilterPopover.vue +111 -0
  73. package/src/components/TabularExplorer/types.ts +83 -0
  74. package/src/composables/useHasTabularData.ts +13 -0
  75. package/src/composables/useMetrics.ts +1 -1
  76. package/src/composables/useResourceCapabilities.ts +1 -1
  77. package/src/composables/useSearchFilter.ts +118 -0
  78. package/src/composables/useStableQueryParams.ts +31 -3
  79. package/src/composables/useTabularProfile.ts +70 -0
  80. package/src/config.ts +20 -3
  81. package/src/functions/api.ts +9 -37
  82. package/src/functions/api.types.ts +1 -0
  83. package/src/functions/charts.ts +68 -0
  84. package/src/functions/datasets.ts +0 -17
  85. package/src/functions/metrics.ts +6 -4
  86. package/src/functions/resources.ts +56 -1
  87. package/src/functions/tabular.ts +60 -0
  88. package/src/functions/tabularApi.ts +138 -11
  89. package/src/main.ts +94 -7
  90. package/src/types/dataservices.ts +2 -0
  91. package/src/types/organizations.ts +1 -1
  92. package/src/types/pages.ts +0 -5
  93. package/src/types/posts.ts +2 -2
  94. package/src/types/reports.ts +5 -1
  95. package/src/types/search.ts +52 -1
  96. package/src/types/site.ts +5 -3
  97. package/src/types/ui.ts +2 -0
  98. package/src/types/users.ts +2 -1
  99. package/src/types/visualizations.ts +89 -0
  100. package/assets/swagger-themes/newspaper.css +0 -1670
  101. package/dist/Datafair.client-E5D6ePRC.js +0 -35
  102. package/dist/JsonPreview.client-C-6eBbPw.js +0 -87
  103. package/dist/MapContainer.client-DRkAmdOc.js +0 -105
  104. package/dist/Swagger.client-D4-F6yEf.js +0 -4
  105. package/dist/XmlPreview.client-Dl2VCgXF.js +0 -79
  106. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  107. package/src/functions/pagination.ts +0 -9
  108. /package/assets/illustrations/{_microscope.svg → microscope.svg} +0 -0
@@ -49,45 +49,23 @@
49
49
  </template>
50
50
 
51
51
  <script setup lang="ts">
52
- import { onMounted, ref } from 'vue'
52
+ import { computed } from 'vue'
53
53
  import type { Resource } from '../../types/resources'
54
- import { useGetProfile } from '../../functions/tabularApi'
55
54
  import { useTranslation } from '../../composables/useTranslation'
55
+ import { injectTabularProfile } from '../../composables/useTabularProfile'
56
56
  import PreviewLoader from './PreviewLoader.vue'
57
57
 
58
58
  const props = defineProps<{ resource: Resource }>()
59
- const getProfile = useGetProfile()
60
59
  const { t } = useTranslation()
61
60
 
62
- type ColumnInfo = {
63
- score: number
64
- format: string
65
- python_type: string
66
- }
61
+ // Profile is shared with sibling components (e.g. TabularExplorer) via
62
+ // `provideTabularProfile` in the parent. Falls back to a local fetch
63
+ // when no parent provides it (standalone usage).
64
+ const { data: profileData, status } = await injectTabularProfile(() => props.resource.id)
67
65
 
68
- const columns = ref<Array<string>>([])
69
- const columnsInfo = ref<Record<string, ColumnInfo>>({})
70
- const loading = ref(true)
71
- const hasError = ref(false)
72
- const hasColumnInfo = ref(false)
73
-
74
- onMounted(async () => {
75
- try {
76
- const response = await getProfile(props.resource.id) // Assurez-vous que cette fonction retourne bien les données attendues
77
- if ('profile' in response && response.profile) {
78
- columns.value = Object.keys(response.profile.columns)
79
- columnsInfo.value = response.profile.columns
80
- hasColumnInfo.value = true
81
- loading.value = false
82
- }
83
- else {
84
- hasError.value = true
85
- loading.value = false
86
- }
87
- }
88
- catch {
89
- hasError.value = true
90
- loading.value = false
91
- }
92
- })
66
+ const loading = computed(() => status.value === 'idle' || status.value === 'pending')
67
+ const hasError = computed(() => status.value === 'error')
68
+ const hasColumnInfo = computed(() => !!profileData.value?.profile?.columns)
69
+ const columns = computed(() => profileData.value?.profile ? Object.keys(profileData.value.profile.columns) : [])
70
+ const columnsInfo = computed(() => profileData.value?.profile?.columns ?? {})
93
71
  </script>
@@ -9,21 +9,15 @@
9
9
  border: none;"
10
10
  />
11
11
  </div>
12
- <SimpleBanner
13
- v-else
14
- type="warning"
15
- class="flex items-center space-x-2"
16
- >
17
- <RiErrorWarningLine class="shrink-0 size-6" />
18
- <span>{{ t("Erreur lors de l'affichage de l'aperçu.") }}</span>
19
- </SimpleBanner>
12
+ <PreviewUnavailable v-else>
13
+ {{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le depuis l'onglet Téléchargements.") }}
14
+ </PreviewUnavailable>
20
15
  </div>
21
16
  </template>
22
17
 
23
18
  <script setup lang="ts">
24
19
  import { computed } from 'vue'
25
- import { RiErrorWarningLine } from '@remixicon/vue'
26
- import SimpleBanner from '../SimpleBanner.vue'
20
+ import PreviewUnavailable from './PreviewUnavailable.vue'
27
21
  import type { Resource } from '../../types/resources'
28
22
  import type { Dataset, DatasetV2 } from '../../types/datasets'
29
23
  import { useTranslation } from '../../composables/useTranslation'
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <dl class="fr-pl-0">
3
+ <dt
4
+ v-if="resource.format === 'url'"
5
+ class="font-bold fr-text--sm fr-mb-0"
6
+ >
7
+ {{ t("URL d'origine") }}
8
+ </dt>
9
+ <dt
10
+ v-else
11
+ class="font-bold fr-text--sm fr-mb-0"
12
+ >
13
+ {{ t('Format original') }}
14
+ </dt>
15
+ <dd class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
16
+ <span
17
+ v-if="resource.format === 'url'"
18
+ class="inline-flex items-center max-w-full"
19
+ >
20
+ <a
21
+ :href="resource.latest"
22
+ class="fr-link no-icon-after truncate"
23
+ rel="ugc nofollow noopener"
24
+ target="_blank"
25
+ @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
26
+ >
27
+ {{ resource.url }}
28
+ </a>
29
+ <span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm shrink-0" />
30
+ </span>
31
+ <span v-else>
32
+ <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
33
+ <a
34
+ :href="resource.latest"
35
+ class="fr-link"
36
+ rel="ugc nofollow noopener"
37
+ @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${resource.format}`)"
38
+ >
39
+ <span>{{ t('Format {format}', { format: resource.format }) }}<span v-if="resourceFilesize"> - {{ filesize(resourceFilesize) }}</span></span>
40
+ </a>
41
+ </span>
42
+ <CopyButton
43
+ :label="t('Copier le lien')"
44
+ :copied-label="t('Lien copié !')"
45
+ :text="resource.latest"
46
+ class="relative"
47
+ />
48
+ </dd>
49
+ <template v-if="generatedFormats.length">
50
+ <dt class="font-bold fr-text--sm fr-mb-0">
51
+ {{ t('Formats générés automatiquement par {platform} (dernière mise à jour {date})', { platform: config.name, date: conversionsLastUpdate }) }}
52
+ </dt>
53
+ <dd
54
+ v-for="generatedFormat in generatedFormats"
55
+ :key="generatedFormat.format"
56
+ class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
57
+ >
58
+ <span>
59
+ <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
60
+ <a
61
+ :href="generatedFormat.url"
62
+ class="fr-link"
63
+ rel="ugc nofollow noopener"
64
+ @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${generatedFormat.format}`)"
65
+ >
66
+ <span>{{ t('Format {format}', { format: generatedFormat.format }) }}<span v-if="generatedFormat.size"> - {{ filesize(generatedFormat.size) }}</span></span>
67
+ </a>
68
+ </span>
69
+ <CopyButton
70
+ :label="t('Copier le lien')"
71
+ :copied-label="t('Lien copié !')"
72
+ :text="generatedFormat.url"
73
+ class="relative"
74
+ />
75
+ </dd>
76
+ </template>
77
+ <template v-if="wfsFormats.length">
78
+ <dt class="font-bold fr-text--sm fr-mb-0">
79
+ <div class="flex gap-1 items-center">
80
+ {{ t('Formats exportés depuis le service WFS') }}
81
+ <span v-if="defaultWfsProjection"> ({{ t('projection {crs}', { crs: defaultWfsProjection }) }})</span>
82
+ <Tooltip>
83
+ <RiInformationLine
84
+ class="flex-none size-4"
85
+ :aria-label="t(`Le lien de téléchargement interroge directement le flux WFS distant. Le nombre de features téléchargées peut être limité.`)"
86
+ aria-hidden="true"
87
+ />
88
+ <template #tooltip>
89
+ <p class="text-sm font-normal mb-0">
90
+ {{ t(`Le lien de téléchargement interroge directement le flux WFS distant.`) }}
91
+ </p>
92
+ <p class="text-sm font-normal mb-0">
93
+ {{ t(`Le nombre de features téléchargées peut être limité.`) }}
94
+ </p>
95
+ </template>
96
+ </Tooltip>
97
+ </div>
98
+ </dt>
99
+ <dd
100
+ v-for="wfsFormat in wfsFormats"
101
+ :key="wfsFormat.format"
102
+ class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
103
+ >
104
+ <span>
105
+ <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
106
+ <a
107
+ :href="wfsFormat.url"
108
+ class="fr-link"
109
+ rel="ugc nofollow noopener"
110
+ @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${wfsFormat.format}`)"
111
+ >
112
+ <span>{{ t('Format {format}', { format: wfsFormat.format }) }}</span>
113
+ </a>
114
+ </span>
115
+ <CopyButton
116
+ :label="t('Copier le lien')"
117
+ :copied-label="t('Lien copié !')"
118
+ :text="wfsFormat.url"
119
+ class="relative"
120
+ />
121
+ </dd>
122
+ </template>
123
+ </dl>
124
+ </template>
125
+
126
+ <script setup lang="ts">
127
+ import { computed } from 'vue'
128
+ import { RiInformationLine } from '@remixicon/vue'
129
+ import CopyButton from '../CopyButton.vue'
130
+ import Tooltip from '../Tooltip.vue'
131
+ import { filesize } from '../../functions/helpers'
132
+ import { getResourceFilesize } from '../../functions/resources'
133
+ import { trackEvent } from '../../functions/matomo'
134
+ import { useComponentsConfig } from '../../config'
135
+ import { useFormatDate } from '../../functions/dates'
136
+ import { useTranslation } from '../../composables/useTranslation'
137
+ import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
138
+ import type { Resource } from '../../types/resources'
139
+ import type { Dataset, DatasetV2 } from '../../types/datasets'
140
+
141
+ const props = defineProps<{
142
+ resource: Resource
143
+ dataset: Dataset | DatasetV2
144
+ }>()
145
+
146
+ const { t } = useTranslation()
147
+ const config = useComponentsConfig()
148
+ const { formatRelativeIfRecentDate } = useFormatDate()
149
+
150
+ const { generatedFormats, wfsFormats, defaultWfsProjection } = useResourceCapabilities(
151
+ () => props.resource,
152
+ () => props.dataset,
153
+ )
154
+
155
+ const resourceFilesize = computed(() => getResourceFilesize(props.resource))
156
+
157
+ const conversionsLastUpdate = computed(() =>
158
+ formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined),
159
+ )
160
+ </script>
@@ -1,65 +1,31 @@
1
1
  <template>
2
- <div class="fr-text--xs">
3
- <div v-if="jsonData">
4
- <JsonViewer
5
- :value="jsonData"
6
- boxed
7
- sort
8
- theme="light"
9
- :max-depth="3"
10
- :expand-depth="2"
11
- :indent-width="2"
12
- />
13
- </div>
14
- <div
15
- v-else-if="loading"
16
- class="text-gray-medium"
17
- >
18
- {{ t("Chargement de l'aperçu JSON...") }}
19
- </div>
20
- <SimpleBanner
21
- v-else-if="fileTooLarge"
22
- type="warning"
23
- class="flex items-center space-x-2"
24
- >
25
- <RiErrorWarningLine class="shrink-0 size-6" />
26
- <span>{{ fileSizeBytes
27
- ? t("Fichier JSON trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
28
- : t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
29
- }}</span>
30
- </SimpleBanner>
31
- <SimpleBanner
32
- v-else-if="error === 'network'"
33
- type="warning"
34
- class="flex items-center space-x-2"
35
- >
36
- <RiErrorWarningLine class="shrink-0 size-6" />
37
- <span>{{ t("Ce fichier JSON ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}</span>
38
- </SimpleBanner>
39
- <SimpleBanner
40
- v-else-if="error"
41
- type="warning"
42
- class="flex items-center space-x-2"
43
- >
44
- <RiErrorWarningLine class="shrink-0 size-6" />
45
- <span>{{ t("Erreur lors du chargement de l'aperçu JSON.") }}</span>
46
- </SimpleBanner>
47
- </div>
2
+ <PreviewWrapper
3
+ v-slot="{ data }"
4
+ file-type="JSON"
5
+ :resource="resource"
6
+ :max-size="config.maxJsonPreviewCharSize"
7
+ :load="load"
8
+ >
9
+ <JsonViewer
10
+ :value="data"
11
+ boxed
12
+ sort
13
+ theme="light"
14
+ :max-depth="3"
15
+ :expand-depth="2"
16
+ :indent-width="2"
17
+ />
18
+ </PreviewWrapper>
48
19
  </template>
49
20
 
50
21
  <script setup lang="ts">
51
- import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
52
- import { RiErrorWarningLine } from '@remixicon/vue'
53
-
22
+ import { defineAsyncComponent } from 'vue'
54
23
  import { useComponentsConfig } from '../../config'
55
- import SimpleBanner from '../SimpleBanner.vue'
24
+ import PreviewWrapper from './PreviewWrapper.vue'
56
25
  import type { Resource } from '../../types/resources'
57
- import { useTranslation } from '../../composables/useTranslation'
58
- import { getResourceFilesize } from '../../functions/datasets'
59
26
 
60
27
  const JsonViewer = defineAsyncComponent(() =>
61
28
  import('vue3-json-viewer').then((module) => {
62
- // Import CSS when component loads
63
29
  import('vue3-json-viewer/dist/vue3-json-viewer.css')
64
30
  return module.JsonViewer
65
31
  }),
@@ -70,74 +36,10 @@ const props = defineProps<{
70
36
  }>()
71
37
 
72
38
  const config = useComponentsConfig()
73
- const { t } = useTranslation()
74
-
75
- const jsonData = ref<unknown>(null)
76
- const loading = ref(false)
77
- const error = ref<string | null>(null)
78
- const fileTooLarge = ref(false)
79
-
80
- const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
81
-
82
- const shouldLoadJson = computed(() => {
83
- const size = fileSizeBytes.value
84
- if (!size) {
85
- // If we don't know the size, don't risk loading a potentially huge file
86
- return false
87
- }
88
-
89
- // Check if maxJsonPreviewCharSize is configured
90
- if (!config.maxJsonPreviewCharSize) {
91
- // If no limit is set, don't load unknown files
92
- return false
93
- }
94
-
95
- // Convert maxJsonPreviewCharSize from characters to bytes (rough estimate)
96
- // Assuming average 1 byte per character for JSON
97
- const maxByteSize = config.maxJsonPreviewCharSize
98
-
99
- return size <= maxByteSize
100
- })
101
39
 
102
- const fetchJsonData = async () => {
103
- // Check if file is too large or size is unknown before making the request
104
- if (!shouldLoadJson.value) {
105
- fileTooLarge.value = true
106
- return
107
- }
108
-
109
- loading.value = true
110
- error.value = null
111
-
112
- try {
113
- const response = await fetch(props.resource.url)
114
- // const response = await fetch('/test-data.json') // For testing locally without CORS issues
115
- if (!response.ok) {
116
- throw new Error(`HTTP error! status: ${response.status}`)
117
- }
118
- const data = await response.json()
119
-
120
- // Use the original data directly - let the JSON viewer handle large files
121
- jsonData.value = data
122
- }
123
- catch (err) {
124
- console.error('Error loading JSON:', err)
125
-
126
- if (err instanceof TypeError) {
127
- error.value = 'network'
128
- }
129
- else {
130
- error.value = 'generic'
131
- }
132
-
133
- jsonData.value = null
134
- }
135
- finally {
136
- loading.value = false
137
- }
40
+ const load = async () => {
41
+ const response = await fetch(props.resource.url)
42
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
43
+ return response.json()
138
44
  }
139
-
140
- onMounted(() => {
141
- fetchJsonData()
142
- })
143
45
  </script>
@@ -1,12 +1,7 @@
1
1
  <template>
2
- <SimpleBanner
3
- v-if="hasError"
4
- type="warning"
5
- class="flex items-center space-x-2"
6
- >
7
- <RiErrorWarningLine class="shrink-0 size-6" />
8
- <span>{{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}</span>
9
- </SimpleBanner>
2
+ <PreviewUnavailable v-if="hasError">
3
+ {{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}
4
+ </PreviewUnavailable>
10
5
  <div
11
6
  v-else
12
7
  id="map"
@@ -16,9 +11,7 @@
16
11
 
17
12
  <script setup lang = "ts">
18
13
  import { onMounted, ref, useTemplateRef } from 'vue'
19
- import { RiErrorWarningLine } from '@remixicon/vue'
20
-
21
- import SimpleBanner from '../SimpleBanner.vue'
14
+ import PreviewUnavailable from './PreviewUnavailable.vue'
22
15
  import type { Resource } from '../../types/resources'
23
16
  import { useTranslation } from '../../composables/useTranslation'
24
17
 
@@ -96,6 +89,7 @@ async function displayMap() {
96
89
 
97
90
  const attributions = new GeoportalAttribution({
98
91
  position: 'bottom-right',
92
+ collapsed: false,
99
93
  })
100
94
  map.addControl(attributions)
101
95
 
@@ -7,9 +7,8 @@ import DescriptionTerm from '../DescriptionTerm.vue'
7
7
  import { useFormatDate } from '../../functions/dates'
8
8
  import { filesize } from '../../functions/helpers'
9
9
  import ExtraAccordion from '../ExtraAccordion.vue'
10
- import { getResourceTitleId, getResourceLabel } from '../../functions/resources'
10
+ import { getResourceTitleId, getResourceLabel, getResourceFilesize } from '../../functions/resources'
11
11
  import { useTranslation } from '../../composables/useTranslation'
12
- import { getResourceFilesize } from '../../functions/datasets'
13
12
 
14
13
  const props = defineProps<{
15
14
  resource: Resource
@@ -1,64 +1,34 @@
1
1
  <template>
2
- <div class="text-xs">
2
+ <PreviewWrapper
3
+ v-slot="{ data }"
4
+ file-type="PDF"
5
+ :resource="resource"
6
+ :max-size="config.maxPdfPreviewByteSize"
7
+ :load="load"
8
+ @loaded="renderAllPages"
9
+ >
3
10
  <div
4
- v-if="pdfReady"
5
11
  ref="containerRef"
6
12
  class="w-full overflow-y-auto max-h-[80vh] space-y-3"
7
13
  >
8
14
  <canvas
9
- v-for="page in totalPages"
15
+ v-for="page in (data as PDFDocumentProxy).numPages"
10
16
  :key="page"
11
17
  :ref="(el) => setCanvasRef(el as HTMLCanvasElement, page)"
12
18
  class="w-full"
13
19
  />
14
20
  </div>
15
- <div
16
- v-else-if="loading"
17
- class="text-gray-medium"
18
- >
19
- {{ t("Chargement de l'aperçu PDF...") }}
20
- </div>
21
- <SimpleBanner
22
- v-else-if="fileTooLarge"
23
- type="warning"
24
- class="flex items-center space-x-2"
25
- >
26
- <RiErrorWarningLine class="flex-none size-6" />
27
- <span>{{ fileSizeBytes
28
- ? t("Fichier PDF trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
29
- : t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
30
- }}</span>
31
- </SimpleBanner>
32
- <SimpleBanner
33
- v-else-if="error === 'network'"
34
- type="warning"
35
- class="flex items-center space-x-2"
36
- >
37
- <RiErrorWarningLine class="flex-none size-6" />
38
- <span>{{ t("Ce fichier PDF ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}</span>
39
- </SimpleBanner>
40
- <SimpleBanner
41
- v-else-if="error"
42
- type="warning"
43
- class="flex items-center space-x-2"
44
- >
45
- <RiErrorWarningLine class="flex-none size-6" />
46
- <span>{{ t("Erreur lors du chargement de l'aperçu PDF. Pour consulter le fichier, téléchargez-le depuis l'onglet Téléchargements.") }}</span>
47
- </SimpleBanner>
48
- </div>
21
+ </PreviewWrapper>
49
22
  </template>
50
23
 
51
24
  <script setup lang="ts">
52
- import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
53
- import { RiErrorWarningLine } from '@remixicon/vue'
25
+ import { onBeforeUnmount, ref } from 'vue'
54
26
  import * as pdfjsLib from 'pdfjs-dist'
55
27
  import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
56
28
  import type { PDFDocumentProxy } from 'pdfjs-dist'
57
- import SimpleBanner from '../SimpleBanner.vue'
29
+ import PreviewWrapper from './PreviewWrapper.vue'
58
30
  import { useComponentsConfig } from '../../config'
59
31
  import type { Resource } from '../../types/resources'
60
- import { useTranslation } from '../../composables/useTranslation'
61
- import { getResourceFilesize } from '../../functions/datasets'
62
32
 
63
33
  pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
64
34
 
@@ -67,15 +37,8 @@ const props = defineProps<{
67
37
  }>()
68
38
 
69
39
  const config = useComponentsConfig()
70
- const { t } = useTranslation()
71
40
 
72
41
  const containerRef = ref<HTMLElement | null>(null)
73
- const pdfReady = ref(false)
74
- const loading = ref(false)
75
- const error = ref<string | null>(null)
76
- const fileTooLarge = ref(false)
77
- const totalPages = ref(0)
78
-
79
42
  let pdfDoc: PDFDocumentProxy | null = null
80
43
  const canvasRefs = new Map<number, HTMLCanvasElement>()
81
44
 
@@ -111,64 +74,22 @@ async function renderPage(pageNum: number) {
111
74
  }).promise
112
75
  }
113
76
 
114
- const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
115
-
116
- const shouldLoadPdf = computed(() => {
117
- const size = fileSizeBytes.value
118
- if (!size) {
119
- // If we don't know the size, don't risk loading a potentially huge file
120
- return false
121
- }
122
-
123
- // Use maxPdfPreviewByteSize from config, fallback to 10 MB if not set
124
- const maxByteSize = config.maxPdfPreviewByteSize ?? 10_000_000
125
- return size <= maxByteSize
126
- })
127
-
128
- const loadPdf = async () => {
129
- if (!shouldLoadPdf.value) {
130
- fileTooLarge.value = true
131
- return
132
- }
133
-
134
- loading.value = true
135
- error.value = null
136
-
137
- try {
138
- const loadingTask = pdfjsLib.getDocument({
139
- url: props.resource.url,
140
- isEvalSupported: false,
141
- })
142
-
143
- pdfDoc = await loadingTask.promise
144
- totalPages.value = pdfDoc.numPages
145
- pdfReady.value = true
146
-
147
- await nextTick()
77
+ const load = async () => {
78
+ const loadingTask = pdfjsLib.getDocument({
79
+ url: props.resource.url,
80
+ isEvalSupported: false,
81
+ })
82
+ pdfDoc = await loadingTask.promise
83
+ return pdfDoc
84
+ }
148
85
 
149
- for (let i = 1; i <= pdfDoc.numPages; i++) {
150
- await renderPage(i)
151
- }
152
- }
153
- catch (err) {
154
- console.error('Error loading PDF:', err)
155
-
156
- if (err instanceof TypeError) {
157
- error.value = 'network'
158
- }
159
- else {
160
- error.value = 'generic'
161
- }
162
- }
163
- finally {
164
- loading.value = false
86
+ const renderAllPages = async () => {
87
+ if (!pdfDoc) return
88
+ for (let i = 1; i <= pdfDoc.numPages; i++) {
89
+ await renderPage(i)
165
90
  }
166
91
  }
167
92
 
168
- onMounted(() => {
169
- loadPdf()
170
- })
171
-
172
93
  onBeforeUnmount(() => {
173
94
  pdfDoc?.destroy()
174
95
  })
@@ -1,13 +1,8 @@
1
1
  <template>
2
2
  <div>
3
- <SimpleBanner
4
- v-if="hasError"
5
- type="warning"
6
- class="flex items-center space-x-2"
7
- >
8
- <RiErrorWarningLine class="shrink-0 size-6" />
9
- <span>{{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}</span>
10
- </SimpleBanner>
3
+ <PreviewUnavailable v-if="hasError">
4
+ {{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}
5
+ </PreviewUnavailable>
11
6
  <div
12
7
  v-else
13
8
  class="-mx-4"
@@ -53,7 +48,7 @@
53
48
 
54
49
  <script setup lang="ts">
55
50
  import { computed, onMounted, ref, useTemplateRef } from 'vue'
56
- import { RiErrorWarningLine, RiExternalLinkFill } from '@remixicon/vue'
51
+ import { RiExternalLinkFill } from '@remixicon/vue'
57
52
  import { Protocol, PMTiles } from 'pmtiles'
58
53
  import maplibregl from 'maplibre-gl'
59
54
  import DOMPurify from 'dompurify'
@@ -64,7 +59,7 @@ import type { Resource } from '../../types/resources'
64
59
  import type { Dataset, DatasetV2 } from '../../types/datasets'
65
60
  import BrandedButton from '../BrandedButton.vue'
66
61
  import styleVector from '../../../assets/json/vector.json'
67
- import SimpleBanner from '../SimpleBanner.vue'
62
+ import PreviewUnavailable from './PreviewUnavailable.vue'
68
63
  import { useTranslation } from '../../composables/useTranslation'
69
64
  import franceSvg from './france.svg?raw'
70
65
  import { getOwnerName, getOwnerPage } from '../../functions/owned'