@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,62 @@
1
+ <template>
2
+ <Popover
3
+ v-slot="{ open }"
4
+ class="relative text-gray-title"
5
+ :focus="true"
6
+ >
7
+ <PopoverButton
8
+ v-bind="$attrs"
9
+ ref="button"
10
+ :as="ToggletipButton"
11
+ >
12
+ <slot />
13
+ </PopoverButton>
14
+ <component
15
+ :is="teleportId ? Teleport : 'div'"
16
+ v-if="open"
17
+ :to="`#${teleportId}`"
18
+ :defer="teleportId ? true : undefined"
19
+ >
20
+ <PopoverPanel
21
+ ref="toggletip"
22
+ v-slot="{ close }"
23
+ class="toggletip"
24
+ :class="{
25
+ 'p-0': noMargin,
26
+ 'left-0': position === 'right',
27
+ 'ml-6 top-24': teleportId,
28
+ }"
29
+ >
30
+ <slot
31
+ name="toggletip"
32
+ :close
33
+ />
34
+ </PopoverPanel>
35
+ </component>
36
+ </Popover>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
41
+ import { Teleport } from 'vue'
42
+ import ToggletipButton from './ToggletipButton.vue'
43
+
44
+ withDefaults(defineProps<{
45
+ noMargin?: boolean
46
+ position?: 'left' | 'right'
47
+ teleportId?: string
48
+ }>(), {
49
+ noMargin: false,
50
+ position: 'left',
51
+ })
52
+ defineOptions({ inheritAttrs: false })
53
+ </script>
54
+
55
+ <style scoped>
56
+ .z-10 {
57
+ z-index: 10;
58
+ }
59
+ .left-0 {
60
+ left: 0;
61
+ }
62
+ </style>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <BrandedButton
3
+ color="secondary-softer"
4
+ icon-only
5
+ :icon="RiInformationLine"
6
+ size="xs"
7
+ keep-margins-even-without-borders
8
+ />
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import { RiInformationLine } from '@remixicon/vue'
13
+ import BrandedButton from './BrandedButton.vue'
14
+ </script>
@@ -0,0 +1,103 @@
1
+ import { computed, ref, toValue, type MaybeRefOrGetter } from 'vue'
2
+ import type { CommonKeyboardKeys } from '../types/keyboard'
3
+
4
+ export type Option = {
5
+ id: string
6
+ }
7
+
8
+ export default function useActiveDescendant<T extends Option>(options: MaybeRefOrGetter<Array<T>>, direction: 'horizontal' | 'vertical') {
9
+ const active = ref<string | undefined>()
10
+
11
+ const activeOption = computed<T | undefined>(() => toValue(options).find(option => option.id === active.value))
12
+
13
+ function isActive(id: string | undefined) {
14
+ return active.value === id
15
+ }
16
+
17
+ function activate(id?: string) {
18
+ if (id === undefined) {
19
+ return activateAtPosition(0)
20
+ }
21
+ active.value = id
22
+ }
23
+
24
+ function focusOut() {
25
+ active.value = undefined
26
+ }
27
+
28
+ function activateAtPosition(position: number) {
29
+ const optionsList = toValue(options)
30
+ if (optionsList[position]) {
31
+ activate(optionsList[position].id)
32
+ }
33
+ }
34
+
35
+ function activateNextOption() {
36
+ let activePosition = 0
37
+ if (active.value) {
38
+ activePosition = toValue(options).findIndex(option => option.id === active.value)
39
+ activePosition++
40
+ if (activePosition === toValue(options).length) {
41
+ activePosition = 0
42
+ }
43
+ }
44
+ activateAtPosition(activePosition)
45
+ }
46
+
47
+ function activatePreviousOption() {
48
+ const lastOptionPosition = toValue(options).length - 1
49
+ let activePosition = lastOptionPosition
50
+ if (active.value) {
51
+ activePosition = toValue(options).findIndex(option => option.id === active.value)
52
+ activePosition--
53
+ if (activePosition < 0) {
54
+ activePosition = lastOptionPosition
55
+ }
56
+ }
57
+ activateAtPosition(activePosition)
58
+ }
59
+
60
+ function handleKeyPressForActiveDescendant(key: KeyboardEvent, alreadyMovedDown = false) {
61
+ switch (key.key as CommonKeyboardKeys) {
62
+ case 'ArrowDown':
63
+ if (direction === 'vertical' && !alreadyMovedDown && !key.altKey) {
64
+ activateNextOption()
65
+ }
66
+ break
67
+ case 'ArrowUp':
68
+ if (direction === 'vertical') {
69
+ activatePreviousOption()
70
+ }
71
+ break
72
+ case 'ArrowLeft':
73
+ if (direction === 'horizontal') {
74
+ activatePreviousOption()
75
+ }
76
+ break
77
+ case 'ArrowRight':
78
+ if (direction === 'horizontal' && !alreadyMovedDown && !key.altKey) {
79
+ activateNextOption()
80
+ }
81
+ break
82
+ case 'Home':
83
+ case 'End':
84
+ case 'Escape':
85
+ focusOut()
86
+ break
87
+ }
88
+ }
89
+
90
+ const ALREADY_MOVED_DOWN = true
91
+ const NOT_MOVED_YET = false
92
+
93
+ return {
94
+ activate,
95
+ active,
96
+ activeOption,
97
+ isActive,
98
+ focusOut,
99
+ handleKeyPressForActiveDescendant,
100
+ ALREADY_MOVED_DOWN,
101
+ NOT_MOVED_YET,
102
+ }
103
+ }
@@ -0,0 +1,14 @@
1
+ import { computedAsync } from '@vueuse/core'
2
+ import { toValue, type MaybeRefOrGetter } from 'vue'
3
+ import { fetchReuseTypes, getType } from '../functions/reuses'
4
+
5
+ export function useReuseType(id: MaybeRefOrGetter<string>) {
6
+ const label = computedAsync(async () => {
7
+ const idValue = toValue(id)
8
+ const types = await fetchReuseTypes()
9
+ return getType(types, idValue)
10
+ }, '')
11
+ return {
12
+ label,
13
+ }
14
+ }
package/src/config.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { inject, type Component, type InjectionKey } from 'vue'
2
+ import type { UseFetchFunction } from './functions/api.types'
3
+
4
+ export type PluginConfig = {
5
+ name: string // Name of the application (ex: data.gouv.fr)
6
+ baseUrl: string
7
+ apiBase: string
8
+ devApiKey?: string | null
9
+ staticUrl: string
10
+ datasetQualityGuideUrl?: string
11
+ schemaValidataUrl: string
12
+ schemaDocumentationUrl: string
13
+ tabularApiUrl?: string
14
+ tabularApiPageSize?: number
15
+ tabularAllowRemote?: boolean
16
+ tabularApiDataserviceId?: string
17
+ customUseFetch?: UseFetchFunction | null
18
+ textClamp?: string | Component | null
19
+ appLink?: Component | null
20
+ i18n?: {
21
+ global: {
22
+ mergeLocaleMessage: (locale: string, messages: unknown) => void
23
+ }
24
+ }
25
+ }
26
+
27
+ export const configKey = Symbol() as InjectionKey<PluginConfig>
28
+
29
+ export function useComponentsConfig(): PluginConfig {
30
+ const config = inject(configKey)
31
+ if (!config) throw new Error('Calling `useComponentsConfig` outside @datagouv/components')
32
+ return config
33
+ }
@@ -0,0 +1,96 @@
1
+ import { ref, toValue, watchEffect, type ComputedRef, type Ref } from 'vue'
2
+ import { ofetch } from 'ofetch'
3
+ import { useI18n } from 'vue-i18n'
4
+ import { useComponentsConfig } from '../config'
5
+ import type { AsyncData, AsyncDataExecuteOptions, AsyncDataRequestStatus, UseFetchOptions } from './api.types'
6
+
7
+ export async function useFetch<DataT, ErrorT = never>(
8
+ url: string | Request | Ref<string | Request> | ComputedRef<string | null> | (() => string | Request),
9
+ options?: UseFetchOptions<DataT>,
10
+ ): Promise<AsyncData<DataT, ErrorT>> {
11
+ const config = useComponentsConfig()
12
+
13
+ const { t, locale } = useI18n()
14
+
15
+ if (config.customUseFetch) {
16
+ return await config.customUseFetch(url, options)
17
+ }
18
+
19
+ const data: Ref<DataT | null> = ref(null)
20
+ const error: Ref<ErrorT | null> = ref(null)
21
+ const status = ref<AsyncDataRequestStatus>('idle')
22
+
23
+ const execute = async (opts?: AsyncDataExecuteOptions) => {
24
+ const urlValue = toValue(url)
25
+ if (!urlValue) return
26
+ status.value = 'pending'
27
+ try {
28
+ data.value = await ofetch(urlValue, {
29
+ baseURL: config.apiBase,
30
+ onRequest({ options }) {
31
+ options.headers.set('Content-Type', 'application/json')
32
+ options.headers.set('Accept', 'application/json')
33
+ options.credentials = 'include'
34
+ if (config.devApiKey) {
35
+ options.headers.set('X-API-KEY', config.devApiKey)
36
+ }
37
+
38
+ if (locale.value) {
39
+ if (!options.params) {
40
+ options.params = {}
41
+ }
42
+ options.params['lang'] = locale.value
43
+ }
44
+ },
45
+ async onResponseError({ response }) {
46
+ // TODO redirect to login outside Nuxt?
47
+ // if (response.status === 401) {
48
+ // await nuxtApp.runWithContext(() => navigateTo(localePath('/login')))
49
+ // }
50
+
51
+ let message
52
+ try {
53
+ if ('error' in response._data) {
54
+ message = response._data.error
55
+ }
56
+ else if ('message' in response._data) {
57
+ message = response._data.message
58
+ }
59
+ }
60
+ catch (e) {
61
+ console.error(e)
62
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
63
+ message = t('The API returned an unexpected error')
64
+ }
65
+
66
+ // TODO Toast outside Nuxt
67
+ // toast.error(message)
68
+ },
69
+ ...options,
70
+ })
71
+ status.value = 'success'
72
+ }
73
+ catch (e) {
74
+ error.value = e as ErrorT
75
+ status.value = 'error'
76
+ }
77
+ }
78
+
79
+ watchEffect(async () => {
80
+ await execute()
81
+ })
82
+
83
+ return {
84
+ data,
85
+ refresh: async (opts?: AsyncDataExecuteOptions) => {
86
+ execute()
87
+ },
88
+ execute,
89
+ clear: () => {
90
+ data.value = null
91
+ status.value = 'idle'
92
+ },
93
+ error,
94
+ status,
95
+ }
96
+ }
@@ -0,0 +1,41 @@
1
+ import type { ComputedRef, Ref, WatchSource } from 'vue'
2
+
3
+ export type UseFetchOptions<DataT> = {
4
+ key?: string
5
+ method?: string
6
+ query?: SearchParams
7
+ params?: SearchParams
8
+ body?: RequestInit['body'] | Record<string, any>
9
+ headers?: Record<string, string> | [key: string, value: string][] | Headers
10
+ baseURL?: string
11
+ server?: boolean
12
+ lazy?: boolean
13
+ immediate?: boolean
14
+ getCachedData?: (key: string, nuxtApp: any) => DataT
15
+ deep?: boolean
16
+ dedupe?: 'cancel' | 'defer'
17
+ default?: () => DataT
18
+ transform?: (input: DataT) => DataT | Promise<DataT>
19
+ pick?: string[]
20
+ watch?: WatchSource[] | false
21
+ }
22
+
23
+ export type AsyncData<DataT, ErrorT> = {
24
+ data: Ref<DataT | null>
25
+ refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
26
+ execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
27
+ clear: () => void
28
+ error: Ref<ErrorT | null>
29
+ status: Ref<AsyncDataRequestStatus>
30
+ }
31
+
32
+ export interface AsyncDataExecuteOptions {
33
+ dedupe?: 'cancel' | 'defer'
34
+ }
35
+
36
+ export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
37
+
38
+ export type UseFetchFunction = (<DataT, ErrorT>(
39
+ url: string | Request | Ref<string | Request> | ComputedRef<string | null> | (() => string | Request),
40
+ options?: UseFetchOptions<DataT>
41
+ ) => Promise<AsyncData<DataT, ErrorT>>)
@@ -0,0 +1,12 @@
1
+ import { ref } from 'vue'
2
+
3
+ export type ComponentsConfig = {
4
+ staticUrl: string
5
+ baseApiUrl: string
6
+ }
7
+
8
+ export const config = ref<ComponentsConfig | null>(null)
9
+
10
+ export function setConfig(newConfig: ComponentsConfig) {
11
+ config.value = newConfig
12
+ }
@@ -0,0 +1,24 @@
1
+ import { useComponentsConfig } from '../config'
2
+ import type { Dataset, DatasetV2 } from '../types/datasets'
3
+ import type { CommunityResource, Resource } from '../types/resources'
4
+
5
+ function constructUrl(baseUrl: string, path: string): string {
6
+ const url = new URL(baseUrl)
7
+ url.pathname = `${url.pathname}${path}`
8
+ return url.toString()
9
+ }
10
+
11
+ export default function getDatasetOEmbedHtml(type: string, id: string): string {
12
+ const config = useComponentsConfig()
13
+
14
+ const staticUrl = constructUrl(config.staticUrl, 'oembed.js')
15
+ return `<div data-udata-${type}="${id}"></div><script data-udata="${config.baseUrl}" src="${staticUrl}" async defer></script>`
16
+ }
17
+
18
+ export function isCommunityResource(resource: Resource | CommunityResource): boolean {
19
+ return 'organization' in resource || 'owner' in resource
20
+ }
21
+
22
+ export function getResourceExternalUrl(dataset: Dataset | DatasetV2 | Omit<Dataset, 'resources' | 'community_resources'>, resource: Resource | CommunityResource): string {
23
+ return `${dataset.page}#/${isCommunityResource(resource) ? 'community-resources' : 'resources'}/${resource.id}`
24
+ }
@@ -0,0 +1,85 @@
1
+ import { useI18n } from 'vue-i18n'
2
+
3
+ const SECONDS_IN_A_DAY = 3600 * 24
4
+
5
+ export function formatDate(date: Date | string | null, options: Intl.DateTimeFormatOptions = {}) {
6
+ if (!date) {
7
+ return ''
8
+ }
9
+ date = new Date(date)
10
+ if (!('dateStyle' in options)) {
11
+ options.dateStyle = 'long'
12
+ }
13
+ const { locale } = useI18n()
14
+ return new Intl.DateTimeFormat(locale.value, options).format(date)
15
+ }
16
+
17
+ /**
18
+ * Format date as relative from now.
19
+ * It displays "today" or Intl.RelativeTimeFormat content, based on date.
20
+ */
21
+ export const formatFromNow = (date: Date | string | null) => {
22
+ if (!date) {
23
+ return ''
24
+ }
25
+ const { t, locale } = useI18n()
26
+ if (!('RelativeTimeFormat' in Intl)) {
27
+ return t('on {date}', { date: formatDate(date) })
28
+ }
29
+ const today = new Date()
30
+ today.setHours(0)
31
+ today.setMinutes(0)
32
+ today.setSeconds(0)
33
+ const dateWithoutTime = new Date(date)
34
+ dateWithoutTime.setHours(0)
35
+ dateWithoutTime.setMinutes(0)
36
+ dateWithoutTime.setSeconds(0)
37
+ // Get the diff in second between today and the provided date
38
+ const diff = Math.round((dateWithoutTime.getTime() - today.getTime()) / 1000)
39
+ const units: Array<{ unit: Intl.RelativeTimeFormatUnit, seconds: number, changeAfter: number }> = [
40
+ {
41
+ unit: 'day',
42
+ seconds: SECONDS_IN_A_DAY,
43
+ changeAfter: 30,
44
+ },
45
+ {
46
+ unit: 'month',
47
+ seconds: SECONDS_IN_A_DAY * 30,
48
+ changeAfter: 12,
49
+ },
50
+ {
51
+ unit: 'year',
52
+ seconds: SECONDS_IN_A_DAY * 365,
53
+ changeAfter: Infinity,
54
+ },
55
+ ]
56
+ const correctUnit = units.find((unit) => {
57
+ const diffInUnit = Math.abs(diff / unit.seconds)
58
+ return diffInUnit < unit.changeAfter
59
+ })!
60
+ return new Intl.RelativeTimeFormat(locale.value, { numeric: 'auto' }).format(Math.round(diff / correctUnit?.seconds), correctUnit?.unit)
61
+ }
62
+
63
+ /**
64
+ * Format date relative form now if date is less than a month ago.
65
+ * Otherwise, show a formatted date.
66
+ */
67
+ export const formatRelativeIfRecentDate = (date: Date | string | null, options: Intl.DateTimeFormatOptions = {}) => {
68
+ if (!date) {
69
+ return ''
70
+ }
71
+ const { t } = useI18n()
72
+ const today = new Date()
73
+ today.setHours(0)
74
+ today.setMinutes(0)
75
+ today.setSeconds(0)
76
+ const dateWithoutTime = new Date(date)
77
+ dateWithoutTime.setHours(0)
78
+ dateWithoutTime.setMinutes(0)
79
+ dateWithoutTime.setSeconds(0)
80
+ const diff = Math.abs(dateWithoutTime.getTime() - today.getTime())
81
+ if (Math.round(diff / (SECONDS_IN_A_DAY * 30)) >= 1) {
82
+ return t('on {date}', { date: formatDate(date, options) })
83
+ }
84
+ return formatFromNow(date)
85
+ }
@@ -0,0 +1,38 @@
1
+ import { useI18n } from 'vue-i18n'
2
+
3
+ export const filesize = (val: number) => {
4
+ const { t } = useI18n()
5
+ const suffix = t('B')
6
+ const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']
7
+ for (const unit of units) {
8
+ if (Math.abs(val) < 1024.0) {
9
+ return `${val.toFixed(1)}${unit}${suffix}`
10
+ }
11
+ val /= 1024.0
12
+ }
13
+ return `${val.toFixed(1)}Y${suffix}`
14
+ }
15
+
16
+ export const summarize = (val: number, fractionDigits = 0) => {
17
+ const toFixedIfNotZero = (value: number) => {
18
+ const asString = value.toFixed(fractionDigits)
19
+ if (!asString.includes('.')) {
20
+ return asString
21
+ }
22
+
23
+ // Remove trailing "0" to not show "1.0" but only "1"
24
+ return asString.replace(/0+$/, '').replace(/\.$/, '')
25
+ }
26
+
27
+ if (!val) {
28
+ return '0'
29
+ }
30
+ const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']
31
+ for (const unit of units) {
32
+ if (Math.abs(val) < 1000.0) {
33
+ return `${toFixedIfNotZero(val)}${unit}`
34
+ }
35
+ val /= 1000.0
36
+ }
37
+ return `${toFixedIfNotZero(val)}Y`
38
+ }
@@ -0,0 +1,47 @@
1
+ import markdownit from 'markdown-it'
2
+ import { remark } from 'remark'
3
+ import remarkGfm from 'remark-gfm'
4
+ import strip from 'strip-markdown'
5
+
6
+ const markdownParser = markdownit({
7
+ html: false,
8
+ linkify: true,
9
+ typographer: true,
10
+ breaks: true,
11
+ })
12
+
13
+ // Disable mail linkification
14
+ markdownParser.linkify.add('mailto:', null)
15
+
16
+ markdownParser.use(function (md) {
17
+ md.renderer.rules.link_open = function (tokens, idx, options, _env, self) {
18
+ const link_open = tokens[idx]
19
+ link_open.attrs?.push(['rel', 'ugc nofollow noopener'])
20
+ return self.renderToken(tokens, idx, options)
21
+ }
22
+ // Render ~~<text>~~ as del tag
23
+ md.renderer.rules.s_open = function (tokens, idx, options, _env, self) {
24
+ const s_open = tokens[idx]
25
+ s_open.type = 'del_open'
26
+ s_open.tag = 'del'
27
+ return self.renderToken(tokens, idx, options)
28
+ }
29
+ md.renderer.rules.s_close = function (tokens, idx, options, _env, self) {
30
+ const s_close = tokens[idx]
31
+ s_close.type = 'del_close'
32
+ s_close.tag = 'del'
33
+ return self.renderToken(tokens, idx, options)
34
+ }
35
+ })
36
+
37
+ export function markdown(text: string): string {
38
+ return markdownParser.render(text).trim()
39
+ }
40
+
41
+ export async function removeMarkdown(text: string) {
42
+ const file = await remark()
43
+ .use(remarkGfm)
44
+ .use(strip)
45
+ .process(text)
46
+ return String(file)
47
+ }
@@ -0,0 +1,3 @@
1
+ export function trackEvent(values: Array<unknown>): void {
2
+ globalThis._paq?.push(['trackEvent', ...values])
3
+ }
@@ -0,0 +1,85 @@
1
+ import { useI18n } from 'vue-i18n'
2
+ import type { Component } from 'vue'
3
+ import { RiBankLine, RiBuilding2Line, RiCommunityLine, RiGovernmentLine, RiUserLine } from '@remixicon/vue'
4
+ import type { Organization } from '../types/organizations'
5
+
6
+ export const CERTIFIED = 'certified'
7
+ export const PUBLIC_SERVICE = 'public-service'
8
+ export const ASSOCIATION = 'association'
9
+ export const COMPANY = 'company'
10
+ export const LOCAL_AUTHORITY = 'local-authority'
11
+ export const OTHER = 'other'
12
+ export const USER = 'user'
13
+
14
+ export type OrganizationTypes = typeof PUBLIC_SERVICE | typeof ASSOCIATION | typeof COMPANY | typeof LOCAL_AUTHORITY | typeof OTHER
15
+
16
+ export type UserType = typeof USER
17
+
18
+ export function isType(organization: Organization, type: OrganizationTypes) {
19
+ return hasBadge(organization, type)
20
+ }
21
+
22
+ export function hasBadge(organization: Organization, kind: string) {
23
+ return organization.badges.some(badge => badge.kind === kind)
24
+ }
25
+
26
+ export function getOrganizationTypes(): Array<{ type: OrganizationTypes | UserType, label: string, icon: Component | null }> {
27
+ const { t } = useI18n()
28
+ return [{
29
+ type: PUBLIC_SERVICE,
30
+ label: t('Public service'),
31
+ icon: RiBankLine,
32
+ },
33
+ {
34
+ type: LOCAL_AUTHORITY,
35
+ label: t('Local authority'),
36
+ icon: RiGovernmentLine,
37
+ },
38
+ {
39
+ type: ASSOCIATION,
40
+ label: t('Association'),
41
+ icon: RiCommunityLine,
42
+ },
43
+ {
44
+ type: COMPANY,
45
+ label: t('Company'),
46
+ icon: RiBuilding2Line,
47
+ },
48
+ {
49
+ type: OTHER,
50
+ label: t('Other'),
51
+ icon: null,
52
+ },
53
+ {
54
+ type: USER,
55
+ label: t('User'),
56
+ icon: RiUserLine,
57
+ }]
58
+ }
59
+
60
+ export function findOrganizationType(searched: OrganizationTypes | UserType) {
61
+ return getOrganizationTypes().find(type => type.type === searched)!
62
+ }
63
+
64
+ export function getOrganizationType(organization: Organization): OrganizationTypes {
65
+ if (isType(organization, LOCAL_AUTHORITY)) {
66
+ return LOCAL_AUTHORITY
67
+ }
68
+ else if (isType(organization, PUBLIC_SERVICE)) {
69
+ return PUBLIC_SERVICE
70
+ }
71
+ else if (isType(organization, ASSOCIATION)) {
72
+ return ASSOCIATION
73
+ }
74
+ else if (isType(organization, COMPANY)) {
75
+ return COMPANY
76
+ }
77
+ else {
78
+ return OTHER
79
+ }
80
+ }
81
+
82
+ export function isOrganizationCertified(organization: Organization | null): boolean {
83
+ if (!organization) return false
84
+ return hasBadge(organization, CERTIFIED) && (isType(organization, PUBLIC_SERVICE) || isType(organization, LOCAL_AUTHORITY))
85
+ }
@@ -0,0 +1,11 @@
1
+ import type { Owned } from '../types/owned'
2
+
3
+ export function getOwnerName(owned: Owned): string {
4
+ if (owned.organization) {
5
+ return owned.organization.name
6
+ }
7
+ else if (owned.owner) {
8
+ return `${owned.owner.first_name} ${owned.owner.last_name}`
9
+ }
10
+ return '' // Not supposed to exist but it does...
11
+ }