@open-commune/web 0.1.3

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 (132) hide show
  1. package/README.md +41 -0
  2. package/app.config.ts +10 -0
  3. package/app.vue +13 -0
  4. package/assets/css/main.css +13 -0
  5. package/components/data/display/row/data-avatar.vue +23 -0
  6. package/components/data/display/row/data-badge.vue +34 -0
  7. package/components/data/display/row/data-color.vue +17 -0
  8. package/components/data/display/row/data-display-badge-publish-status.vue +29 -0
  9. package/components/data/display/row/data-label.vue +37 -0
  10. package/components/data/display/row/data-last-update.vue +45 -0
  11. package/components/data/display/row/data-spacer.vue +4 -0
  12. package/components/data/display/row/data-summary.vue +44 -0
  13. package/components/data/display/row/data-text.vue +14 -0
  14. package/components/data/display/row/data-toggle.vue +16 -0
  15. package/components/data/display/row.vue +111 -0
  16. package/components/data/lists/data-list.vue +85 -0
  17. package/components/data/lists/data-lists-table.vue +155 -0
  18. package/components/elements/avatars/avatar-circular.vue +68 -0
  19. package/components/elements/avatars/avatar-with-text.vue +47 -0
  20. package/components/elements/badges/badge.vue +85 -0
  21. package/components/elements/buttons/primary.vue +32 -0
  22. package/components/elements/buttons/secondary.vue +35 -0
  23. package/components/elements/buttons/tertiary.vue +25 -0
  24. package/components/elements/dropdowns/dots.vue +75 -0
  25. package/components/elements/media/image.vue +82 -0
  26. package/components/elements/notifications/default.vue +90 -0
  27. package/components/forms/handlers/handler-collections.vue +189 -0
  28. package/components/forms/handlers/handler-publication-status.vue +193 -0
  29. package/components/forms/handlers/handler-records.vue +138 -0
  30. package/components/forms/handlers/handler-roles.vue +176 -0
  31. package/components/forms/handlers/handler-schedules.vue +218 -0
  32. package/components/forms/handlers/handler-steps.vue +147 -0
  33. package/components/forms/handlers/handler-tokens.vue +79 -0
  34. package/components/forms/inputs/input-color-picker.vue +159 -0
  35. package/components/forms/inputs/input-editor.vue +363 -0
  36. package/components/forms/inputs/input-files-media.vue +150 -0
  37. package/components/forms/inputs/input-files.vue +98 -0
  38. package/components/forms/inputs/input-json.vue +65 -0
  39. package/components/forms/inputs/input-location.vue +179 -0
  40. package/components/forms/inputs/input-text-area.vue +65 -0
  41. package/components/forms/inputs/input-text.vue +122 -0
  42. package/components/forms/inputs/input-toggle.vue +125 -0
  43. package/components/forms/inputs/media-section/media-editor.vue +95 -0
  44. package/components/forms/layouts/form.vue +203 -0
  45. package/components/forms/layouts/layout.vue +47 -0
  46. package/components/forms/layouts/modal.vue +96 -0
  47. package/components/forms/layouts/navigation.vue +48 -0
  48. package/components/forms/layouts/section.vue +118 -0
  49. package/components/forms/selects/select-collection-enum.vue +195 -0
  50. package/components/forms/selects/select-date.vue +245 -0
  51. package/components/forms/selects/select-enum.vue +186 -0
  52. package/components/forms/selects/select-record.vue +274 -0
  53. package/components/layout/headings/headings-collection.vue +29 -0
  54. package/components/layout/headings/headings-record.vue +33 -0
  55. package/components/layout/navigations/primary/dropdown-publish.vue +147 -0
  56. package/components/layout/navigations/primary.vue +132 -0
  57. package/components/layout/notifications/notifications-list.vue +19 -0
  58. package/composables/core/domains/use-definitions.ts +57 -0
  59. package/composables/core/domains/use-entries.ts +62 -0
  60. package/composables/core/domains/use-media.ts +82 -0
  61. package/composables/core/use-core.ts +12 -0
  62. package/composables/use-ability.js +38 -0
  63. package/composables/use-models.js +27 -0
  64. package/composables/use-session.ts +80 -0
  65. package/composables/use-timer.js +62 -0
  66. package/composables/use-trpc.js +17 -0
  67. package/editor/index.ts +44 -0
  68. package/editor/views/CalloutView.vue +22 -0
  69. package/editor/views/DetailsContentView.vue +11 -0
  70. package/editor/views/DetailsSummaryView.vue +11 -0
  71. package/editor/views/DetailsView.vue +72 -0
  72. package/editor/views/EmbedView.vue +53 -0
  73. package/editor/views/FileView.vue +79 -0
  74. package/editor/views/ImageView.vue +56 -0
  75. package/editor/views/UploadView.vue +71 -0
  76. package/layouts/default.vue +21 -0
  77. package/layouts/entrance.vue +10 -0
  78. package/layouts/record.vue +21 -0
  79. package/middleware/00.auth.global.js +16 -0
  80. package/middleware/01.instance.global.js +21 -0
  81. package/middleware/03.verify-permissions.global.js +11 -0
  82. package/middleware/04.prefetch-page.global.js +18 -0
  83. package/middleware/05.headings-page.global.js +25 -0
  84. package/models/_commun.js +243 -0
  85. package/models/_factory.js +34 -0
  86. package/models/collection.js +87 -0
  87. package/models/entrance.js +42 -0
  88. package/models/media.js +82 -0
  89. package/models/organization.js +92 -0
  90. package/models/record.js +165 -0
  91. package/models/role.js +66 -0
  92. package/models/token.js +56 -0
  93. package/models/user.js +97 -0
  94. package/nuxt.config.ts +151 -0
  95. package/package.json +71 -0
  96. package/pages/[collection]/[record]/index.vue +88 -0
  97. package/pages/[collection]/index.vue +55 -0
  98. package/pages/_lab/editor.vue +149 -0
  99. package/pages/auth/callback.vue +31 -0
  100. package/pages/index/index.vue +85 -0
  101. package/pages/index/password/define/[token].vue +79 -0
  102. package/pages/index/password/recover/index.vue +72 -0
  103. package/pages/index/welcome/[token].vue +90 -0
  104. package/pages/index.vue +44 -0
  105. package/pages/management/index.vue +15 -0
  106. package/pages/management/members.vue +176 -0
  107. package/pages/management/organizations.vue +192 -0
  108. package/pages/management/roles.vue +183 -0
  109. package/pages/overview.vue +15 -0
  110. package/pages/settings.vue +197 -0
  111. package/plugins/01.session.client.ts +5 -0
  112. package/plugins/02.vue-query.ts +25 -0
  113. package/public/favicon.ico +0 -0
  114. package/public/logo-black.svg +1 -0
  115. package/public/logo-white.svg +1 -0
  116. package/server/api/trpc/[...trpc].ts +28 -0
  117. package/server/api/v1/content/deployment.get.ts +45 -0
  118. package/server/api/v1/content/records.get.ts +12 -0
  119. package/server/middleware/0.security.ts +19 -0
  120. package/server/middleware/1.cors.ts +31 -0
  121. package/server/middleware/2.session.ts +20 -0
  122. package/server/middleware/3.api-token.ts +20 -0
  123. package/server/plugins/core.ts +8 -0
  124. package/server/routes/_tasks/[name].post.ts +20 -0
  125. package/server/routes/health.get.ts +6 -0
  126. package/server/tasks/deploy.ts +26 -0
  127. package/server/types.d.ts +8 -0
  128. package/server/utils/core.ts +18 -0
  129. package/store/layout/headings.js +21 -0
  130. package/store/layout/modal.js +80 -0
  131. package/store/layout/notifications.js +52 -0
  132. package/store/layout/workspace.js +80 -0
