@datagouv/components-next 0.0.7 → 0.0.9

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 (103) hide show
  1. package/README.md +34 -14
  2. package/assets/json/vector.json +2377 -0
  3. package/assets/main.css +3 -0
  4. package/assets/swagger-themes/newspaper.css +1669 -0
  5. package/assets/tailwind.config.js +1 -1
  6. package/dist/JsonPreview.client-BIz1_EiB.js +92 -0
  7. package/dist/MapContainer.client-ZDwr4Q_I.js +78276 -0
  8. package/dist/PdfPreview.client-BTTMM27i.js +112 -0
  9. package/dist/Pmtiles.client-4kOoUQcR.js +22377 -0
  10. package/dist/Swagger.client-Q7a5wb51.js +4 -0
  11. package/dist/XmlPreview.client-BYIIkDqf.js +84 -0
  12. package/dist/components-next.css +52 -1
  13. package/dist/components-next.js +42 -41
  14. package/dist/components.css +1 -1
  15. package/dist/main-CLUk9Jj7.js +105843 -0
  16. package/dist/pdf-vue3-BZh6kzke.js +273 -0
  17. package/dist/pdf.min-f72cfa08-DAetWL3M.js +9501 -0
  18. package/dist/{text-clamp.esm-DurZFOvT.js → text-clamp.esm-DP59tec5.js} +1 -1
  19. package/dist/vue3-json-viewer-DIQzFF6K.js +1089 -0
  20. package/dist/vue3-xml-viewer.common-BmKw6vER.js +5437 -0
  21. package/package.json +7 -5
  22. package/src/components/AvatarWithName.vue +6 -2
  23. package/src/components/BannerAction.vue +1 -1
  24. package/src/components/BrandedButton.vue +13 -8
  25. package/src/components/CopyButton.vue +7 -7
  26. package/src/components/DataserviceCard.vue +54 -23
  27. package/src/components/DatasetCard.vue +36 -24
  28. package/src/components/DatasetInformationPanel.vue +19 -18
  29. package/src/components/DatasetQuality.vue +21 -18
  30. package/src/components/DatasetQualityInline.vue +1 -1
  31. package/src/components/DatasetQualityItem.vue +3 -3
  32. package/src/components/DatasetQualityItemWarning.vue +2 -2
  33. package/src/components/DatasetQualityScore.vue +2 -2
  34. package/src/components/DatasetQualityTooltipContent.vue +29 -29
  35. package/src/components/DescriptionDetails.vue +2 -2
  36. package/src/components/ExtraAccordion.vue +10 -7
  37. package/src/components/OrganizationCard.vue +9 -4
  38. package/src/components/OrganizationNameWithCertificate.vue +25 -11
  39. package/src/components/Pagination.vue +26 -15
  40. package/src/components/ReadMore.vue +2 -2
  41. package/src/components/ResourceAccordion/DataStructure.vue +2 -2
  42. package/src/components/ResourceAccordion/EditButton.vue +10 -6
  43. package/src/components/ResourceAccordion/JsonPreview.client.vue +153 -0
  44. package/src/components/ResourceAccordion/MapContainer.client.vue +137 -0
  45. package/src/components/ResourceAccordion/Metadata.vue +33 -54
  46. package/src/components/ResourceAccordion/PdfPreview.client.vue +189 -0
  47. package/src/components/ResourceAccordion/Pmtiles.client.vue +166 -0
  48. package/src/components/ResourceAccordion/Preview.vue +39 -37
  49. package/src/components/ResourceAccordion/ResourceAccordion.vue +141 -63
  50. package/src/components/ResourceAccordion/ResourceIcon.vue +7 -1
  51. package/src/components/ResourceAccordion/SchemaBadge.vue +26 -26
  52. package/src/components/ResourceAccordion/{Swagger.vue → Swagger.client.vue} +1 -1
  53. package/src/components/ResourceAccordion/XmlPreview.client.vue +143 -0
  54. package/src/components/ReuseCard.vue +10 -7
  55. package/src/components/ReuseDetails.vue +3 -3
  56. package/src/components/SimpleBanner.vue +7 -4
  57. package/src/components/SmallChart.vue +23 -9
  58. package/src/components/StatBox.vue +92 -10
  59. package/src/config.ts +6 -2
  60. package/src/functions/api.ts +18 -18
  61. package/src/functions/dates.ts +81 -74
  62. package/src/functions/helpers.ts +5 -4
  63. package/src/functions/organizations.ts +5 -5
  64. package/src/functions/resources.ts +34 -5
  65. package/src/functions/schemas.ts +4 -3
  66. package/src/functions/tabularApi.ts +1 -1
  67. package/src/main.ts +10 -11
  68. package/src/types/badges.ts +3 -3
  69. package/src/types/contact_point.ts +5 -5
  70. package/src/types/dataservices.ts +16 -2
  71. package/src/types/datasets.ts +20 -2
  72. package/src/types/frequency.ts +5 -5
  73. package/src/types/granularity.ts +12 -4
  74. package/src/types/harvest.ts +2 -2
  75. package/src/types/licenses.ts +8 -8
  76. package/src/types/organizations.ts +6 -0
  77. package/src/types/resources.ts +3 -3
  78. package/src/types/reuses.ts +3 -1
  79. package/src/types/site.ts +8 -0
  80. package/src/types/ui.ts +2 -2
  81. package/src/types/users.ts +24 -8
  82. package/src/types/vue3-xml-viewer.d.ts +10 -0
  83. package/dist/Swagger-DjysB-OI.js +0 -67851
  84. package/dist/en-DCRve7vN.js +0 -613
  85. package/dist/fr-DCOnbL-p.js +0 -613
  86. package/dist/locales/de.js +0 -155
  87. package/dist/locales/en.js +0 -155
  88. package/dist/locales/es.js +0 -155
  89. package/dist/locales/fr.js +0 -155
  90. package/dist/locales/it.js +0 -155
  91. package/dist/locales/pt.js +0 -155
  92. package/dist/locales/sr.js +0 -155
  93. package/dist/main-CPW2vNLE.js +0 -32008
  94. package/src/components/DescriptionList/DescriptionDetails.stories.ts +0 -43
  95. package/src/components/DescriptionList/DescriptionList.stories.ts +0 -47
  96. package/src/components/DescriptionList/DescriptionTerm.stories.ts +0 -28
  97. package/src/locales/de.json +0 -154
  98. package/src/locales/en.json +0 -154
  99. package/src/locales/es.json +0 -154
  100. package/src/locales/fr.json +0 -154
  101. package/src/locales/it.json +0 -154
  102. package/src/locales/pt.json +0 -154
  103. package/src/locales/sr.json +0 -154
