@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.
- package/README.md +41 -0
- package/app.config.ts +10 -0
- package/app.vue +13 -0
- package/assets/css/main.css +13 -0
- package/components/data/display/row/data-avatar.vue +23 -0
- package/components/data/display/row/data-badge.vue +34 -0
- package/components/data/display/row/data-color.vue +17 -0
- package/components/data/display/row/data-display-badge-publish-status.vue +29 -0
- package/components/data/display/row/data-label.vue +37 -0
- package/components/data/display/row/data-last-update.vue +45 -0
- package/components/data/display/row/data-spacer.vue +4 -0
- package/components/data/display/row/data-summary.vue +44 -0
- package/components/data/display/row/data-text.vue +14 -0
- package/components/data/display/row/data-toggle.vue +16 -0
- package/components/data/display/row.vue +111 -0
- package/components/data/lists/data-list.vue +85 -0
- package/components/data/lists/data-lists-table.vue +155 -0
- package/components/elements/avatars/avatar-circular.vue +68 -0
- package/components/elements/avatars/avatar-with-text.vue +47 -0
- package/components/elements/badges/badge.vue +85 -0
- package/components/elements/buttons/primary.vue +32 -0
- package/components/elements/buttons/secondary.vue +35 -0
- package/components/elements/buttons/tertiary.vue +25 -0
- package/components/elements/dropdowns/dots.vue +75 -0
- package/components/elements/media/image.vue +82 -0
- package/components/elements/notifications/default.vue +90 -0
- package/components/forms/handlers/handler-collections.vue +189 -0
- package/components/forms/handlers/handler-publication-status.vue +193 -0
- package/components/forms/handlers/handler-records.vue +138 -0
- package/components/forms/handlers/handler-roles.vue +176 -0
- package/components/forms/handlers/handler-schedules.vue +218 -0
- package/components/forms/handlers/handler-steps.vue +147 -0
- package/components/forms/handlers/handler-tokens.vue +79 -0
- package/components/forms/inputs/input-color-picker.vue +159 -0
- package/components/forms/inputs/input-editor.vue +363 -0
- package/components/forms/inputs/input-files-media.vue +150 -0
- package/components/forms/inputs/input-files.vue +98 -0
- package/components/forms/inputs/input-json.vue +65 -0
- package/components/forms/inputs/input-location.vue +179 -0
- package/components/forms/inputs/input-text-area.vue +65 -0
- package/components/forms/inputs/input-text.vue +122 -0
- package/components/forms/inputs/input-toggle.vue +125 -0
- package/components/forms/inputs/media-section/media-editor.vue +95 -0
- package/components/forms/layouts/form.vue +203 -0
- package/components/forms/layouts/layout.vue +47 -0
- package/components/forms/layouts/modal.vue +96 -0
- package/components/forms/layouts/navigation.vue +48 -0
- package/components/forms/layouts/section.vue +118 -0
- package/components/forms/selects/select-collection-enum.vue +195 -0
- package/components/forms/selects/select-date.vue +245 -0
- package/components/forms/selects/select-enum.vue +186 -0
- package/components/forms/selects/select-record.vue +274 -0
- package/components/layout/headings/headings-collection.vue +29 -0
- package/components/layout/headings/headings-record.vue +33 -0
- package/components/layout/navigations/primary/dropdown-publish.vue +147 -0
- package/components/layout/navigations/primary.vue +132 -0
- package/components/layout/notifications/notifications-list.vue +19 -0
- package/composables/core/domains/use-definitions.ts +57 -0
- package/composables/core/domains/use-entries.ts +62 -0
- package/composables/core/domains/use-media.ts +82 -0
- package/composables/core/use-core.ts +12 -0
- package/composables/use-ability.js +38 -0
- package/composables/use-models.js +27 -0
- package/composables/use-session.ts +80 -0
- package/composables/use-timer.js +62 -0
- package/composables/use-trpc.js +17 -0
- package/editor/index.ts +44 -0
- package/editor/views/CalloutView.vue +22 -0
- package/editor/views/DetailsContentView.vue +11 -0
- package/editor/views/DetailsSummaryView.vue +11 -0
- package/editor/views/DetailsView.vue +72 -0
- package/editor/views/EmbedView.vue +53 -0
- package/editor/views/FileView.vue +79 -0
- package/editor/views/ImageView.vue +56 -0
- package/editor/views/UploadView.vue +71 -0
- package/layouts/default.vue +21 -0
- package/layouts/entrance.vue +10 -0
- package/layouts/record.vue +21 -0
- package/middleware/00.auth.global.js +16 -0
- package/middleware/01.instance.global.js +21 -0
- package/middleware/03.verify-permissions.global.js +11 -0
- package/middleware/04.prefetch-page.global.js +18 -0
- package/middleware/05.headings-page.global.js +25 -0
- package/models/_commun.js +243 -0
- package/models/_factory.js +34 -0
- package/models/collection.js +87 -0
- package/models/entrance.js +42 -0
- package/models/media.js +82 -0
- package/models/organization.js +92 -0
- package/models/record.js +165 -0
- package/models/role.js +66 -0
- package/models/token.js +56 -0
- package/models/user.js +97 -0
- package/nuxt.config.ts +151 -0
- package/package.json +71 -0
- package/pages/[collection]/[record]/index.vue +88 -0
- package/pages/[collection]/index.vue +55 -0
- package/pages/_lab/editor.vue +149 -0
- package/pages/auth/callback.vue +31 -0
- package/pages/index/index.vue +85 -0
- package/pages/index/password/define/[token].vue +79 -0
- package/pages/index/password/recover/index.vue +72 -0
- package/pages/index/welcome/[token].vue +90 -0
- package/pages/index.vue +44 -0
- package/pages/management/index.vue +15 -0
- package/pages/management/members.vue +176 -0
- package/pages/management/organizations.vue +192 -0
- package/pages/management/roles.vue +183 -0
- package/pages/overview.vue +15 -0
- package/pages/settings.vue +197 -0
- package/plugins/01.session.client.ts +5 -0
- package/plugins/02.vue-query.ts +25 -0
- package/public/favicon.ico +0 -0
- package/public/logo-black.svg +1 -0
- package/public/logo-white.svg +1 -0
- package/server/api/trpc/[...trpc].ts +28 -0
- package/server/api/v1/content/deployment.get.ts +45 -0
- package/server/api/v1/content/records.get.ts +12 -0
- package/server/middleware/0.security.ts +19 -0
- package/server/middleware/1.cors.ts +31 -0
- package/server/middleware/2.session.ts +20 -0
- package/server/middleware/3.api-token.ts +20 -0
- package/server/plugins/core.ts +8 -0
- package/server/routes/_tasks/[name].post.ts +20 -0
- package/server/routes/health.get.ts +6 -0
- package/server/tasks/deploy.ts +26 -0
- package/server/types.d.ts +8 -0
- package/server/utils/core.ts +18 -0
- package/store/layout/headings.js +21 -0
- package/store/layout/modal.js +80 -0
- package/store/layout/notifications.js +52 -0
- 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>
|