@datagouv/components-next 0.0.1

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 (119) hide show
  1. package/README.md +150 -0
  2. package/assets/main.css +136 -0
  3. package/assets/placeholders/author.png +0 -0
  4. package/assets/placeholders/dataset.png +0 -0
  5. package/assets/placeholders/news.png +0 -0
  6. package/assets/placeholders/organization.png +0 -0
  7. package/assets/placeholders/reuse.png +0 -0
  8. package/assets/tailwind.config.js +24 -0
  9. package/dist/components.css +2 -0
  10. package/dist/locales/de.js +155 -0
  11. package/dist/locales/en.js +155 -0
  12. package/dist/locales/es.js +155 -0
  13. package/dist/locales/fr.js +155 -0
  14. package/dist/locales/it.js +155 -0
  15. package/dist/locales/pt.js +155 -0
  16. package/dist/locales/sr.js +155 -0
  17. package/package.json +72 -0
  18. package/src/components/AppLink.vue +51 -0
  19. package/src/components/Avatar.vue +27 -0
  20. package/src/components/AvatarWithName.vue +26 -0
  21. package/src/components/BannerAction.vue +39 -0
  22. package/src/components/BrandedButton.vue +170 -0
  23. package/src/components/CopyButton.vue +84 -0
  24. package/src/components/DataserviceCard.vue +184 -0
  25. package/src/components/DatasetCard.vue +198 -0
  26. package/src/components/DatasetInformationPanel.vue +210 -0
  27. package/src/components/DatasetQuality.vue +68 -0
  28. package/src/components/DatasetQualityInline.vue +32 -0
  29. package/src/components/DatasetQualityItem.vue +32 -0
  30. package/src/components/DatasetQualityItemWarning.vue +21 -0
  31. package/src/components/DatasetQualityScore.vue +35 -0
  32. package/src/components/DatasetQualityTooltipContent.vue +79 -0
  33. package/src/components/DescriptionDetails.vue +23 -0
  34. package/src/components/DescriptionList/DescriptionDetails.stories.ts +43 -0
  35. package/src/components/DescriptionList/DescriptionList.stories.ts +47 -0
  36. package/src/components/DescriptionList/DescriptionTerm.stories.ts +28 -0
  37. package/src/components/DescriptionList.vue +8 -0
  38. package/src/components/DescriptionTerm.vue +8 -0
  39. package/src/components/ExtraAccordion.vue +78 -0
  40. package/src/components/Icons/Archive.vue +21 -0
  41. package/src/components/Icons/Code.vue +21 -0
  42. package/src/components/Icons/Documentation.vue +21 -0
  43. package/src/components/Icons/File.vue +21 -0
  44. package/src/components/Icons/Image.vue +7 -0
  45. package/src/components/Icons/Link.vue +21 -0
  46. package/src/components/Icons/Table.vue +21 -0
  47. package/src/components/OrganizationCard.vue +68 -0
  48. package/src/components/OrganizationNameWithCertificate.vue +45 -0
  49. package/src/components/OwnerType.vue +43 -0
  50. package/src/components/OwnerTypeIcon.vue +18 -0
  51. package/src/components/Pagination.vue +205 -0
  52. package/src/components/Placeholder.vue +29 -0
  53. package/src/components/ReadMore.vue +107 -0
  54. package/src/components/ResourceAccordion/DataStructure.vue +87 -0
  55. package/src/components/ResourceAccordion/EditButton.vue +34 -0
  56. package/src/components/ResourceAccordion/Metadata.vue +171 -0
  57. package/src/components/ResourceAccordion/Preview.vue +229 -0
  58. package/src/components/ResourceAccordion/PreviewLoader.vue +148 -0
  59. package/src/components/ResourceAccordion/ResourceAccordion.vue +484 -0
  60. package/src/components/ResourceAccordion/ResourceIcon.vue +16 -0
  61. package/src/components/ResourceAccordion/SchemaBadge.vue +148 -0
  62. package/src/components/ResourceAccordion/SchemaLoader.vue +30 -0
  63. package/src/components/ResourceAccordion/Swagger.vue +46 -0
  64. package/src/components/ResourceAccordion/france.svg +1 -0
  65. package/src/components/ReuseCard.vue +106 -0
  66. package/src/components/ReuseDetails.vue +45 -0
  67. package/src/components/SimpleBanner.vue +24 -0
  68. package/src/components/SmallChart.vue +149 -0
  69. package/src/components/StatBox.vue +100 -0
  70. package/src/components/Tabs/Tab.vue +62 -0
  71. package/src/components/Tabs/TabGroup.vue +20 -0
  72. package/src/components/Tabs/TabList.vue +15 -0
  73. package/src/components/Tabs/TabPanel.vue +7 -0
  74. package/src/components/Tabs/TabPanels.vue +7 -0
  75. package/src/components/Toggletip.vue +62 -0
  76. package/src/components/ToggletipButton.vue +14 -0
  77. package/src/composables/useActiveDescendant.ts +103 -0
  78. package/src/composables/useReuseType.ts +14 -0
  79. package/src/config.ts +33 -0
  80. package/src/functions/api.ts +96 -0
  81. package/src/functions/api.types.ts +41 -0
  82. package/src/functions/config.ts +12 -0
  83. package/src/functions/datasets.ts +24 -0
  84. package/src/functions/dates.ts +85 -0
  85. package/src/functions/helpers.ts +38 -0
  86. package/src/functions/markdown.ts +47 -0
  87. package/src/functions/matomo.ts +3 -0
  88. package/src/functions/organizations.ts +85 -0
  89. package/src/functions/owned.ts +11 -0
  90. package/src/functions/resources.ts +99 -0
  91. package/src/functions/reuses.ts +28 -0
  92. package/src/functions/schemas.ts +96 -0
  93. package/src/functions/tabularApi.ts +27 -0
  94. package/src/functions/users.ts +7 -0
  95. package/src/locales/de.json +154 -0
  96. package/src/locales/en.json +154 -0
  97. package/src/locales/es.json +154 -0
  98. package/src/locales/fr.json +154 -0
  99. package/src/locales/it.json +154 -0
  100. package/src/locales/pt.json +154 -0
  101. package/src/locales/sr.json +154 -0
  102. package/src/main.ts +147 -0
  103. package/src/types/badges.ts +5 -0
  104. package/src/types/contact_point.ts +7 -0
  105. package/src/types/dataservices.ts +68 -0
  106. package/src/types/datasets.ts +80 -0
  107. package/src/types/frequency.ts +6 -0
  108. package/src/types/granularity.ts +6 -0
  109. package/src/types/harvest.ts +3 -0
  110. package/src/types/keyboard.ts +1 -0
  111. package/src/types/licenses.ts +9 -0
  112. package/src/types/organizations.ts +41 -0
  113. package/src/types/owned.ts +9 -0
  114. package/src/types/resources.ts +37 -0
  115. package/src/types/reuses.ts +49 -0
  116. package/src/types/site.ts +23 -0
  117. package/src/types/topics.ts +20 -0
  118. package/src/types/ui.ts +3 -0
  119. package/src/types/users.ts +10 -0
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <component
3
+ :is="config?.appLink"
4
+ v-if="config.appLink"
5
+ :to
6
+ >
7
+ <slot />
8
+ </component>
9
+ <a
10
+ v-else-if="isExternal"
11
+ :href="to"
12
+ >
13
+ <slot />
14
+ </a>
15
+ <RouterLink
16
+ v-else
17
+ :to
18
+ ><slot /></RouterLink>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { computed } from 'vue'
23
+ import { RouterLink } from 'vue-router'
24
+ import { useI18n } from 'vue-i18n'
25
+ import type { RouteLocationRaw } from 'vue-router'
26
+ import { useComponentsConfig } from '../config'
27
+
28
+ const config = useComponentsConfig()
29
+ const { locale } = useI18n()
30
+
31
+ const props = defineProps<{
32
+ to: string | RouteLocationRaw
33
+ }>()
34
+
35
+ const isExternal = computed(() => {
36
+ if (typeof props.to !== 'string') return false
37
+ return props.to && props.to.startsWith('http')
38
+ })
39
+ const to = computed(() => {
40
+ // If the `appLink` component is override, the override is responsible of the locale management
41
+ if (config.appLink) return props.to
42
+
43
+ // If it's an external link, no locale management needed.
44
+ if (isExternal.value) return props.to
45
+
46
+ // TODO harden this for path not starting with "/"x
47
+ if (typeof props.to === 'string') return `/${locale.value}${props.to}`
48
+
49
+ return props.to
50
+ })
51
+ </script>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <img
3
+ :class="{ 'rounded-full border border-gray-default': rounded }"
4
+ :src="avatarUrl"
5
+ :width="size"
6
+ :height="size"
7
+ loading="lazy"
8
+ alt=""
9
+ >
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { computed } from 'vue'
14
+ import { getUserAvatar } from '../functions/users'
15
+ import type { User } from '../types/users'
16
+
17
+ type Props = {
18
+ rounded?: boolean
19
+ size?: number
20
+ user: User
21
+ }
22
+ const props = withDefaults(defineProps<Props>(), {
23
+ rounded: false,
24
+ size: 40,
25
+ })
26
+ const avatarUrl = computed(() => getUserAvatar(props.user, props.size))
27
+ </script>
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <span class="inline-flex items-center space-x-1">
3
+ <Avatar
4
+ v-bind="$attrs"
5
+ :user="user"
6
+ :size
7
+ :rounded="true"
8
+ />
9
+ <span class="fr-text--bold">
10
+ {{ user.first_name }}
11
+ {{ user.last_name }}
12
+ </span>
13
+ </span>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import type { User } from '../types/users'
18
+ import Avatar from './Avatar.vue'
19
+
20
+ withDefaults(defineProps<{
21
+ user: User
22
+ size?: number
23
+ }>(), {
24
+ size: 12,
25
+ })
26
+ </script>
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <div
3
+ class="group flex items-center justify-between rounded border p-2.5"
4
+ :data-banner-action-type="type"
5
+ :class="{
6
+ '!border-datagouv bg-datagouv-lightest text-datagouv-dark': type === 'primary',
7
+ '!border-danger-dark bg-danger-lightest text-danger-dark': type === 'danger',
8
+ '!border-warning-dark bg-warning-lightest text-warning-dark': type === 'warning',
9
+ }"
10
+ >
11
+ <div>
12
+ <div class="text-gray-900">
13
+ {{ title }}
14
+ </div>
15
+ <div
16
+ v-if="$slots.default"
17
+ class="text-xs mt-1"
18
+ >
19
+ <slot />
20
+ </div>
21
+ </div>
22
+ <div v-if="$slots.button">
23
+ <slot name="button" />
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script lang="ts">
29
+ export const bannerActionTypeKey = Symbol() as InjectionKey<'primary' | 'danger' | 'warning'>
30
+ </script>
31
+
32
+ <script setup lang="ts">
33
+ const props = defineProps<{
34
+ type: 'primary' | 'danger' | 'warning'
35
+ title: string
36
+ }>()
37
+
38
+ provide(bannerActionTypeKey, props.type)
39
+ </script>
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <component
3
+ :is="href ? AppLink: 'button'"
4
+ class="inline-flex items-center rounded-full font-medium border !bg-none !no-underline after:content-none"
5
+ :class="[colors, sizes, removePaddingsIfNoBorders, isDisabled ? '!opacity-50' : '', iconRight && !newTab ? 'flex-row-reverse space-x-reverse' : '']"
6
+ :disabled="isDisabled"
7
+ :aria-disabled="isDisabled"
8
+ :role="href ? 'link' : ''"
9
+ :to="isDisabled ? undefined : href"
10
+ :target="newTab ? '_blank' : undefined"
11
+ :type
12
+ >
13
+ <AdminLoader
14
+ v-if="loading"
15
+ size="16"
16
+ :color="color === 'primary' ? 'white' : 'primary'"
17
+ />
18
+ <component
19
+ :is="icon"
20
+ v-else-if="icon"
21
+ :class="iconSize"
22
+ v-bind="iconAttrs"
23
+ />
24
+ <span
25
+ v-if="hasText"
26
+ class="whitespace-nowrap"
27
+ :class="iconOnly ? 'sr-only' : ''"
28
+ ><slot /></span>
29
+ <RiExternalLinkLine
30
+ v-if="newTab"
31
+ :class="iconSize"
32
+ />
33
+ </component>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import type {
38
+ Component,
39
+ Slot,
40
+ VNode,
41
+ } from 'vue'
42
+ import {
43
+ Comment,
44
+ computed,
45
+ inject,
46
+ Text,
47
+ useSlots,
48
+ } from 'vue'
49
+ import { RiExternalLinkLine } from '@remixicon/vue'
50
+ import AppLink from './AppLink.vue'
51
+ import { bannerActionTypeKey } from './BannerAction.vue'
52
+
53
+ type ColorType = 'primary' | 'primary-soft' | 'primary-softer' | 'secondary' | 'secondary-softer' | 'warning' | 'danger' | 'tertiary'
54
+
55
+ const props = withDefaults(defineProps<{
56
+ size?: '2xs' | 'xs' | 'sm' | 'lg'
57
+ color?: ColorType
58
+ disabled?: boolean
59
+ loading?: boolean
60
+ icon?: Component
61
+ iconAttrs?: Record<string, string>
62
+ href?: string
63
+ newTab?: boolean
64
+ iconOnly?: boolean
65
+ iconRight?: boolean
66
+ keepMarginsEvenWithoutBorders?: boolean
67
+ type?: 'submit' | 'button'
68
+ }>(), {
69
+ newTab: false,
70
+ iconOnly: false,
71
+ iconRight: false,
72
+ keepMarginsEvenWithoutBorders: false,
73
+ })
74
+
75
+ const slots = useSlots()
76
+
77
+ const type = computed(() => {
78
+ if (props.type) return props.type
79
+ if (props.href) return undefined
80
+
81
+ return 'button'
82
+ })
83
+
84
+ const size = computed(() => {
85
+ if (props.size) return props.size
86
+ if (bannerActionType) return 'xs'
87
+ return 'sm'
88
+ })
89
+
90
+ const color = computed<ColorType>(() => {
91
+ if (props.color) return props.color
92
+ if (bannerActionType) {
93
+ return {
94
+ primary: 'primary-soft' as ColorType,
95
+ warning: 'warning' as ColorType,
96
+ danger: 'danger' as ColorType,
97
+ }[bannerActionType]
98
+ }
99
+ return 'primary'
100
+ })
101
+
102
+ const hasText = computed(() => {
103
+ return hasSlotContent(slots.default) && !props.iconOnly
104
+ })
105
+ const bannerActionType = inject(bannerActionTypeKey, null)
106
+
107
+ const isDisabled = computed(() => props.disabled || props.loading)
108
+
109
+ const colors = computed(() => {
110
+ return {
111
+ 'primary': `text-white bg-datagouv-dark !border-datagouv-dark ${!isDisabled.value ? 'hover:!bg-datagouv-hover hover:!border-datagouv-hover' : ''}`,
112
+ 'primary-soft': `text-datagouv-dark bg-transparent !border-datagouv-dark ${!isDisabled.value ? '[&&]:hover:!bg-gray-some' : ''}`,
113
+ 'primary-softer': `text-datagouv-dark bg-transparent !border-transparent ${!isDisabled.value ? '[&&]:hover:!bg-gray-some' : ''}`,
114
+ 'secondary': `text-gray-plain bg-white !border-gray-plain ${!isDisabled.value ? '[&&]:hover:!bg-gray-some' : ''}`,
115
+ 'secondary-softer': `text-gray-plain !border-transparent ${!isDisabled.value ? '[&&]:hover:!bg-gray-some' : ''}`,
116
+ 'warning': `text-warning-dark bg-white !border-warning-dark ${!isDisabled.value ? '[&&]:hover:!bg-gray-some' : ''}`,
117
+ 'danger': `!text-danger-dark bg-white !border-danger-dark ${!isDisabled.value ? '[&&]:hover:!bg-gray-some' : ''}`,
118
+ 'tertiary': `!border-none bg-transparent text-datagouv-dark ${!isDisabled.value ? '[&&]:hover:!bg-gray-some' : ''}`,
119
+ }[color.value]
120
+ })
121
+
122
+ const sizes = computed(() => {
123
+ return {
124
+ 'lg': `text-lg ${hasText.value ? 'px-4 py-2 space-x-2' : 'p-3'}`,
125
+ 'sm': `text-sm leading-none ${hasText.value ? 'px-4 py-3 space-x-1' : 'p-2.5'}`,
126
+ 'xs': `text-xs leading-[0.875rem] ${hasText.value ? 'px-4 py-2 space-x-1' : 'p-2'}`,
127
+ '2xs': `text-xs leading-[0.875rem] p-1 space-x-1`,
128
+ }[size.value]
129
+ })
130
+
131
+ const hasBorders = computed(() => {
132
+ return props.color !== 'primary-softer' && props.color !== 'secondary-softer'
133
+ })
134
+
135
+ const removePaddingsIfNoBorders = computed(() => {
136
+ if (hasBorders.value) return ''
137
+ if (props.keepMarginsEvenWithoutBorders) return ''
138
+
139
+ return {
140
+ 'lg': hasText.value ? '-mx-6' : '-mx-3',
141
+ 'sm': hasText.value ? '-mx-4' : '-mx-2.5',
142
+ 'xs': hasText.value ? '-mx-4' : '-mx-2',
143
+ '2xs': '-m-1',
144
+ }[size.value]
145
+ })
146
+
147
+ const iconSize = computed(() => {
148
+ return {
149
+ 'lg': (hasBorders.value || hasText.value) ? 'size-6' : 'size-8',
150
+ 'sm': (hasBorders.value || hasText.value) ? 'size-4' : 'size-6',
151
+ 'xs': (hasBorders.value || hasText.value) ? 'size-3' : 'size-5',
152
+ '2xs': (hasBorders.value || hasText.value) ? 'size-3' : 'size-4',
153
+ }[size.value]
154
+ })
155
+
156
+ function hasSlotContent(slot: Slot | undefined, slotProps = {}): boolean {
157
+ if (!slot) return false
158
+
159
+ return slot(slotProps).some((vnode: VNode) => {
160
+ if (vnode.type === Comment) return false
161
+
162
+ if (Array.isArray(vnode.children) && !vnode.children.length) return false
163
+
164
+ return (
165
+ vnode.type !== Text
166
+ || (typeof vnode.children === 'string' && vnode.children.trim() !== '')
167
+ )
168
+ })
169
+ }
170
+ </script>
@@ -0,0 +1,84 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ class="text-sm mb-0 whitespace-nowrap relative p-1 text-gray-medium leading-none"
5
+ :class="{ 'border bg-white rounded-sm text-gray-title': hideLabel }"
6
+ @click="copy"
7
+ >
8
+ <span
9
+ v-if="copied"
10
+ class="flex items-center"
11
+ :class="{ 'flex-row-reverse': reverse }"
12
+ style="color: #3558a2;"
13
+ >
14
+ <RiCheckLine class="size-4 inline" />
15
+ <span
16
+ class="fr-ml-1v copy-label"
17
+ :class="{ 'fr-sr-only': hideLabel }"
18
+ >{{ copiedLabel }}</span>
19
+ </span>
20
+ <span
21
+ v-if="!copied"
22
+ class="flex items-center"
23
+ :class="{ 'flex-row-reverse': reverse }"
24
+ >
25
+ <component
26
+ :is="hideLabel ? RiClipboardLine : RiFileCopyLine"
27
+ class="size-4 inline"
28
+ />
29
+ <span
30
+ class="fr-ml-1v copy-link copy-label"
31
+ :class="{ 'fr-sr-only': hideLabel }"
32
+ >{{ label }}</span>
33
+ </span>
34
+ </button>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ import { RiCheckLine, RiClipboardLine, RiFileCopyLine } from '@remixicon/vue'
39
+ import { ref } from 'vue'
40
+
41
+ const props = withDefaults(defineProps<{
42
+ text: string
43
+ label: string
44
+ copiedLabel: string
45
+ hideLabel?: boolean
46
+ reverse?: boolean
47
+ }>(), {
48
+ hideLabel: false,
49
+ reverse: false,
50
+ })
51
+
52
+ const copied = ref(false)
53
+
54
+ const copy = () => {
55
+ navigator.clipboard.writeText(props.text)
56
+ copied.value = true
57
+ setTimeout(() => copied.value = false, 2000)
58
+ }
59
+ </script>
60
+
61
+ <style scoped>
62
+ button:hover .copy-icon {
63
+ color: #3558a2;
64
+ }
65
+
66
+ .copy-link {
67
+ /* Using opacity here to prevent moving object with display:none (for example when clamping a text before the button) */
68
+ opacity: 0%;
69
+ }
70
+ button:hover .copy-link {
71
+ opacity: 100%;
72
+ }
73
+
74
+ /*
75
+ We may want to enable this to leave more space for title in small screens… But it also affects
76
+ buttons in the body of the resources panels and we don't want it…
77
+ @container (max-width: 600px) {
78
+ .copy-label {
79
+ position: absolute;
80
+ bottom: -1rem;
81
+ left: 0;
82
+ }
83
+ } */
84
+ </style>
@@ -0,0 +1,184 @@
1
+ <template>
2
+ <article
3
+ class="my-4 p-4 border border-gray-default fr-enlarge-link"
4
+ :class="{
5
+ 'border-tabular-api': isTabularApi,
6
+ 'mt-6': showBadge,
7
+ }"
8
+ >
9
+ <div
10
+ v-if="showBadge"
11
+ class="absolute top-0 fr-grid-row fr-grid-row--middle fr-mt-n3v fr-ml-n1v"
12
+ >
13
+ <p
14
+ v-if="dataservice.access_type === 'restricted'"
15
+ class="fr-badge fr-badge--sm fr-badge--mention-grey text-gray-medium mr-2"
16
+ >
17
+ <span
18
+ class="fr-icon-lock-line fr-icon--sm"
19
+ aria-hidden="true"
20
+ />
21
+ {{ t('Restricted access') }}
22
+ </p>
23
+ <p
24
+ v-if="dataservice.private"
25
+ class="fr-badge fr-badge--sm fr-badge--mention-grey text-gray-medium mr-2"
26
+ >
27
+ <span
28
+ class="fr-icon-lock-line fr-icon--sm"
29
+ aria-hidden="true"
30
+ />
31
+ {{ t('Draft') }}
32
+ </p>
33
+ <p
34
+ v-if="dataservice.archived_at"
35
+ class="fr-badge fr-badge--sm fr-badge--mention-grey text-gray-medium mr-2"
36
+ >
37
+ <span
38
+ class="fr-icon-lock-line fr-icon--sm"
39
+ aria-hidden="true"
40
+ />
41
+ {{ t('Archived') }}
42
+ </p>
43
+ </div>
44
+ <h4 class="fr-text--md fr-mb-0">
45
+ <slot
46
+ name="dataserviceUrl"
47
+ :dataservice="dataservice"
48
+ :dataservice-url="dataserviceUrl"
49
+ >
50
+ <svg
51
+ v-if="isTabularApi"
52
+ xmlns="http://www.w3.org/2000/svg"
53
+ viewBox="0 0 24 24"
54
+ fill="currentColor"
55
+ aria-hidden="true"
56
+ class="fr-mr-1v"
57
+ style="width:1rem"
58
+ ><path d="M17.0007 1.20825 18.3195 3.68108 20.7923 4.99992 18.3195 6.31876 17.0007 8.79159 15.6818 6.31876 13.209 4.99992 15.6818 3.68108 17.0007 1.20825ZM10.6673 9.33325 15.6673 11.9999 10.6673 14.6666 8.00065 19.6666 5.33398 14.6666.333984 11.9999 5.33398 9.33325 8.00065 4.33325 10.6673 9.33325ZM11.4173 11.9999 9.18905 10.8115 8.00065 8.58325 6.81224 10.8115 4.58398 11.9999 6.81224 13.1883 8.00065 15.4166 9.18905 13.1883 11.4173 11.9999ZM19.6673 16.3333 18.0007 13.2083 16.334 16.3333 13.209 17.9999 16.334 19.6666 18.0007 22.7916 19.6673 19.6666 22.7923 17.9999 19.6673 16.3333Z" /></svg>
59
+ <span
60
+ v-else
61
+ class="fr-icon-terminal-line fr-icon--sm fr-mr-1v"
62
+ aria-hidden="true"
63
+ />
64
+ <AppLink
65
+ class="inline-flex"
66
+ :to="dataserviceUrl"
67
+ >
68
+ <component
69
+ :is="config.textClamp"
70
+ v-if="config.textClamp"
71
+ :auto-resize="true"
72
+ :text="dataservice.title"
73
+ :max-lines="1"
74
+ />
75
+ </AppLink>
76
+ </slot>
77
+ </h4>
78
+ <p
79
+ v-if="dataservice.organization || dataservice.owner"
80
+ class="text-sm text-gray-medium inline-flex mt-1 mb-0"
81
+ >
82
+ <span
83
+ v-if="dataservice.organization"
84
+ class="not-enlarged"
85
+ >
86
+ <AppLink
87
+ v-if="organizationUrl"
88
+ class="fr-link fr-text--sm"
89
+ :to="organizationUrl"
90
+ >
91
+ <OrganizationNameWithCertificate :organization="dataservice.organization" />
92
+ </AppLink>
93
+ <OrganizationNameWithCertificate
94
+ v-else
95
+ :organization="dataservice.organization"
96
+ />
97
+ </span>
98
+ <component
99
+ :is="config.textClamp"
100
+ v-else-if="config.textClamp"
101
+ class="not-enlarged fr-mr-1v"
102
+ :auto-resize="true"
103
+ :text="ownerName"
104
+ :max-lines="1"
105
+ />
106
+ <span class="dash-before whitespace-nowrap">{{ t('Updated {date}', { date: formatRelativeIfRecentDate(dataservice.metadata_modified_at, { dateStyle: 'medium' }) }) }}</span>
107
+ </p>
108
+ <p class="text-sm text-gray-medium mb-0 mt-1">
109
+ <span class="fr-icon-information-line fr-icon--sm text-gray-medium" />
110
+ {{ t('Availability :') }}
111
+ <span class="text-gray-plain">
112
+ <template v-if="dataservice.availability">
113
+ {{ t('{n}%', dataservice.availability) }}
114
+ </template>
115
+ <template v-else>
116
+ {{ t('unknown') }}
117
+ </template>
118
+ </span>
119
+ </p>
120
+ <component
121
+ :is="config.textClamp"
122
+ v-if="config.textClamp && description && showDescription"
123
+ class="fr-text--sm fr-mt-1w fr-mb-0 overflow-wrap-anywhere"
124
+ :auto-resize="true"
125
+ :text="description"
126
+ :max-lines="2"
127
+ />
128
+ </article>
129
+ </template>
130
+
131
+ <script setup lang="ts">
132
+ import { computed, ref, watchEffect } from 'vue'
133
+ import { useI18n } from 'vue-i18n'
134
+ import type { RouteLocationRaw } from 'vue-router'
135
+ import { useComponentsConfig } from '../config'
136
+ import { formatRelativeIfRecentDate } from '../functions/dates'
137
+ import { removeMarkdown } from '../functions/markdown'
138
+ import { getOwnerName } from '../functions/owned'
139
+ import type { Dataservice } from '../types/dataservices'
140
+ import OrganizationNameWithCertificate from './OrganizationNameWithCertificate.vue'
141
+ import AppLink from './AppLink.vue'
142
+
143
+ type Props = {
144
+ dataservice: Dataservice
145
+
146
+ /**
147
+ * The dataserviceUrl is a route location object to allow Vue Router to navigate to the details of a dataservice.
148
+ * It is used as a separate prop to allow other sites using the package to define their own dataservice pages.
149
+ */
150
+ dataserviceUrl: RouteLocationRaw
151
+
152
+ /**
153
+ * The organizationUrl is an optional route location object to allow Vue Router to navigate to the details of the organization linked to tha dataservice.
154
+ * It is used as a separate prop to allow other sites using the package to define their own organization pages.
155
+ */
156
+ organizationUrl?: RouteLocationRaw
157
+ showDescription?: boolean
158
+ }
159
+
160
+ const props = withDefaults(defineProps<Props>(), {
161
+ style: () => ({}),
162
+ showDescription: true,
163
+ })
164
+
165
+ const { t } = useI18n()
166
+ const ownerName = computed(() => getOwnerName(props.dataservice))
167
+ const showBadge = computed(() => props.dataservice.access_type === 'restricted' || props.dataservice.private || props.dataservice.archived_at)
168
+
169
+ const config = useComponentsConfig()
170
+ const isTabularApi = computed(() => {
171
+ return config.tabularApiDataserviceId === props.dataservice.id
172
+ })
173
+
174
+ const description = ref('')
175
+ watchEffect(async () => {
176
+ description.value = await removeMarkdown(props.dataservice.description)
177
+ })
178
+ </script>
179
+
180
+ <style scoped>
181
+ .border-tabular-api {
182
+ border-color: #373C42 !important;
183
+ }
184
+ </style>