@@ -0,0 +1,155 @@
1
+ <template>
2
+ <div class="w-full">
3
+ <table class="table-auto w-full h-12">
4
+ <thead
5
+ v-if="displayHeader"
6
+ class="bg-gray-50 border border-gray-200 rounded-md"
7
+ >
8
+ <tr>
9
+ <th
10
+ v-for="({ title, property, alignRight = false }) in headers"
11
+ :key="property"
12
+ scope="col"
13
+ class="px-4 py-4 text-left text-xs font-light text-gray-500 uppercase tracking-wider"
14
+ :class="alignRight ? 'text-right' : 'text-left'"
15
+ >
16
+ {{ title }}
17
+ </th>
18
+ </tr>
19
+ </thead>
20
+ <tbody class="bg-white divide-y divide-gray-200">
21
+ <tr
22
+ v-for="(item, i) in items"
23
+ :key="i"
24
+ class="hover:bg-gray-50"
25
+ >
26
+ <td
27
+ v-for="({ property, component, componentOptions, nestedValue = true, alignRight = false }) in headers"
28
+ :key="property"
29
+ :class="alignRight ? 'text-right' : 'text-left'"
30
+ class="flex-row items-center px-4 py-3"
31
+ >
32
+ <data-badge
33
+ v-if="component === 'data-badge'"
34
+ v-bind="componentOptions"
35
+ :value="nestedValue ? item[property] : item"
36
+ @click="$emitters('update', item)"
37
+ />
38
+ <data-label
39
+ v-if="component === 'data-label'"
40
+ v-bind="componentOptions"
41
+ :value="nestedValue ? item[property] : item"
42
+ @click="$emitters('update', item)"
43
+ />
44
+ <data-last-update
45
+ v-if="component === 'data-last-update'"
46
+ v-bind="componentOptions"
47
+ :value="nestedValue ? item[property] : item"
48
+ @click="$emitters('update', item)"
49
+ />
50
+ <data-text
51
+ v-if="component === 'data-text'"
52
+ v-bind="componentOptions"
53
+ :value="nestedValue ? item[property] : item"
54
+ @click="$emitters('update', item)"
55
+ />
56
+ <!-- <data-color
57
+ v-if="component === 'data-color'"
58
+ v-bind="componentOptions"
59
+ :value="nestedValue ? item[property] : item"
60
+ class="hidden lg:block"
61
+ @click="$emitters('update', item)"
62
+ />
63
+ -->
64
+ <data-summary
65
+ v-if="component === 'data-summary'"
66
+ v-bind="componentOptions"
67
+ :value="nestedValue ? item[property] : item"
68
+ @click="$emitters('update', item)"
69
+ />
70
+ <data-toggle
71
+ v-if="component === 'data-toggle'"
72
+ v-bind="componentOptions"
73
+ :value="nestedValue ? item[property] : item"
74
+ @change="$emit(property, { ...item, [property]: $event })"
75
+ />
76
+ <dropdown-dots
77
+ v-else-if="component === 'actions' && enabledActions.length"
78
+ :options="enabledActions"
79
+ @action="$emitters($event, item)"
80
+ />
81
+ </td>
82
+ </tr>
83
+ </tbody>
84
+ </table>
85
+ <button-secondary
86
+ v-if="displayPaginationButton"
87
+ class="w-full mt-2"
88
+ label="Plus de résultats"
89
+ @click="_displayMoreItem"
90
+ />
91
+ </div>
92
+ </template>
93
+
94
+ <script setup>
95
+ import dataBadge from '~/components/data/display/row/data-badge'
96
+ // import dataColor from '~/components/data/display/row/data-color'
97
+ import dropdownDots from '~/components/elements/dropdowns/dots'
98
+ import dataLabel from '~/components/data/display/row/data-label'
99
+ import dataLastUpdate from '~/components/data/display/row/data-last-update'
100
+ import dataSummary from '~/components/data/display/row/data-summary'
101
+ import dataText from '~/components/data/display/row/data-text'
102
+ import dataToggle from '~/components/data/display/row/data-toggle'
103
+ import buttonSecondary from '~/components/elements/buttons/secondary'
104
+
105
+ // Prepare constants
106
+ const DOTS_OPTIONS = [
107
+ { label: 'Modifier', action: 'update' },
108
+ { label: 'Supprimer', action: 'remove' }
109
+ ]
110
+
111
+ // Prepare emitters
112
+ const $emitters = defineEmits(['update', 'remove', 'fetch-more', 'usage'])
113
+
114
+ // Prepare props
115
+ const $props = defineProps({
116
+ displayHeader: {
117
+ type: Boolean,
118
+ default: false
119
+ },
120
+ headers: {
121
+ type: Array,
122
+ default: () => []
123
+ },
124
+ items: {
125
+ type: Array,
126
+ default: () => []
127
+ },
128
+ actions: {
129
+ type: Array,
130
+ default: () => []
131
+ },
132
+ paginationSize: {
133
+ type: Number,
134
+ default: 10
135
+ },
136
+ paginatedContent: {
137
+ type: Boolean,
138
+ default: false
139
+ }
140
+ })
141
+
142
+ // Prepare reactive data
143
+ const pagination = ref(1)
144
+
145
+ // Prepare computed
146
+ const enabledActions = computed(() => $props.actions.map(name => DOTS_OPTIONS.find(({ action }) => action === name)))
147
+ const paginatedItems = computed(() => $props.paginatedContent ? $props.items.slice(0, $props.paginationSize * pagination.value) : $props.items)
148
+ const displayPaginationButton = computed(() => $props.paginatedContent && pagination.value * $props.paginationSize <= paginatedItems.value.length)
149
+
150
+ // Prepare methods
151
+ const _displayMoreItem = async () => {
152
+ $emitters('fetch-more', pagination.value * $props.paginationSize)
153
+ pagination.value++
154
+ }
155
+ </script>
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <span
3
+ class="relative inline-flex items-center justify-center rounded-full focus:outline-none border border-gray-100 text-white bg-black"
4
+ :class="containerSize"
5
+ >
6
+ <media-image v-if="media" :media-id="media" :organization="organization" class="inline-block w-full h-full rounded-full" />
7
+ <span v-else :class="textSize" class="leading-none">
8
+ {{ initials }}
9
+ </span>
10
+ </span>
11
+ </template>
12
+
13
+ <script setup>
14
+ import mediaImage from '@/components/elements/media/image'
15
+
16
+ // Prepare props
17
+ const $props = defineProps({
18
+ organization: {
19
+ type: String,
20
+ default: ''
21
+ },
22
+ media: {
23
+ type: String,
24
+ default: ''
25
+ },
26
+ size: {
27
+ type: String,
28
+ default: 'xs'
29
+ },
30
+ placeHolder: {
31
+ type: String,
32
+ default: ''
33
+ }
34
+ })
35
+
36
+ // Prepare computed data
37
+ const containerSize = computed(() => {
38
+ const sizes = {
39
+ xs: 'h-6 w-6',
40
+ sm: 'h-8 w-8',
41
+ md: 'h-10 w-10',
42
+ lg: 'h-12 w-12',
43
+ xl: 'h-14 w-14',
44
+ '2xl': 'h-16 w-16',
45
+ '3xl': 'h-20 w-20',
46
+ '4xl': 'h-24 w-24',
47
+ '5xl': 'h-28 w-28',
48
+ '6xl': 'h-32 w-32'
49
+ }
50
+ return sizes[$props.size]
51
+ })
52
+ const textSize = computed(() => {
53
+ const sizes = {
54
+ xs: 'text-2xs',
55
+ sm: 'text-8',
56
+ md: 'text-10',
57
+ lg: 'text-12',
58
+ xl: 'text-14',
59
+ '2xl': 'text-16',
60
+ '3xl': 'text-20',
61
+ '4xl': 'text-24',
62
+ '5xl': 'text-28',
63
+ '6xl': 'text-32'
64
+ }
65
+ return sizes[$props.size]
66
+ })
67
+ const initials = computed(() => $props.placeHolder.match(/(\b\S)?/g).join('').match(/(^\S|\S$)?/g).join('').toUpperCase())
68
+ </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div
3
+ class="inline-flex items-center justify-start rounded-md hover:bg-gray-100 p-1"
4
+ @click="$emitters('click')"
5
+ >
6
+ <avatar-circular
7
+ v-if="!noAvatar"
8
+ :size="'xs'"
9
+ :place-holder="fullName"
10
+ :media="avatar"
11
+ />
12
+ <div class="text-sm truncate">
13
+ {{ prefix }}{{ fullName }}
14
+ </div>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup>
19
+ import avatarCircular from '~/components/elements/avatars/avatar-circular'
20
+
21
+ // Prepare emitters
22
+ const $emitters = defineEmits(['click'])
23
+
24
+ // Prepare props
25
+ const $props = defineProps({
26
+ prefix: {
27
+ type: String,
28
+ default: ''
29
+ },
30
+ noAvatar: {
31
+ type: Boolean,
32
+ default: false
33
+ },
34
+ user: {
35
+ type: Object,
36
+ default: () => ({})
37
+ },
38
+ disabled: {
39
+ type: Boolean,
40
+ default: false
41
+ }
42
+ })
43
+
44
+ // Prepare computed
45
+ const fullName = computed(() => [$props.user?.firstName, $props.user?.lastName].join(' '))
46
+ const avatar = computed(() => $props.user?.avatar)
47
+ </script>
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <div
3
+ :style="border ? `border-color: ${background};` : `background-color: ${background}; color: ${_textColor(background)};`"
4
+ class="relative inline-flex items-center text-xs h-6 font-light px-2 py-0.5 rounded-full text-gray-900 border border-white"
5
+ :class="{ 'pl-5':dot }"
6
+ @click="$emitters('click', title)"
7
+ >
8
+ <span
9
+ v-show="dot"
10
+ class="absolute h-2 w-2 left-2 rounded-full"
11
+ :style="`background-color: ${background}`"
12
+ />
13
+ <icon
14
+ v-show="icon"
15
+ :icon="icon"
16
+ class="h-4 w-4 mr-2"
17
+ />
18
+ {{ title }}
19
+ </div>
20
+ </template>
21
+
22
+ <script setup>
23
+ import { Icon } from '@iconify/vue'
24
+
25
+ const $emitters = defineEmits(['click'])
26
+
27
+ // Prepare props
28
+ const $props = defineProps({
29
+ dot: {
30
+ type: Boolean,
31
+ default: false
32
+ },
33
+ border: {
34
+ type: Boolean,
35
+ default: false
36
+ },
37
+ color: {
38
+ type: String,
39
+ default: ''
40
+ },
41
+ title: {
42
+ type: String,
43
+ default: ''
44
+ },
45
+ icon: {
46
+ type: String,
47
+ default: ''
48
+ }
49
+ })
50
+
51
+ // Thématique sans couleur (ou blanche) : repli sur un gris neutre lisible au
52
+ // lieu d'une pastille blanche invisible (upgrade-admin-nuxt4).
53
+ const FALLBACK_COLOR = '#e5e7eb'
54
+ const background = computed(() => {
55
+ const color = ($props.color || '').trim()
56
+ return !color || ['#ffffff', '#fff', 'white'].includes(color.toLowerCase())
57
+ ? FALLBACK_COLOR
58
+ : color
59
+ })
60
+
61
+ // prepare methods
62
+ const _textColor = (backgroundColor) => {
63
+ // hex (#rgb / #rrggbb) OU rgb() — l'ancien parseur ne lisait que rgb() et
64
+ // rendait du texte illisible sur les couleurs claires exprimées en hex.
65
+ let rgb
66
+ const hex = backgroundColor?.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)?.[1]
67
+ if (hex) {
68
+ const full = hex.length === 3 ? hex.split('').map((c) => c + c).join('') : hex
69
+ rgb = [full.slice(0, 2), full.slice(2, 4), full.slice(4, 6)].map((part) => parseInt(part, 16))
70
+ } else {
71
+ rgb = backgroundColor?.match(/\d+/g)?.map(Number)
72
+ }
73
+
74
+ if (!rgb || rgb.length < 3) {
75
+ return '#000'
76
+ }
77
+
78
+ // Calculate the relative luminance using the formula
79
+ // (source: https://www.w3.org/TR/WCAG20/#relativeluminancedef)
80
+ const luminance = (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) / 255;
81
+
82
+ // Return white (#fff) or black (#000) based on the contrast ratio
83
+ return luminance > 0.5 ? '#000' : '#fff';
84
+ }
85
+ </script>
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <button
3
+ :type="type"
4
+ :disabled="loading"
5
+ class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm text-white dark:text-black bg-black dark:bg-white hover:bg-white dark:hover:bg-gray-950 hover:text-black dark:hover:text-white focus:outline-none hover:border-black dark:hover:border-gray-700 disabled:bg-gray-200 disabled:border-gray-300 disabled:text-gray-400"
6
+ @click="$emit('click')"
7
+ >
8
+ <icon v-if="loading" icon="ei:spinner-2" class="animate-spin h-6 w-6 mr-1" />
9
+ {{ label }}
10
+ </button>
11
+ </template>
12
+
13
+ <script setup>
14
+ import { Icon } from '@iconify/vue'
15
+
16
+ defineProps({
17
+ label: {
18
+ type: String,
19
+ default: ''
20
+ },
21
+ type: {
22
+ type: String,
23
+ default: 'button'
24
+ },
25
+ loading: {
26
+ type: Boolean,
27
+ default: false
28
+ }
29
+ })
30
+
31
+ const $emit = defineEmits(['click'])
32
+ </script>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <button
3
+ :type="type"
4
+ class="px-4 py-2 border border-gray-200 shadow-xs rounded-md text-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none"
5
+ @click="$emit('click')"
6
+ >
7
+ <icon
8
+ v-show="icon"
9
+ :icon="icon"
10
+ class="h-5 w-5 text-gray-700"
11
+ />
12
+ {{ label }}
13
+ </button>
14
+ </template>
15
+
16
+ <script setup>
17
+ import { Icon } from '@iconify/vue'
18
+
19
+ defineProps({
20
+ label: {
21
+ type: String,
22
+ default: ''
23
+ },
24
+ type: {
25
+ type: String,
26
+ default: 'button'
27
+ },
28
+ icon: {
29
+ type: String,
30
+ default: ''
31
+ }
32
+ })
33
+
34
+ const $emit = defineEmits(['click'])
35
+ </script>
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ class="px-4 py-2 border border-transparent rounded-md text-sm text-gray-500 bg-white hover:underline hover:bg-white hover:text-black focus:outline-none"
5
+ @click="$emit('click')"
6
+ >
7
+ {{ label }}
8
+ </button>
9
+ </template>
10
+
11
+ <script setup>
12
+ defineProps({
13
+ label: {
14
+ type: String,
15
+ default: ''
16
+ },
17
+ type: {
18
+ type: String,
19
+ default: 'button'
20
+ }
21
+ })
22
+
23
+ const $emit = defineEmits(['click'])
24
+ </script>
25
+
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <div v-if="options.length" class="relative inline-block text-left">
3
+ <button
4
+ id="options-menu"
5
+ class="rounded-full flex items-center focus:outline-none cursor-pointer"
6
+ aria-haspopup="true"
7
+ aria-expanded="true"
8
+ @click.prevent="_toggle"
9
+ >
10
+ <span class="sr-only">Open options</span>
11
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
12
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z" />
13
+ </svg>
14
+ </button>
15
+ <transition
16
+ enter-active-class="transition ease-out duration-100"
17
+ enter-class="transform opacity-0 scale-95"
18
+ enter-to-class="transform opacity-100 scale-100"
19
+ leave-active-class="transition ease-in duration-75"
20
+ leave-class="transform opacity-100 scale-100"
21
+ leave-to-class="transform opacity-0 scale-95"
22
+ >
23
+ <div
24
+ v-if="display"
25
+ ref="dropdownDots"
26
+ class="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black/5 z-10"
27
+ >
28
+ <div class="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
29
+ <div
30
+ v-for="({ label, action }) in options"
31
+ :key="action"
32
+ class="block px-4 pr-16 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 cursor-pointer"
33
+ role="menuitem"
34
+ @click="_select(action)"
35
+ >
36
+ {{ label }}
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </transition>
41
+ </div>
42
+ </template>
43
+
44
+ <script setup>
45
+ import { onClickOutside } from '@vueuse/core'
46
+
47
+ defineProps({
48
+ options: {
49
+ type: Array,
50
+ default: () => []
51
+ }
52
+ })
53
+
54
+ // Prepare dynamic emitters
55
+ const $emitters = defineEmits(['action'])
56
+
57
+ // Prepare reactive data
58
+ const dropdownDots = ref(null)
59
+ const display = ref(false)
60
+
61
+ // Prepare methods
62
+ const _toggle = () => {
63
+ display.value = !display.value
64
+ }
65
+ const _close = () => {
66
+ display.value = false
67
+ }
68
+ const _select = (action) => {
69
+ _close()
70
+ $emitters('action', action)
71
+ }
72
+
73
+ // Prepare directives
74
+ onClickOutside(dropdownDots, _close)
75
+ </script>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <img
3
+ :src="originalUrl"
4
+ :srcset="srcSetUrls"
5
+ :alt="name"
6
+ >
7
+ </template>
8
+
9
+ <script setup>
10
+ // import { Icon } from '@iconify/vue2'
11
+ import { computedAsync } from '@vueuse/core'
12
+ import axios from 'axios'
13
+
14
+ const { Media } = useModels()
15
+
16
+ // Prepare props
17
+ const $props = defineProps({
18
+ organization: {
19
+ type: String,
20
+ default: ''
21
+ },
22
+ mediaId: {
23
+ type: String,
24
+ default: ''
25
+ }
26
+ })
27
+
28
+ // Prepare reactive data
29
+ const media = ref(undefined)
30
+
31
+ // Prepare computed
32
+ const name = computed(() => media.value?.name)
33
+ const originalUrl = computed(() => media.value?.objects?.original)
34
+ const srcSetUrls = computedAsync(async () => {
35
+ const filteredUrls = [];
36
+ const objects = media.value?.objects || {};
37
+
38
+ for (const [key, url] of Object.entries(objects)) {
39
+ if (!key.includes('original') && !key.includes('thumb')) {
40
+ try {
41
+ // Perform a GET request on the URL to ensure file exist
42
+ await axios({
43
+ method: 'GET',
44
+ url,
45
+ headers: {
46
+ 'Range': 'bytes=0-0'
47
+ }
48
+ })
49
+
50
+ const [, width] = key.split('-');
51
+ filteredUrls.push(`${url} ${width}w`)
52
+ } catch (err) {
53
+ console.log('Media thumb does not exist yet.')
54
+ }
55
+ }
56
+ }
57
+
58
+ return filteredUrls.join(', ');
59
+ });
60
+
61
+ // Prepare methods
62
+ const _getMediaRecord = async (_id) => {
63
+ let data
64
+ [data] = Media.repo.query().where('_id', _id).get()
65
+ console.log(data)
66
+ if (!data) {
67
+ (data = await Media.read(_id, undefined, $props.organization))
68
+ }
69
+ media.value = data
70
+ }
71
+
72
+ // Prepare watchers
73
+ watch(
74
+ () => $props.mediaId,
75
+ async (newValue) => {
76
+ if (newValue) {
77
+ await _getMediaRecord(newValue)
78
+ }
79
+ },
80
+ { immediate: true }
81
+ )
82
+ </script>
@@ -0,0 +1,90 @@
1
+ <template>
2
+ <div class="relative w-full pointer-events-auto p-4 bg-black text-white rounded-md shadow-xs ring-1 ring-gray-200 dark:ring-gray-800">
3
+ <div class="flex flex-row items-start">
4
+ <icon v-if="icon" :icon="icon" class="h-6 w-6 mr-1" />
5
+ <p>
6
+ {{ title }}
7
+ </p>
8
+ </div>
9
+ <div class="absolute bottom-0 w-full end-0 start-0 bg-gray-800 rounded-b-md">
10
+ <div v-if="timeout" class="h-1 bg-white rounded-bl-md" :style="progressStyle" />
11
+ </div>
12
+ </div>
13
+ </template>
14
+
15
+ <script setup>
16
+ import { Icon } from '@iconify/vue'
17
+ import useTimer from '~/composables/use-timer'
18
+
19
+ // Prepare props
20
+ const $props = defineProps({
21
+ title: {
22
+ type: String,
23
+ default: ''
24
+ },
25
+ description: {
26
+ type: String,
27
+ default: ''
28
+ },
29
+ type: {
30
+ type: String,
31
+ enum: ['default', 'info', 'warn', 'error'],
32
+ default: 'default'
33
+ },
34
+ icon: {
35
+ type: String,
36
+ default: ''
37
+ },
38
+ closeButton: {
39
+ type: Boolean,
40
+ default: false
41
+ },
42
+ timeout: {
43
+ type: Number,
44
+ default: 6000
45
+ },
46
+ loader: {
47
+ type: Boolean,
48
+ default: false
49
+ }
50
+ })
51
+
52
+ // Prepare emitters
53
+ const $emitters = defineEmits(['close'])
54
+
55
+ // Prepare reactive data
56
+ let timer = null
57
+ const remaining = ref($props.timeout)
58
+
59
+ // Prepare computed
60
+ const progressStyle = computed(() => {
61
+ const remainingPercent = remaining.value / $props.timeout * 100
62
+
63
+ return { width: `${remainingPercent || 0}%` }
64
+ })
65
+
66
+ // Prepare methods
67
+ const _onClose = () => {
68
+ if (timer) {
69
+ timer.stop()
70
+ }
71
+ $emitters('close')
72
+ }
73
+
74
+ // Prepare lifecycly hooks
75
+ onMounted(() => {
76
+ if ($props.timeout) {
77
+ timer = useTimer(_onClose, $props.timeout)
78
+
79
+ watchEffect(() => {
80
+ remaining.value = timer.remaining.value
81
+ })
82
+ }
83
+ })
84
+
85
+ onUnmounted(() => {
86
+ if (timer) {
87
+ timer.stop()
88
+ }
89
+ })
90
+ </script>