@@ -0,0 +1,153 @@
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="shink-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="shink-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="shink-0 size-6" />
45
+ <span>{{ $t("Erreur lors du chargement de l'aperçu JSON.") }}</span>
46
+ </SimpleBanner>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
52
+ import { RiErrorWarningLine } from '@remixicon/vue'
53
+
54
+ import { useComponentsConfig } from '../../config'
55
+ import SimpleBanner from '../SimpleBanner.vue'
56
+ import type { Resource } from '../../types/resources'
57
+
58
+ const JsonViewer = defineAsyncComponent(() =>
59
+ import('vue3-json-viewer').then((module) => {
60
+ // Import CSS when component loads
61
+ import('vue3-json-viewer/dist/vue3-json-viewer.css')
62
+ return module.JsonViewer
63
+ }),
64
+ )
65
+
66
+ const props = defineProps<{
67
+ resource: Resource
68
+ }>()
69
+
70
+ const config = useComponentsConfig()
71
+
72
+ const jsonData = ref<unknown>(null)
73
+ const loading = ref(false)
74
+ const error = ref<string | null>(null)
75
+ const fileTooLarge = ref(false)
76
+
77
+ const fileSizeBytes = computed(() => {
78
+ // Check if resource has filesize
79
+ if (props.resource.filesize) {
80
+ return props.resource.filesize
81
+ }
82
+
83
+ // Check if resource has content-length in extras (from API metadata)
84
+ const contentLength = props.resource.extras?.['analysis:content-length']
85
+ if (contentLength && typeof contentLength === 'number') {
86
+ return contentLength
87
+ }
88
+
89
+ return null
90
+ })
91
+
92
+ const shouldLoadJson = computed(() => {
93
+ const size = fileSizeBytes.value
94
+ if (!size) {
95
+ // If we don't know the size, don't risk loading a potentially huge file
96
+ return false
97
+ }
98
+
99
+ // Check if maxJsonPreviewSize is configured
100
+ if (!config.maxJsonPreviewSize) {
101
+ // If no limit is set, don't load unknown files
102
+ return false
103
+ }
104
+
105
+ // Convert maxJsonPreviewSize from characters to bytes (rough estimate)
106
+ // Assuming average 1 byte per character for JSON
107
+ const maxSizeBytes = config.maxJsonPreviewSize
108
+
109
+ return size <= maxSizeBytes
110
+ })
111
+
112
+ const fetchJsonData = async () => {
113
+ // Check if file is too large or size is unknown before making the request
114
+ if (!shouldLoadJson.value) {
115
+ fileTooLarge.value = true
116
+ return
117
+ }
118
+
119
+ loading.value = true
120
+ error.value = null
121
+
122
+ try {
123
+ const response = await fetch(props.resource.url)
124
+ // const response = await fetch('/test-data.json') // For testing locally without CORS issues
125
+ if (!response.ok) {
126
+ throw new Error(`HTTP error! status: ${response.status}`)
127
+ }
128
+ const data = await response.json()
129
+
130
+ // Use the original data directly - let the JSON viewer handle large files
131
+ jsonData.value = data
132
+ }
133
+ catch (err) {
134
+ console.error('Error loading JSON:', err)
135
+
136
+ if (err instanceof TypeError) {
137
+ error.value = 'network'
138
+ }
139
+ else {
140
+ error.value = 'generic'
141
+ }
142
+
143
+ jsonData.value = null
144
+ }
145
+ finally {
146
+ loading.value = false
147
+ }
148
+ }
149
+
150
+ onMounted(() => {
151
+ fetchJsonData()
152
+ })
153
+ </script>
@@ -0,0 +1,137 @@
1
+ <template>
2
+ <SimpleBanner
3
+ v-if="hasError"
4
+ type="warning"
5
+ class="flex items-center space-x-2"
6
+ >
7
+ <RiErrorWarningLine class="shink-0 size-6" />
8
+ <span>{{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}</span>
9
+ </SimpleBanner>
10
+ <div
11
+ v-else
12
+ id="map"
13
+ ref="mapRef"
14
+ />
15
+ </template>
16
+
17
+ <script setup lang = "ts">
18
+ import { onMounted, ref } from 'vue'
19
+ import { useI18n } from 'vue-i18n'
20
+ import { RiErrorWarningLine } from '@remixicon/vue'
21
+
22
+ import View from 'ol/View'
23
+ import Map from 'ol/Map'
24
+ import ScaleLine from 'ol/control/ScaleLine'
25
+ import TileLayer from 'ol/layer/Tile'
26
+ import OSM from 'ol/source/OSM'
27
+
28
+ import {
29
+ CRS,
30
+ GeoportalAttribution,
31
+ GeoportalFullScreen,
32
+ GeoportalZoom,
33
+ LayerImport,
34
+ LayerSwitcher,
35
+ } from 'geopf-extensions-openlayers'
36
+
37
+ import SimpleBanner from '../SimpleBanner.vue'
38
+ import type { Resource } from '../../types/resources'
39
+
40
+ const props = defineProps<{ resource: Resource }>()
41
+
42
+ const { t } = useI18n()
43
+
44
+ let map = null
45
+ const mapRef = ref(0)
46
+ const hasError = ref(false)
47
+
48
+ async function displayMap() {
49
+ await import('ol/ol.css')
50
+ await import('@gouvfr/dsfr/dist/dsfr.css')
51
+ await import('@gouvfr/dsfr/dist/utility/icons/icons.css')
52
+ await import('geopf-extensions-openlayers/css/Dsfr.css')
53
+
54
+ CRS.load()
55
+ map = new Map({
56
+ target: mapRef.value,
57
+ layers: [
58
+ new TileLayer({
59
+ source: new OSM(),
60
+ }),
61
+ ],
62
+ view: new View({
63
+ center: [288074.8449901076, 6247982.515792289],
64
+ zoom: 8,
65
+ constrainResolution: true,
66
+ }),
67
+ })
68
+
69
+ const scaleControl = new ScaleLine({
70
+ units: 'metric',
71
+ bar: false,
72
+ })
73
+ map.addControl(scaleControl)
74
+
75
+ const fullscreen = new GeoportalFullScreen({
76
+ position: 'top-right',
77
+ })
78
+ map.addControl(fullscreen)
79
+
80
+ const zoom = new GeoportalZoom({
81
+ position: 'bottom-left',
82
+ })
83
+ map.addControl(zoom)
84
+
85
+ const layerSwitcher = new LayerSwitcher({
86
+ options: {
87
+ position: 'top-right',
88
+ },
89
+ })
90
+ map.addControl(layerSwitcher)
91
+
92
+ const attributions = new GeoportalAttribution({
93
+ position: 'bottom-right',
94
+ })
95
+ map.addControl(attributions)
96
+
97
+ const layerImport = new LayerImport({
98
+ position: 'bottom-left',
99
+ listable: true,
100
+ layerTypes: ['WMS'],
101
+ })
102
+ layerImport._serviceUrlImportInput.value = props.resource.url
103
+ layerImport._formContainer.dispatchEvent(new CustomEvent('submit', { cancelable: true }))
104
+
105
+ map.addControl(layerImport)
106
+
107
+ // Wait for GetCapabilities to be called before trying to show layer
108
+ // TODO: use signal handling to know whether GetCapabilities failed or not
109
+ const waitTimeout = 500
110
+ let retry = 20
111
+ function showLayer() {
112
+ if (!layerImport._getCapResponseWMSLayers) {
113
+ retry--
114
+ if (retry > 0)
115
+ setTimeout(showLayer, waitTimeout)
116
+ else
117
+ hasError.value = true
118
+ }
119
+ else {
120
+ const layerInfo = layerImport._getCapResponseWMSLayers.filter(layer => layer.Name == props.resource.title)[0]
121
+ layerImport._addGetCapWMSLayer(layerInfo)
122
+ }
123
+ }
124
+ setTimeout(showLayer, waitTimeout)
125
+ }
126
+
127
+ onMounted(() => {
128
+ displayMap()
129
+ })
130
+ </script>
131
+
132
+ <style>
133
+ #map {
134
+ width: 100%;
135
+ height: 500px;
136
+ }
137
+ </style>
@@ -6,11 +6,10 @@ import CopyButton from '../CopyButton.vue'
6
6
  import DescriptionDetails from '../DescriptionDetails.vue'
