@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,195 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<label
|
|
4
|
+
v-if="label"
|
|
5
|
+
id="listbox-label"
|
|
6
|
+
class="block text-2xs font-light tracking-wider text-gray-500 uppercase"
|
|
7
|
+
>
|
|
8
|
+
{{ label }}
|
|
9
|
+
</label>
|
|
10
|
+
<div
|
|
11
|
+
:class="{ 'mt-1':label }"
|
|
12
|
+
class="relative"
|
|
13
|
+
>
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
aria-haspopup="listbox"
|
|
17
|
+
aria-expanded="true"
|
|
18
|
+
aria-labelledby="listbox-label"
|
|
19
|
+
:class="{ 'ring-gray-50 border-gray-900':display && !readOnly, 'bg-gray-50 text-gray-500 ring-gray-200': readOnly }"
|
|
20
|
+
class="relative w-full rounded-md pl-3 pr-10 py-2 text-left cursor-pointer shadow-xs border border-gray-200 focus:outline-none text-sm"
|
|
21
|
+
@click="_toggle"
|
|
22
|
+
>
|
|
23
|
+
<span :class="{ 'text-gray-700':!selectionText }" class="block truncate">
|
|
24
|
+
{{ selectionText || placeHolder }}
|
|
25
|
+
</span>
|
|
26
|
+
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
27
|
+
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
28
|
+
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
29
|
+
</svg>
|
|
30
|
+
</span>
|
|
31
|
+
</button>
|
|
32
|
+
<transition
|
|
33
|
+
leave-active-class="transition ease-in duration-100"
|
|
34
|
+
leave-from-class="opacity-100"
|
|
35
|
+
leave-to-class="opacity-0"
|
|
36
|
+
>
|
|
37
|
+
<div
|
|
38
|
+
v-if="display"
|
|
39
|
+
ref="selectCollectionEnum"
|
|
40
|
+
class="absolute mt-1 w-full rounded-md bg-white shadow-lg z-50"
|
|
41
|
+
>
|
|
42
|
+
<ul tabindex="-1" role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-item-3" class="max-h-60 rounded-md p-2 text-base ring-1 ring-black/5 overflow-auto focus:outline-none sm:text-sm">
|
|
43
|
+
<li
|
|
44
|
+
v-for="(option, i) in list"
|
|
45
|
+
:id="`listbox-item-${i}`"
|
|
46
|
+
:key="option"
|
|
47
|
+
:class="readOnly ? 'cursor-not-allowed text-gray-300' : 'cursor-pointer text-black'"
|
|
48
|
+
class=" select-none rounded-sm relative py-2 px-4 hover:bg-gray-50"
|
|
49
|
+
role="option"
|
|
50
|
+
@click="!readOnly && _select(option)"
|
|
51
|
+
>
|
|
52
|
+
<span :class="{ 'font-normal text-black': _isSelected(option) }" class="font-light block truncate">
|
|
53
|
+
{{ option }}
|
|
54
|
+
</span>
|
|
55
|
+
<span
|
|
56
|
+
v-show="_isSelected(option)"
|
|
57
|
+
class="absolute text-black inset-y-0 right-0 flex items-center pr-4"
|
|
58
|
+
>
|
|
59
|
+
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
60
|
+
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
61
|
+
</svg>
|
|
62
|
+
</span>
|
|
63
|
+
</li>
|
|
64
|
+
</ul>
|
|
65
|
+
</div>
|
|
66
|
+
</transition>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup>
|
|
72
|
+
import { onClickOutside } from '@vueuse/core'
|
|
73
|
+
|
|
74
|
+
// Prepare $emitters
|
|
75
|
+
const $emitters = defineEmits(['change'])
|
|
76
|
+
|
|
77
|
+
// Prepare props
|
|
78
|
+
const $props = defineProps({
|
|
79
|
+
attribute: {
|
|
80
|
+
type: String,
|
|
81
|
+
default: ''
|
|
82
|
+
},
|
|
83
|
+
filter: {
|
|
84
|
+
type: Function,
|
|
85
|
+
default: () => () => true
|
|
86
|
+
},
|
|
87
|
+
itemKey: {
|
|
88
|
+
type: String,
|
|
89
|
+
default: 'label'
|
|
90
|
+
},
|
|
91
|
+
itemValue: {
|
|
92
|
+
type: String,
|
|
93
|
+
default: 'id'
|
|
94
|
+
},
|
|
95
|
+
label: {
|
|
96
|
+
type: String,
|
|
97
|
+
default: ''
|
|
98
|
+
},
|
|
99
|
+
enableMultiple: {
|
|
100
|
+
type: Boolean,
|
|
101
|
+
default: false
|
|
102
|
+
},
|
|
103
|
+
object: {
|
|
104
|
+
type: Boolean,
|
|
105
|
+
default: true
|
|
106
|
+
},
|
|
107
|
+
readOnly: {
|
|
108
|
+
type: Boolean,
|
|
109
|
+
default: false
|
|
110
|
+
},
|
|
111
|
+
placeHolder: {
|
|
112
|
+
type: String,
|
|
113
|
+
default: 'Sélectionnez'
|
|
114
|
+
},
|
|
115
|
+
value: {
|
|
116
|
+
type: [String, Array],
|
|
117
|
+
default: ''
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Prepare reactive data
|
|
122
|
+
const items = ref([])
|
|
123
|
+
const display = ref(false)
|
|
124
|
+
const selection = ref('')
|
|
125
|
+
const selectCollectionEnum = ref(null)
|
|
126
|
+
|
|
127
|
+
// Prepare computed
|
|
128
|
+
const list = computed(() => {
|
|
129
|
+
const list = items.value.filter($props.filter)
|
|
130
|
+
if ($props.object) {
|
|
131
|
+
return list.map(item => item[$props.itemKey])
|
|
132
|
+
}
|
|
133
|
+
return list
|
|
134
|
+
})
|
|
135
|
+
const selectionText = computed(() => {
|
|
136
|
+
if ($props.object) {
|
|
137
|
+
return items.value.filter(item => _isSelected(item[$props.itemKey])).map(item => item[$props.itemKey]).join(', ')
|
|
138
|
+
}
|
|
139
|
+
return $props.enableMultiple ? selection.value.join(', ') : selection.value
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Prepare watchers
|
|
143
|
+
watch(
|
|
144
|
+
() => $props.value,
|
|
145
|
+
(newValue) => {
|
|
146
|
+
if (newValue) {
|
|
147
|
+
selection.value = newValue
|
|
148
|
+
} else {
|
|
149
|
+
selection.value = $props.enableMultiple ? [] : ''
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{ immediate: true }
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
// Prepare methods
|
|
156
|
+
const _isSelected = (option) => {
|
|
157
|
+
const val = $props.object
|
|
158
|
+
? items.value.find(item => item[$props.itemKey] === option)?.[$props.itemValue]
|
|
159
|
+
: option
|
|
160
|
+
return $props.enableMultiple ? selection.value.includes(val) : selection.value === val
|
|
161
|
+
}
|
|
162
|
+
const _select = (option) => {
|
|
163
|
+
let val = option
|
|
164
|
+
if ($props.object) {
|
|
165
|
+
const { [$props.itemValue]: objectVal } = items.value.find(({ [$props.itemKey]: val }) => val === option)
|
|
166
|
+
val = objectVal
|
|
167
|
+
}
|
|
168
|
+
if ($props.enableMultiple) {
|
|
169
|
+
const index = selection.value.indexOf(val)
|
|
170
|
+
selection.value = index >= 0 ? selection.value.filter(check => check !== val) : [...selection.value, val]
|
|
171
|
+
} else {
|
|
172
|
+
selection.vale = val
|
|
173
|
+
_close()
|
|
174
|
+
}
|
|
175
|
+
$emitters('change', selection.value)
|
|
176
|
+
}
|
|
177
|
+
const _toggle = () => {
|
|
178
|
+
display.value = !display.value
|
|
179
|
+
}
|
|
180
|
+
const _close = () => {
|
|
181
|
+
display.value = false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Prepare mounted
|
|
185
|
+
onMounted(() => {
|
|
186
|
+
const { Collection } = useModels()
|
|
187
|
+
const $route = useRoute()
|
|
188
|
+
const collectionRecord = Collection.repo.where('slug', $route.params.collection).first()
|
|
189
|
+
const { options } = collectionRecord.attributes.find(({ name }) => name === $props.attribute)
|
|
190
|
+
items.value = options.items
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Prepare directives
|
|
194
|
+
onClickOutside(selectCollectionEnum, _close)
|
|
195
|
+
</script>
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<label
|
|
4
|
+
v-if="label"
|
|
5
|
+
id="listbox-label"
|
|
6
|
+
class="block text-2xs font-light tracking-wider text-gray-500 uppercase"
|
|
7
|
+
>
|
|
8
|
+
{{ label }}
|
|
9
|
+
</label>
|
|
10
|
+
<div
|
|
11
|
+
:class="{ 'mt-1':label }"
|
|
12
|
+
class="relative"
|
|
13
|
+
>
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
aria-haspopup="listbox"
|
|
17
|
+
aria-expanded="true"
|
|
18
|
+
aria-labelledby="listbox-label"
|
|
19
|
+
:class="{ 'ring-gray-50 border-gray-900':display, '!bg-gray-100 !text-gray-200': readOnly }"
|
|
20
|
+
:disabled="readOnly"
|
|
21
|
+
class="relative w-full bg-white rounded-md pl-3 pr-10 py-2 text-left cursor-pointer shadow-xs border border-gray-200 focus:outline-none focus:ring-gray-50 focus:border-black hover:border-black text-sm"
|
|
22
|
+
@click="_toggle"
|
|
23
|
+
>
|
|
24
|
+
<div class="flex justify-between items-center">
|
|
25
|
+
<span :class="{ '!text-gray-400': readOnly }" class="truncate">
|
|
26
|
+
{{ selectionDate }}
|
|
27
|
+
</span>
|
|
28
|
+
<span v-if="timePicker">
|
|
29
|
+
{{ selectionTimeBegin }} <span v-if="timeEnd">→ {{ selectionTimeEnd }}</span>
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
33
|
+
<icon
|
|
34
|
+
class="text-gray-400 h-5 w-5"
|
|
35
|
+
:icon="timePicker ? 'carbon:event-schedule' : 'carbon:calendar'"
|
|
36
|
+
/>
|
|
37
|
+
</span>
|
|
38
|
+
</button>
|
|
39
|
+
<transition
|
|
40
|
+
leave-active-class="transition ease-in duration-100"
|
|
41
|
+
leave-from-class="opacity-100"
|
|
42
|
+
leave-to-class="opacity-0"
|
|
43
|
+
>
|
|
44
|
+
<div v-if="display" ref="calendar" class="absolute flex flex-col mt-1 p-4 w-full min-w-[20rem] bg-white shadow-lg z-50 rounded-md">
|
|
45
|
+
<!-- time picker -->
|
|
46
|
+
<div v-if="timePicker" class="inline-flex space-x-1 pb-2 border-b border-gray-100">
|
|
47
|
+
<select-enum
|
|
48
|
+
:items="hoursBegin"
|
|
49
|
+
:value="selectionTimeBegin"
|
|
50
|
+
:object="false"
|
|
51
|
+
class="w-full"
|
|
52
|
+
@change="_updateHour('begin', $event)"
|
|
53
|
+
/>
|
|
54
|
+
<select-enum
|
|
55
|
+
v-if="timeEnd"
|
|
56
|
+
:items="hoursEnd"
|
|
57
|
+
:value="selectionTimeEnd"
|
|
58
|
+
:object="false"
|
|
59
|
+
class="w-full"
|
|
60
|
+
@change="_updateHour('end', $event)"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
<!-- Header -->
|
|
64
|
+
<div class="flex flex-col items-start md:items-center md:flex-row justify-between w-full pt-2 mb-1">
|
|
65
|
+
<span class="text-sm font-bold mb-2 md:mb-0">
|
|
66
|
+
{{ calendarMonth }}
|
|
67
|
+
</span>
|
|
68
|
+
<div class="inline-flex">
|
|
69
|
+
<icon
|
|
70
|
+
class="text-gray-400 hover:text-white hover:bg-primary p-1 cursor-pointer rounded-md h-6 w-6"
|
|
71
|
+
icon="material-symbols:chevron-left"
|
|
72
|
+
@click="_previousMonth"
|
|
73
|
+
/>
|
|
74
|
+
<icon
|
|
75
|
+
class="text-gray-400 hover:text-white hover:bg-primary p-1 cursor-pointer rounded-md h-6 w-6"
|
|
76
|
+
icon="material-symbols:chevron-right-rounded"
|
|
77
|
+
@click="_nextMonth"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- Calendar Body -->
|
|
83
|
+
<div class="flex-grow grid grid-cols-7 gap-0.5 rounded-md">
|
|
84
|
+
<div v-for="day in WEEK_DAYS" :key="day" class="h-min text-center text-gray-500 font-semibold mt-3 text-xs self-end">
|
|
85
|
+
{{ day }}
|
|
86
|
+
</div>
|
|
87
|
+
<button
|
|
88
|
+
v-for="({ dt, selected, current }) in calendarDays"
|
|
89
|
+
:key="dt.ordinal"
|
|
90
|
+
:class="{
|
|
91
|
+
'bg-white hover:bg-primary text-black hover:text-white': current && !selected,
|
|
92
|
+
'bg-primary text-white': current && selected,
|
|
93
|
+
'text-gray-400': !current && !selected
|
|
94
|
+
}"
|
|
95
|
+
class="relative w-full rounded-md px-2 py-1.5"
|
|
96
|
+
@click="_select(dt)"
|
|
97
|
+
>
|
|
98
|
+
<span class="text-xs self-end">
|
|
99
|
+
{{ dt.day }}
|
|
100
|
+
</span>
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</transition>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
</template>
|
|
109
|
+
|
|
110
|
+
<script setup>
|
|
111
|
+
import { Icon } from '@iconify/vue'
|
|
112
|
+
import { DateTime, Info } from 'luxon'
|
|
113
|
+
import selectEnum from '~/components/forms/selects/select-enum'
|
|
114
|
+
|
|
115
|
+
// Prepare constant
|
|
116
|
+
const TODAY = DateTime.now({ zone: 'Europe/Paris' })
|
|
117
|
+
const WEEK_DAYS = Info.weekdays('narrow', { locale: 'fr' })
|
|
118
|
+
const HOURS = ['00:00', '00:30', '01:00', '01:30', '02:00', '02:30', '03:00', '03:30', '04:00', '04:30', '05:00', '05:30', '06:00', '06:30', '07:00', '07:30', '08:00', '08:30', '09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00', '12:30', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30', '17:00', '17:30', '18:00', '18:30', '19:00', '19:30', '20:00', '20:30', '21:00', '21:30', '22:00', '22:30', '23:00', '23:30']
|
|
119
|
+
|
|
120
|
+
// Prepare props
|
|
121
|
+
const $props = defineProps({
|
|
122
|
+
label: {
|
|
123
|
+
type: String,
|
|
124
|
+
default: ''
|
|
125
|
+
},
|
|
126
|
+
timePicker: {
|
|
127
|
+
type: Boolean,
|
|
128
|
+
default: false
|
|
129
|
+
},
|
|
130
|
+
timeEnd: {
|
|
131
|
+
type: Boolean,
|
|
132
|
+
default: false
|
|
133
|
+
},
|
|
134
|
+
readOnly: {
|
|
135
|
+
type: Boolean,
|
|
136
|
+
default: false
|
|
137
|
+
},
|
|
138
|
+
value: {
|
|
139
|
+
type: String,
|
|
140
|
+
default: ''
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Prepare emitters
|
|
145
|
+
const $emitters = defineEmits(['select'])
|
|
146
|
+
|
|
147
|
+
// Prepare dynamic data
|
|
148
|
+
const display = ref(false)
|
|
149
|
+
const selectionBegin = ref(TODAY.set({ hour: 0, minute: 0, second: 0 }))
|
|
150
|
+
const selectionEnd = ref(TODAY.set({ hour: 23, minute: 59, second: 0 }))
|
|
151
|
+
const calendar = ref(null)
|
|
152
|
+
|
|
153
|
+
// Prepare computed
|
|
154
|
+
const hoursBegin = computed(() => HOURS)
|
|
155
|
+
const hoursEnd = computed(() => HOURS)
|
|
156
|
+
const selectionDate = computed(() => selectionBegin.value.toFormat('ccc d LLL', { locale: 'fr' }))
|
|
157
|
+
const selectionTimeBegin = computed(() => selectionBegin.value.toFormat('HH:mm', { locale: 'fr' }))
|
|
158
|
+
const selectionTimeEnd = computed(() => selectionEnd.value.toFormat('HH:mm', { locale: 'fr' }))
|
|
159
|
+
const calendarMonth = computed(() => selectionBegin.value.toFormat('LLLL yyyy', { locale: 'fr' }))
|
|
160
|
+
const calendarDays = computed(() => {
|
|
161
|
+
// Prepare array of days
|
|
162
|
+
const selectedDT = selectionBegin.value
|
|
163
|
+
const days = []
|
|
164
|
+
|
|
165
|
+
// Determine the final days of the previous month until the last monday
|
|
166
|
+
let previousMonthDT = selectedDT.minus({ months: 1 }).endOf('month')
|
|
167
|
+
while (previousMonthDT.weekday !== 1) {
|
|
168
|
+
days.push({ dt: previousMonthDT })
|
|
169
|
+
previousMonthDT = previousMonthDT.minus({ days: 1 })
|
|
170
|
+
}
|
|
171
|
+
days.push({ dt: previousMonthDT })
|
|
172
|
+
|
|
173
|
+
// Complete array with seleted month days
|
|
174
|
+
days.reverse().push(
|
|
175
|
+
...Array.from(
|
|
176
|
+
{ length: selectedDT.endOf('month').day },
|
|
177
|
+
(_, day) => ({
|
|
178
|
+
dt: selectedDT.set({ day: day + 1 }),
|
|
179
|
+
selected: selectedDT.day.toString() === (day + 1).toString(),
|
|
180
|
+
current: true
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
// Complete array to with the next month beginning day
|
|
186
|
+
let nextMonthDT = selectedDT.plus({ months: 1 }).startOf('month')
|
|
187
|
+
while (nextMonthDT.weekday !== 1) {
|
|
188
|
+
days.push({ dt: nextMonthDT })
|
|
189
|
+
nextMonthDT = nextMonthDT.plus({ days: 1 })
|
|
190
|
+
}
|
|
191
|
+
return days
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// Prepare watchers
|
|
195
|
+
watch(
|
|
196
|
+
() => $props.value,
|
|
197
|
+
(newValue) => {
|
|
198
|
+
if (newValue) {
|
|
199
|
+
console.log(newValue)
|
|
200
|
+
selectionBegin.value = DateTime.fromISO(newValue)
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{ immediate: true }
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// Prepare methods
|
|
207
|
+
const _emitChange = () => {
|
|
208
|
+
$emitters('change', {
|
|
209
|
+
date: selectionBegin.value.toISO(),
|
|
210
|
+
beginTime: selectionBegin.value.toISO(),
|
|
211
|
+
endTime: selectionEnd.value.toISO()
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
const _updateHour = (beginOrEnd, time) => {
|
|
215
|
+
const [hour, minute] = time.split(':')
|
|
216
|
+
if (beginOrEnd === 'begin') {
|
|
217
|
+
selectionBegin.value = selectionBegin.value.set({ hour, minute })
|
|
218
|
+
} else {
|
|
219
|
+
selectionEnd.value = selectionEnd.value.set({ hour, minute })
|
|
220
|
+
}
|
|
221
|
+
_emitChange()
|
|
222
|
+
}
|
|
223
|
+
const _select = (dt) => {
|
|
224
|
+
selectionBegin.value = dt.set({ hour: selectionBegin.value.hour, minute: selectionBegin.value.minute })
|
|
225
|
+
selectionEnd.value = dt.set({ hour: selectionEnd.value.hour, minute: selectionEnd.value.minute })
|
|
226
|
+
_emitChange()
|
|
227
|
+
}
|
|
228
|
+
const _nextMonth = () => {
|
|
229
|
+
selectionBegin.value = selectionBegin.value.plus({ month: 1 })
|
|
230
|
+
selectionEnd.value = selectionEnd.value.plus({ month: 1 })
|
|
231
|
+
}
|
|
232
|
+
const _previousMonth = () => {
|
|
233
|
+
selectionBegin.value = selectionBegin.value.minus({ months: 1 })
|
|
234
|
+
selectionEnd.value = selectionEnd.value.minus({ months: 1 })
|
|
235
|
+
}
|
|
236
|
+
const _toggle = () => {
|
|
237
|
+
display.value = !display.value
|
|
238
|
+
}
|
|
239
|
+
const _close = () => {
|
|
240
|
+
display.value = false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Prepare directives
|
|
244
|
+
onClickOutside(calendar, _close)
|
|
245
|
+
</script>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<label
|
|
4
|
+
v-if="label"
|
|
5
|
+
id="listbox-label"
|
|
6
|
+
class="block text-2xs font-light tracking-wider text-gray-500 uppercase"
|
|
7
|
+
>
|
|
8
|
+
{{ label }}
|
|
9
|
+
</label>
|
|
10
|
+
<div
|
|
11
|
+
:class="{ 'mt-1':label }"
|
|
12
|
+
class="relative"
|
|
13
|
+
>
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
aria-haspopup="listbox"
|
|
17
|
+
aria-expanded="true"
|
|
18
|
+
aria-labelledby="listbox-label"
|
|
19
|
+
:class="{ 'ring-gray-50 border-gray-900':display, '!bg-gray-100 !text-gray-200': readOnly }"
|
|
20
|
+
:disabled="readOnly"
|
|
21
|
+
class="relative w-full bg-white rounded-md pl-3 pr-10 py-2 text-left cursor-pointer shadow-xs border border-gray-200 focus:outline-none focus:ring-gray-50 focus:border-black hover:border-black text-sm"
|
|
22
|
+
@click="_toggle"
|
|
23
|
+
>
|
|
24
|
+
<span :class="{ 'text-gray-700':!selectionText, '!text-gray-400': readOnly }" class="block truncate">
|
|
25
|
+
{{ selectionText || placeHolder }}
|
|
26
|
+
</span>
|
|
27
|
+
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
28
|
+
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
29
|
+
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
30
|
+
</svg>
|
|
31
|
+
</span>
|
|
32
|
+
</button>
|
|
33
|
+
<transition
|
|
34
|
+
leave-active-class="transition ease-in duration-100"
|
|
35
|
+
leave-from-class="opacity-100"
|
|
36
|
+
leave-to-class="opacity-0"
|
|
37
|
+
>
|
|
38
|
+
<div
|
|
39
|
+
v-if="display"
|
|
40
|
+
ref="selectEnum"
|
|
41
|
+
class="absolute mt-1 w-full rounded-md bg-white shadow-lg z-50"
|
|
42
|
+
>
|
|
43
|
+
<ul tabindex="-1" role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-item-3" class="max-h-60 rounded-md p-2 text-base ring-1 ring-black/5 overflow-auto focus:outline-none sm:text-sm">
|
|
44
|
+
<li
|
|
45
|
+
v-for="(option, i) in list"
|
|
46
|
+
:id="`listbox-item-${i}`"
|
|
47
|
+
:key="option"
|
|
48
|
+
class="cursor-default select-none rounded-md relative py-2 pl-4 pr-4 text-gray-900 hover:bg-gray-100"
|
|
49
|
+
role="option"
|
|
50
|
+
@click="_select(option)"
|
|
51
|
+
>
|
|
52
|
+
<span :class="{ 'font-semibold':_isSelected(option) }" class="font-normal block truncate">
|
|
53
|
+
{{ option }}
|
|
54
|
+
</span>
|
|
55
|
+
<span
|
|
56
|
+
v-show="_isSelected(option)"
|
|
57
|
+
class="absolute text-gray-900 inset-y-0 right-0 flex items-center pr-4"
|
|
58
|
+
>
|
|
59
|
+
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
60
|
+
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
61
|
+
</svg>
|
|
62
|
+
</span>
|
|
63
|
+
</li>
|
|
64
|
+
</ul>
|
|
65
|
+
</div>
|
|
66
|
+
</transition>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup>
|
|
72
|
+
import { onClickOutside } from '@vueuse/core'
|
|
73
|
+
|
|
74
|
+
// Prepare $emitters
|
|
75
|
+
const $emitters = defineEmits(['change'])
|
|
76
|
+
|
|
77
|
+
// Prepare props
|
|
78
|
+
const $props = defineProps({
|
|
79
|
+
filter: {
|
|
80
|
+
type: Function,
|
|
81
|
+
default: () => () => true
|
|
82
|
+
},
|
|
83
|
+
items: {
|
|
84
|
+
type: Array,
|
|
85
|
+
default: () => []
|
|
86
|
+
},
|
|
87
|
+
itemKey: {
|
|
88
|
+
type: String,
|
|
89
|
+
default: 'name'
|
|
90
|
+
},
|
|
91
|
+
itemValue: {
|
|
92
|
+
type: String,
|
|
93
|
+
default: '_id'
|
|
94
|
+
},
|
|
95
|
+
label: {
|
|
96
|
+
type: String,
|
|
97
|
+
default: ''
|
|
98
|
+
},
|
|
99
|
+
enableMultiple: {
|
|
100
|
+
type: Boolean,
|
|
101
|
+
default: false
|
|
102
|
+
},
|
|
103
|
+
object: {
|
|
104
|
+
type: Boolean,
|
|
105
|
+
default: true
|
|
106
|
+
},
|
|
107
|
+
placeHolder: {
|
|
108
|
+
type: String,
|
|
109
|
+
default: 'Sélectionnez'
|
|
110
|
+
},
|
|
111
|
+
readOnly: {
|
|
112
|
+
type: Boolean,
|
|
113
|
+
default: false
|
|
114
|
+
},
|
|
115
|
+
value: {
|
|
116
|
+
type: [String, Array],
|
|
117
|
+
default: ''
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Prepare reactive data
|
|
122
|
+
const display = ref(false)
|
|
123
|
+
const selection = ref($props.enableMultiple ? [] : '')
|
|
124
|
+
const selectEnum = ref(null)
|
|
125
|
+
|
|
126
|
+
// Prepare computed
|
|
127
|
+
const list = computed(() => {
|
|
128
|
+
const list = $props.items.filter($props.filter)
|
|
129
|
+
if ($props.object) {
|
|
130
|
+
return list.map(item => item[$props.itemKey])
|
|
131
|
+
}
|
|
132
|
+
return list
|
|
133
|
+
})
|
|
134
|
+
const selectionText = computed(() => {
|
|
135
|
+
const selectedItems = $props.object
|
|
136
|
+
? $props.items.map(item => item[$props.itemKey]).filter(_isSelected)
|
|
137
|
+
: selection.value
|
|
138
|
+
|
|
139
|
+
return $props.enableMultiple ? selectedItems.join(', ') : $props.object ? selectedItems[0] : selectedItems
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Prepare watchers
|
|
143
|
+
watch(
|
|
144
|
+
() => $props.value,
|
|
145
|
+
(newValue) => {
|
|
146
|
+
if (newValue) {
|
|
147
|
+
selection.value = newValue
|
|
148
|
+
} else {
|
|
149
|
+
selection.value = $props.enableMultiple ? [] : ''
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{ immediate: true }
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
// Prepare methods
|
|
156
|
+
const _isSelected = (option) => {
|
|
157
|
+
const val = $props.object
|
|
158
|
+
? $props.items.find(item => item[$props.itemKey] === option)?.[$props.itemValue]
|
|
159
|
+
: option
|
|
160
|
+
return $props.enableMultiple ? selection.value.includes(val) : selection.value === val;
|
|
161
|
+
}
|
|
162
|
+
const _select = (option) => {
|
|
163
|
+
let val = option
|
|
164
|
+
if ($props.object) {
|
|
165
|
+
const { [$props.itemValue]: objectVal } = $props.items.find(({ [$props.itemKey]: val }) => val === option)
|
|
166
|
+
val = objectVal
|
|
167
|
+
}
|
|
168
|
+
if ($props.enableMultiple) {
|
|
169
|
+
const index = selection.value?.indexOf(val)
|
|
170
|
+
selection.value = index >= 0 ? selection.value.filter(check => check !== val) : [...selection.value, val]
|
|
171
|
+
} else {
|
|
172
|
+
selection.value = val
|
|
173
|
+
_close()
|
|
174
|
+
}
|
|
175
|
+
$emitters('change', selection.value)
|
|
176
|
+
}
|
|
177
|
+
const _toggle = () => {
|
|
178
|
+
display.value = !display.value
|
|
179
|
+
}
|
|
180
|
+
const _close = () => {
|
|
181
|
+
display.value = false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Prepare directives
|
|
185
|
+
onClickOutside(selectEnum, _close)
|
|
186
|
+
</script>
|