7
7
  import DescriptionList from '../DescriptionList.vue'
8
8
  import DescriptionTerm from '../DescriptionTerm.vue'
9
- import { formatDate } from '../../functions/dates'
9
+ import { useFormatDate } from '../../functions/dates'
10
10
  import { filesize } from '../../functions/helpers'
11
11
  import ExtraAccordion from '../ExtraAccordion.vue'
12
12
  import { getResourceTitleId, getResourceLabel } from '../../functions/resources'
13
- import { useComponentsConfig } from '../../config'
14
13
 
15
14
  const props = defineProps<{
16
15
  resource: Resource
@@ -20,109 +19,89 @@ const hasExtras = computed(() => Object.keys(props.resource.extras).length)
20
19
  const resourceTitleId = computed(() => getResourceTitleId(props.resource))
21
20
 
22
21
  const { t } = useI18n()
23
- const config = useComponentsConfig()
22
+ const { formatDate } = useFormatDate()
24
23
  </script>
25
24
 
26
25
  <template>
27
26
  <div>
28
- <div class="flex gap-3rem flex-col-on-small">
29
- <DescriptionList class="flex-1">
27
+ <div class="flex flex-wrap gap-12 flex-col md:flex-row overflow-hidden">
28
+ <DescriptionList class="flex-1 max-w-full">
30
29
  <DescriptionTerm>
31
30
  {{ t('URL') }}
32
31
  <CopyButton
33
- :label="$t('Copy URL')"
34
- :copied-label="$t('URL copied!')"
32
+ :label="$t(`Copier l'URL`)"
33
+ :copied-label="$t('URL copiée !')"
35
34
  :text="resource.url"
36
35
  :aria-describedby="resourceTitleId"
37
36
  />
38
37
  </DescriptionTerm>
39
38
  <DescriptionDetails :with-ellipsis="false">
40
- <code class="code">
41
- <a :href="resource.url"><component
42
- :is="config.textClamp"
43
- v-if="config && config.textClamp"
44
- :max-lines="1"
45
- :autoresize="true"
46
- :text="resource.url"
47
- /></a>
39
+ <code class="code truncate p-1">
40
+ <a :href="resource.url">
41
+ {{ resource.url }}
42
+ </a>
48
43
  </code>
49
44
  </DescriptionDetails>
50
45
  <DescriptionTerm>
51
- {{ t('Stable URL') }}
46
+ {{ t('URL stable') }}
52
47
  <CopyButton
53
- :label="$t('Copy stable URL')"
54
- :copied-label="$t('Stable URL copied!')"
48
+ :label="$t(`Copier l'URL stable`)"
49
+ :copied-label="$t('URL stable copiée !')"
55
50
  :text="resource.latest"
56
51
  :aria-describedby="resourceTitleId"
57
52
  />
58
53
  </DescriptionTerm>
59
54
  <DescriptionDetails :with-ellipsis="false">
60
- <code class="code">
61
- <a :href="resource.latest"><component
62
- :is="config.textClamp"
63
- v-if="config && config.textClamp"
64
- :max-lines="1"
65
- :autoresize="true"
66
- :text="resource.latest"
67
- /></a>
55
+ <code class="code truncate p-1">
56
+ <a :href="resource.latest">
57
+ {{ resource.latest }}
58
+ </a>
68
59
  </code>
69
60
  </DescriptionDetails>
70
61
  <DescriptionTerm>
71
- {{ t('Identifier') }}
62
+ {{ t('Identifiant') }}
72
63
  <CopyButton
73
- :label="$t('Copy ID')"
74
- :copied-label="$t('ID copied!')"
64
+ :label="$t(`Copier l'identifiant`)"
65
+ :copied-label="$t('ID copié !')"
75
66
  :text="resource.id"
76
67
  :aria-describedby="resourceTitleId"
77
68
  />
78
69
  </DescriptionTerm>
79
70
  <DescriptionDetails :with-ellipsis="false">
80
- <code class="code">
81
- <component
82
- :is="config.textClamp"
83
- v-if="config && config.textClamp"
84
- :max-lines="1"
85
- :autoresize="true"
86
- :text="resource.id"
87
- />
71
+ <code class="code truncate p-1">
72
+ {{ resource.id }}
88
73
  </code>
89
74
  </DescriptionDetails>
90
75
  <template v-if="resource.checksum">
91
76
  <DescriptionTerm>
92
77
  {{ resource.checksum.type }}
93
78
  <CopyButton
94
- :label="$t('Copy checksum')"
95
- :copied-label="$t('Checksum copied!')"
79
+ :label="$t('Copier la somme de contrôle')"
80
+ :copied-label="$t('Somme de contrôle copiée !')"
96
81
  :text="resource.checksum.value"
97
82
  :aria-describedby="resourceTitleId"
98
83
  />
99
84
  </DescriptionTerm>
100
85
  <DescriptionDetails :with-ellipsis="false">
101
- <code class="code">
102
- <component
103
- :is="config.textClamp"
104
- v-if="config && config.textClamp"
105
- :max-lines="1"
106
- :autoresize="true"
107
- :text="resource.checksum.value"
108
- />
86
+ <code class="code truncate p-1">
87
+ {{ resource.checksum.value }}
109
88
  </code>
110
89
  </DescriptionDetails>
111
90
  </template>
112
91
  </DescriptionList>
113
92
  <DescriptionList style="flex-shrink: 0;">
114
- <DescriptionTerm>{{ t('Created on') }}</DescriptionTerm>
93
+ <DescriptionTerm>{{ t('Créée le') }}</DescriptionTerm>
115
94
  <DescriptionDetails>
116
95
  {{ formatDate(resource.created_at) }}
117
96
  </DescriptionDetails>
118
- <DescriptionTerm>{{ t('Modified on') }}</DescriptionTerm>
97
+ <DescriptionTerm>{{ t('Modifiée le') }}</DescriptionTerm>
119
98
  <DescriptionDetails>
120
99
  {{ formatDate(resource.last_modified) }}
121
100
  </DescriptionDetails>
122
101
  </DescriptionList>
123
102
  <DescriptionList style="flex-shrink: 0;">
124
103
  <template v-if="resource.filesize">
125
- <DescriptionTerm>{{ t('Size') }}</DescriptionTerm>
104
+ <DescriptionTerm>{{ t('Taille') }}</DescriptionTerm>
126
105
  <DescriptionDetails>
127
106
  {{ filesize(resource.filesize) }}
128
107
  </DescriptionDetails>
@@ -134,9 +113,9 @@ const config = useComponentsConfig()
134
113
  </DescriptionDetails>
135
114
  </template>
136
115
  <template v-if="resource.mime">
137
- <DescriptionTerm>{{ t('MIME Type') }}</DescriptionTerm>
116
+ <DescriptionTerm>{{ t('Type MIME') }}</DescriptionTerm>
138
117
  <DescriptionDetails>
139
- <code class="code text-overflow-ellipsis">{{ resource.mime }}</code>
118
+ <code class="code truncate">{{ resource.mime }}</code>
140
119
  </DescriptionDetails>
141
120
  </template>
142
121
  </DescriptionList>
@@ -145,8 +124,8 @@ const config = useComponentsConfig()
145
124
  <ExtraAccordion
146
125
  v-if="hasExtras"
147
126
  class="pt-6 mt-6 border-top border-gray-default"
148
- :button-text="t('See extras')"
149
- :title-text="t('Resource Extras')"
127
+ :button-text="t('Voir les extras')"
128
+ :title-text="t('Extras de la ressource')"
150
129
  title-level="h5"
151
130
  :extra="resource.extras"
152
131
  />
@@ -0,0 +1,189 @@
1
+ <template>
2
+ <div class="text-xs">
3
+ <div v-if="pdfData">
4
+ <!--
5
+ We use props.resource.url instead of props.resource.latest
6
+ because the PDF component raises an error otherwise.
7
+ See https://github.com/datagouv/cdata/pull/611
8
+ -->
9
+ <PDF
10
+ :src="props.resource.url"
11
+ :show-progress="true"
12
+ progress-color="#0063cb"
13
+ :show-page-tooltip="true"
14
+ :show-back-to-top-btn="true"
15
+ :scroll-threshold="300"
16
+ pdf-width="100%"
17
+ :row-gap="12"
18
+ :use-system-fonts="true"
19
+ :disable-range="false"
20
+ :disable-stream="false"
21
+ :disable-auto-fetch="false"
22
+ class="w-full"
23
+ @on-progress="handleProgress"
24
+ @on-complete="handleComplete"
25
+ @on-page-change="handlePageChange"
26
+ @on-pdf-init="handlePdfInit"
27
+ @on-error="handleError"
28
+ />
29
+ </div>
30
+ <div
31
+ v-else-if="loading"
32
+ class="text-gray-medium"
33
+ >
34
+ {{ $t("Chargement de l'aperçu PDF...") }}
35
+ </div>
36
+ <SimpleBanner
37
+ v-else-if="fileTooLarge"
38
+ type="warning"
39
+ class="flex items-center space-x-2"
40
+ >
41
+ <RiErrorWarningLine class="shink-0 size-6" />
42
+ <span>{{ fileSizeBytes
43
+ ? $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.")
44
+ : $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.")
45
+ }}</span>
46
+ </SimpleBanner>
47
+ <SimpleBanner
48
+ v-else-if="error === 'network'"
49
+ type="warning"
50
+ class="flex items-center space-x-2"
51
+ >
52
+ <RiErrorWarningLine class="shink-0 size-6" />
53
+ <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>
54
+ </SimpleBanner>
55
+ <SimpleBanner
56
+ v-else-if="error"
57
+ type="warning"
58
+ class="flex items-center space-x-2"
59
+ >
60
+ <RiErrorWarningLine class="shink-0 size-6" />
61
+ <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>
62
+ </SimpleBanner>
63
+ </div>
64
+ </template>
65
+
66
+ <script setup lang="ts">
67
+ import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
68
+ import { RiErrorWarningLine } from '@remixicon/vue'
69
+ import SimpleBanner from '../SimpleBanner.vue'
70
+ import { useComponentsConfig } from '../../config'
71
+ import type { Resource } from '../../types/resources'
72
+
73
+ const PDF = defineAsyncComponent(() =>
74
+ import('pdf-vue3').then((module) => {
75
+ return module.default
76
+ }),
77
+ )
78
+
79
+ const props = defineProps<{
80
+ resource: Resource
81
+ }>()
82
+
83
+ const config = useComponentsConfig()
84
+
85
+ const pdfData = ref<boolean>(false)
86
+ const loading = ref(false)
87
+ const error = ref<string | null>(null)
88
+ const fileTooLarge = ref(false)
89
+
90
+ const fileSizeBytes = computed(() => {
91
+ // Check if resource has filesize
92
+ if (props.resource.filesize) {
93
+ return props.resource.filesize
94
+ }
95
+
96
+ // Check if resource has content-length in extras (from API metadata)
97
+ const contentLength = props.resource.extras?.['analysis:content-length']
98
+ if (contentLength && typeof contentLength === 'number') {
99
+ return contentLength
100
+ }
101
+
102
+ return null
103
+ })
104
+
105
+ const shouldLoadPdf = computed(() => {
106
+ const size = fileSizeBytes.value
107
+ if (!size) {
108
+ // If we don't know the size, don't risk loading a potentially huge file
109
+ return false
110
+ }
111
+
112
+ // Use maxPdfPreviewSize from config, fallback to 10 MB if not set
113
+ const maxSizeBytes = config.maxPdfPreviewSize ?? 10_000_000
114
+ return size <= maxSizeBytes
115
+ })
116
+
117
+ const loadPdf = async () => {
118
+ // Check if file is too large or size is unknown before loading
119
+ if (!shouldLoadPdf.value) {
120
+ fileTooLarge.value = true
121
+ return
122
+ }
123
+
124
+ loading.value = true
125
+ error.value = null
126
+
127
+ try {
128
+ // Test if the PDF URL is accessible
129
+ const response = await fetch(props.resource.url, { method: 'HEAD' })
130
+ // const response = await fetch('/test-data.pdf') // For testing locally without CORS issues
131
+ if (!response.ok) {
132
+ throw new Error(`HTTP error! status: ${response.status}`)
133
+ }
134
+
135
+ // If the URL is accessible, set pdfData to true
136
+ // The PDF component will handle the actual loading
137
+ pdfData.value = true
138
+ }
139
+ catch (err) {
140
+ console.error('Error testing PDF URL:', err)
141
+
142
+ if (err instanceof TypeError) {
143
+ error.value = 'network'
144
+ }
145
+ else {
146
+ error.value = 'generic'
147
+ }
148
+
149
+ pdfData.value = false
150
+ }
151
+ finally {
152
+ loading.value = false
153
+ }
154
+ }
155
+
156
+ // Event handlers for PDF component
157
+ const handleProgress = (loadRatio: number) => {
158
+ console.log(`PDF loading progress: ${loadRatio}%`)
159
+ }
160
+
161
+ const handleComplete = () => {
162
+ console.log('PDF download completed')
163
+ }
164
+
165
+ const handlePageChange = (page: number) => {
166
+ console.log(`PDF page changed to: ${page}`)
167
+ }
168
+
169
+ const handlePdfInit = (pdf: unknown) => {
170
+ console.log('PDF initialized:', pdf)
171
+ }
172
+
173
+ const handleError = (err: unknown) => {
174
+ console.error('PDF loading error:', err)
175
+
176
+ if (err instanceof TypeError) {
177
+ error.value = 'network'
178
+ }
179
+ else {
180
+ error.value = 'generic'
181
+ }
182
+
183
+ pdfData.value = false
184
+ }
185
+
186
+ onMounted(() => {
187
+ loadPdf()
188
+ })
189
+ </script>