@raclettejs/workbench 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.
- package/README.md +1 -0
- package/config/compositions.js +282 -0
- package/config/interactionLinks.js +164 -0
- package/i18n/de-DE.json +245 -0
- package/i18n/en-EU.json +245 -0
- package/i18n/sk-SK.json +245 -0
- package/package.json +63 -0
- package/packages.json +33 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionConfiguration.vue +140 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionIntegration.vue +3 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionWidgetsLayout.vue +129 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetContextMenu.vue +53 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetIcon.vue +99 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetList.vue +122 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetRenderer.vue +65 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetSettings.vue +96 -0
- package/plugins/pacifico__compositions/frontend/widgets/CompositionCreateWidget.vue +51 -0
- package/plugins/pacifico__compositions/frontend/widgets/CompositionEditWidget.vue +66 -0
- package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +132 -0
- package/plugins/pacifico__compositions/raclette.plugin.ts +8 -0
- package/plugins/pacifico__core/backend/index.ts +25 -0
- package/plugins/pacifico__core/backend/routes.ts +220 -0
- package/plugins/pacifico__core/raclette.plugin.ts +9 -0
- package/plugins/pacifico__interactionLinks/frontend/components/InteractionLinkConfiguration.vue +145 -0
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkCreateWidget.vue +48 -0
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkEditWidget.vue +66 -0
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +126 -0
- package/plugins/pacifico__interactionLinks/raclette.plugin.ts +8 -0
- package/plugins/pacifico__plugins/frontend/index.ts +42 -0
- package/plugins/pacifico__plugins/frontend/widgets/PluginDetailWidget.vue +26 -0
- package/plugins/pacifico__plugins/frontend/widgets/PluginListWidget.vue +59 -0
- package/plugins/pacifico__plugins/raclette.plugin.ts +8 -0
- package/plugins/pacifico__tags/frontend/components/TagConfiguration.vue +99 -0
- package/plugins/pacifico__tags/frontend/widgets/TagCreateWidget.vue +43 -0
- package/plugins/pacifico__tags/frontend/widgets/TagEditWidget.vue +64 -0
- package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +71 -0
- package/plugins/pacifico__tags/raclette.plugin.ts +8 -0
- package/plugins/pacifico__users/frontend/components/UserConfiguration.vue +110 -0
- package/plugins/pacifico__users/frontend/widgets/UserCreateWidget.vue +48 -0
- package/plugins/pacifico__users/frontend/widgets/UserEditWidget.vue +64 -0
- package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +86 -0
- package/plugins/pacifico__users/raclette.plugin.ts +8 -0
- package/raclette.config.js +150 -0
- package/services/frontend/.DS_Store +0 -0
- package/services/frontend/public/favicon.png +0 -0
- package/services/frontend/public/home-icon.png +0 -0
- package/services/frontend/public/logo.png +0 -0
- package/services/frontend/src/app/.DS_Store +0 -0
- package/services/frontend/src/app/components/BaseDataTable.vue +284 -0
- package/services/frontend/src/app/components/dynamicForm/DynamicForm.vue +207 -0
- package/services/frontend/src/app/components/dynamicForm/DynamicFormTypes.ts +63 -0
- package/services/frontend/src/app/components/dynamicForm/DynamicInput.vue +344 -0
- package/services/frontend/src/app/components/dynamicForm/TextDivider.vue +13 -0
- package/services/frontend/src/app/components/dynamicForm/testing/DynamicFormInputTesting.vue +477 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/CheckboxInput.vue +16 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/ColorInput.vue +52 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/CompositionInput.vue +32 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/IconInput.vue +40 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/ListInput.vue +51 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/LocalizedInput.vue +51 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/PasswordInput.vue +23 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/RecordInput.vue +91 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/SwitchInput.vue +17 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/TagsInput.vue +58 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/TriggerInput.vue +159 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/UserInput.vue +63 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/mdi-icons.ts +7002 -0
- package/services/frontend/src/app/components/dynamicForm/validators.ts +19 -0
- package/services/frontend/src/app/components/stepNavigator/StepNavigator.vue +331 -0
- package/services/frontend/src/app/components/stepNavigator/StepNavigatorTypes.ts +12 -0
- package/services/frontend/src/app/composables/useUserHelper.ts +254 -0
- package/services/frontend/src/app/composables/useVantaFog.ts +37 -0
- package/services/frontend/src/app/composables/useWorkbenchPlugins.ts +44 -0
- package/services/frontend/src/orchestrator/.DS_Store +0 -0
- package/services/frontend/src/orchestrator/LoginApp.vue +62 -0
- package/services/frontend/src/orchestrator/WelcomeScreen.vue +39 -0
- package/services/frontend/src/orchestrator/assets/styles/themes/dark.ts +39 -0
- package/services/frontend/src/orchestrator/assets/styles/themes/highContrast.ts +39 -0
- package/services/frontend/src/orchestrator/assets/styles/themes/light.ts +39 -0
- package/services/frontend/src/orchestrator/components/InitProjectScreen.vue +298 -0
- package/services/frontend/src/orchestrator/components/LoginScreen.vue +73 -0
- package/services/frontend/src/orchestrator/components/RacletteAdCorner.vue +313 -0
- package/services/frontend/src/orchestrator/components/layout/AppButton.vue +109 -0
- package/services/frontend/src/orchestrator/components/layout/AppHeadline.vue +46 -0
- package/services/frontend/src/orchestrator/components/layout/AppLabelledInput.vue +94 -0
- package/services/frontend/src/orchestrator/components/layout/AppModal.vue +28 -0
- package/services/frontend/src/orchestrator/components/layout/SvgButtonArrow.vue +14 -0
- package/services/frontend/src/orchestrator/composables/useWelcomeScreen.ts +19 -0
- package/yarn.lock +3435 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const emailValidator = (value: string): string | null => {
|
|
2
|
+
if (!value) return null
|
|
3
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
4
|
+
return emailRegex.test(value)
|
|
5
|
+
? null
|
|
6
|
+
: "workbench.dynamicInputs.validation.invalidEmail"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const urlValidator = (value: string): string | null => {
|
|
10
|
+
const { t } = useI18n()
|
|
11
|
+
|
|
12
|
+
if (!value) return null
|
|
13
|
+
try {
|
|
14
|
+
new URL(value)
|
|
15
|
+
return null
|
|
16
|
+
} catch {
|
|
17
|
+
return "workbench.dynamicInputs.validation.invalidUrl"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="tw:w-full">
|
|
3
|
+
<!-- Step Navigator Header -->
|
|
4
|
+
<v-navigation-drawer
|
|
5
|
+
v-if="steps.length > 1"
|
|
6
|
+
location="top"
|
|
7
|
+
permanent
|
|
8
|
+
:width="NAVIGATION_HEIGHT"
|
|
9
|
+
>
|
|
10
|
+
<div class="tw:size-full tw:flex tw:items-center tw:align-center">
|
|
11
|
+
<div class="tw:overflow-x-auto tw:w-full">
|
|
12
|
+
<div
|
|
13
|
+
class="tw:flex tw:items-center tw:justify-center tw:min-w-max tw:px-4"
|
|
14
|
+
>
|
|
15
|
+
<div class="tw:flex tw:items-center tw:space-x-4">
|
|
16
|
+
<template v-for="(step, index) in steps" :key="step.id">
|
|
17
|
+
<!-- Step Item -->
|
|
18
|
+
<div
|
|
19
|
+
@click="
|
|
20
|
+
allowFreeNavigation ? setCurrentStep(index + 1) : null
|
|
21
|
+
"
|
|
22
|
+
class="tw:flex tw:items-center tw:cursor-pointer tw:transition-all tw:duration-200 tw:group tw:flex-shrink-0"
|
|
23
|
+
:class="{
|
|
24
|
+
'tw:opacity-100': currentStepIndex >= index + 1,
|
|
25
|
+
'tw:opacity-60':
|
|
26
|
+
currentStepIndex < index + 1 &&
|
|
27
|
+
!isStepAccessible(index + 1),
|
|
28
|
+
}"
|
|
29
|
+
>
|
|
30
|
+
<!-- Step Circle -->
|
|
31
|
+
<div
|
|
32
|
+
class="tw:w-10 tw:h-10 tw:rounded-full tw:flex tw:items-center tw:justify-center tw:transition-all tw:duration-200 tw:border-2"
|
|
33
|
+
:class="{
|
|
34
|
+
'tw:bg-[#DD8B41] tw:border-[#DD8B41] tw:text-white':
|
|
35
|
+
currentStepIndex === index + 1,
|
|
36
|
+
'tw:bg-green-500 tw:border-green-500 tw:text-white':
|
|
37
|
+
currentStepIndex > index + 1,
|
|
38
|
+
'tw:bg-white tw:border-gray-300 tw:text-gray-500 group-hover:tw:border-blue-300':
|
|
39
|
+
currentStepIndex !== index + 1 &&
|
|
40
|
+
currentStepIndex < index + 1,
|
|
41
|
+
}"
|
|
42
|
+
>
|
|
43
|
+
<v-icon
|
|
44
|
+
v-if="currentStepIndex > index + 1"
|
|
45
|
+
class="tw:text-sm"
|
|
46
|
+
>mdi-check</v-icon
|
|
47
|
+
>
|
|
48
|
+
<span v-else class="tw:text-sm tw:font-semibold">
|
|
49
|
+
{{ index + 1 }}
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Step Label -->
|
|
54
|
+
<div class="tw:ml-3 tw:min-w-0">
|
|
55
|
+
<div
|
|
56
|
+
v-if="step.title"
|
|
57
|
+
class="tw:text-sm tw:font-medium tw:transition-colors tw:duration-200 tw:whitespace-nowrap"
|
|
58
|
+
:class="{
|
|
59
|
+
'tw:text-[#DD8B41]': currentStepIndex === index + 1,
|
|
60
|
+
'tw:text-green-600': currentStepIndex > index + 1,
|
|
61
|
+
'tw:text-gray-600 group-hover:tw:text-blue-600':
|
|
62
|
+
currentStepIndex !== index + 1,
|
|
63
|
+
}"
|
|
64
|
+
>
|
|
65
|
+
{{ step.title }}
|
|
66
|
+
</div>
|
|
67
|
+
<div
|
|
68
|
+
v-if="step.subtitle"
|
|
69
|
+
class="tw:text-xs tw:text-gray-500 tw:whitespace-nowrap"
|
|
70
|
+
>
|
|
71
|
+
{{ step.subtitle }}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- Connector Line (not for last step) -->
|
|
77
|
+
<div
|
|
78
|
+
v-if="index < steps.length - 1"
|
|
79
|
+
class="tw:w-12 tw:h-0.5 tw:transition-colors tw:duration-200 tw:flex-shrink-0"
|
|
80
|
+
:class="{
|
|
81
|
+
'tw:bg-green-500': currentStepIndex > index + 1,
|
|
82
|
+
'tw:bg-gray-300': currentStepIndex <= index + 1,
|
|
83
|
+
}"
|
|
84
|
+
></div>
|
|
85
|
+
</template>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</v-navigation-drawer>
|
|
91
|
+
|
|
92
|
+
<!-- Step Content Container -->
|
|
93
|
+
<!-- <div class="tw:min-h-full"> -->
|
|
94
|
+
<slot v-bind="{ currentStep, currentStepIndex }" />
|
|
95
|
+
<!-- </div> -->
|
|
96
|
+
|
|
97
|
+
<v-navigation-drawer location="bottom" permanent :width="84">
|
|
98
|
+
<div
|
|
99
|
+
class="tw:flex tw:h-full tw:items-center tw:justify-between tw:container tw:mx-auto tw:px-4"
|
|
100
|
+
>
|
|
101
|
+
<!-- Left side - Back button -->
|
|
102
|
+
<div>
|
|
103
|
+
<v-btn
|
|
104
|
+
v-if="steps.length > 1"
|
|
105
|
+
@click="previousStep"
|
|
106
|
+
:disabled="isFirstStep || isSaving"
|
|
107
|
+
color="primary"
|
|
108
|
+
prepend-icon="mdi-chevron-left"
|
|
109
|
+
variant="outlined"
|
|
110
|
+
:text="$t('workbench.global.back')"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="tw:flex tw:items-center tw:gap-3">
|
|
115
|
+
<v-btn
|
|
116
|
+
v-if="showSaveButton"
|
|
117
|
+
@click="save()"
|
|
118
|
+
:disabled="isSaving"
|
|
119
|
+
:loading="isSaving"
|
|
120
|
+
color="success"
|
|
121
|
+
prepend-icon="mdi-check"
|
|
122
|
+
:text="$t('workbench.global.save')"
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
<v-btn
|
|
126
|
+
v-if="!isLastStep"
|
|
127
|
+
@click="nextStep()"
|
|
128
|
+
:disabled="isSaving"
|
|
129
|
+
color="primary"
|
|
130
|
+
append-icon="mdi-chevron-right"
|
|
131
|
+
:text="$t('workbench.global.next_step')"
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<v-btn
|
|
135
|
+
v-else-if="!showSaveButton"
|
|
136
|
+
@click="save(true)"
|
|
137
|
+
:disabled="isSaving"
|
|
138
|
+
:loading="isSaving"
|
|
139
|
+
color="success"
|
|
140
|
+
prepend-icon="mdi-check-circle-outline"
|
|
141
|
+
:text="$t('workbench.global.save_finish')"
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</v-navigation-drawer>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
148
|
+
|
|
149
|
+
<script setup lang="ts">
|
|
150
|
+
import { ref, computed, watch } from "vue"
|
|
151
|
+
import { Step, StepChangeType } from "./StepNavigatorTypes"
|
|
152
|
+
|
|
153
|
+
const NAVIGATION_HEIGHT = 84
|
|
154
|
+
|
|
155
|
+
const props = withDefaults(
|
|
156
|
+
defineProps<{
|
|
157
|
+
steps: Step[]
|
|
158
|
+
initialStep?: number
|
|
159
|
+
allowFreeNavigation?: boolean
|
|
160
|
+
canProceed?: boolean
|
|
161
|
+
canFinish?: boolean
|
|
162
|
+
isSaving?: boolean
|
|
163
|
+
showSaveButton?: boolean
|
|
164
|
+
paddingTop?: boolean
|
|
165
|
+
paddingBottom?: boolean
|
|
166
|
+
}>(),
|
|
167
|
+
{
|
|
168
|
+
initialStep: 1,
|
|
169
|
+
allowFreeNavigation: true,
|
|
170
|
+
paddingBottom: true,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const emit = defineEmits<{
|
|
175
|
+
"step-changed": [StepChangeType]
|
|
176
|
+
"step-selected": [StepChangeType]
|
|
177
|
+
"validation-failed": [{ step: Step; stepIndex: number }]
|
|
178
|
+
save: [void]
|
|
179
|
+
"save-and-finish": [void]
|
|
180
|
+
}>()
|
|
181
|
+
|
|
182
|
+
const currentStepIndex = ref(props.initialStep)
|
|
183
|
+
|
|
184
|
+
const currentStep = computed(() => props.steps[currentStepIndex.value - 1])
|
|
185
|
+
const isFirstStep = computed(() => currentStepIndex.value === 1)
|
|
186
|
+
const isLastStep = computed(() => currentStepIndex.value === props.steps.length)
|
|
187
|
+
|
|
188
|
+
const isStepAccessible = (stepNumber: number) => {
|
|
189
|
+
if (props.allowFreeNavigation) return true
|
|
190
|
+
|
|
191
|
+
// Step 1 is always accessible
|
|
192
|
+
if (stepNumber === 1) return true
|
|
193
|
+
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const validateCurrentStep = () => {
|
|
198
|
+
const step = currentStep.value
|
|
199
|
+
if (!step?.validator) {
|
|
200
|
+
return true // No validator means validation passes
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
return step.validator()
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error("Step validation error:", error)
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const setCurrentStep = (step: number) => {
|
|
212
|
+
if (currentStepIndex.value === step) {
|
|
213
|
+
// Step is already active
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Validate step exists
|
|
218
|
+
if (step < 1 || step > props.steps.length) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// // Validate step access
|
|
223
|
+
// if (!isStepAccessible(step)) {
|
|
224
|
+
// return
|
|
225
|
+
// }
|
|
226
|
+
|
|
227
|
+
// If moving forward (not backward), validate current step first
|
|
228
|
+
if (step > currentStepIndex.value) {
|
|
229
|
+
const isValid = validateCurrentStep()
|
|
230
|
+
if (!isValid) {
|
|
231
|
+
emit("validation-failed", {
|
|
232
|
+
step: currentStep.value,
|
|
233
|
+
stepIndex: currentStepIndex.value - 1,
|
|
234
|
+
})
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
currentStepIndex.value = step
|
|
240
|
+
emit("step-changed", {
|
|
241
|
+
stepNumber: step,
|
|
242
|
+
stepData: props.steps[step - 1],
|
|
243
|
+
stepIndex: step - 1,
|
|
244
|
+
})
|
|
245
|
+
emit("step-selected", {
|
|
246
|
+
stepNumber: step,
|
|
247
|
+
stepData: props.steps[step - 1],
|
|
248
|
+
stepIndex: step - 1,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const save = (finish = false) => {
|
|
253
|
+
if (validateCurrentStep()) {
|
|
254
|
+
if (finish) {
|
|
255
|
+
emit("save-and-finish")
|
|
256
|
+
} else {
|
|
257
|
+
emit("save")
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Public methods (exposed to parent)
|
|
263
|
+
const goToStep = (step) => {
|
|
264
|
+
setCurrentStep(step)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const nextStep = () => {
|
|
268
|
+
if (currentStepIndex.value < props.steps.length) {
|
|
269
|
+
setCurrentStep(currentStepIndex.value + 1)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const previousStep = () => {
|
|
274
|
+
if (currentStepIndex.value > 1) {
|
|
275
|
+
setCurrentStep(currentStepIndex.value - 1)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Add validation method to exposed methods
|
|
280
|
+
const validateStep = () => {
|
|
281
|
+
return validateCurrentStep()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
defineExpose({
|
|
285
|
+
goToStep,
|
|
286
|
+
nextStep,
|
|
287
|
+
previousStep,
|
|
288
|
+
validateStep,
|
|
289
|
+
currentStep,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
watch(
|
|
293
|
+
() => props.initialStep,
|
|
294
|
+
(newStep) => {
|
|
295
|
+
if (newStep >= 1 && newStep <= props.steps.length) {
|
|
296
|
+
currentStepIndex.value = newStep
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
watch(
|
|
302
|
+
() => props.steps,
|
|
303
|
+
(newSteps) => {
|
|
304
|
+
if (newSteps.length > 0 && currentStepIndex.value > newSteps.length) {
|
|
305
|
+
currentStepIndex.value = 1
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{ immediate: true },
|
|
309
|
+
)
|
|
310
|
+
</script>
|
|
311
|
+
|
|
312
|
+
<style scoped>
|
|
313
|
+
/* Custom scrollbar for step navigator */
|
|
314
|
+
.tw:overflow-x-auto::-webkit-scrollbar {
|
|
315
|
+
height: 4px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.tw:overflow-x-auto::-webkit-scrollbar-track {
|
|
319
|
+
background: #f1f5f9;
|
|
320
|
+
border-radius: 2px;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.tw:overflow-x-auto::-webkit-scrollbar-thumb {
|
|
324
|
+
background: #cbd5e1;
|
|
325
|
+
border-radius: 2px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.tw:overflow-x-auto::-webkit-scrollbar-thumb:hover {
|
|
329
|
+
background: #94a3b8;
|
|
330
|
+
}
|
|
331
|
+
</style>
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { User } from "@raclettejs/core"
|
|
2
|
+
import { computed, type ComputedRef } from "vue"
|
|
3
|
+
|
|
4
|
+
// TODO: could be moved to orchestrator once we split the core
|
|
5
|
+
export const useUserHelper = (user: User | ComputedRef<User>) => {
|
|
6
|
+
// Handle both reactive and non-reactive user objects
|
|
7
|
+
const userRef = computed(() =>
|
|
8
|
+
typeof user === "object" && "value" in user ? user.value : user,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Display name with fallback logic
|
|
12
|
+
const displayName = computed(() => {
|
|
13
|
+
const u = userRef.value
|
|
14
|
+
if (!u) return "Unknown User"
|
|
15
|
+
|
|
16
|
+
const parts = []
|
|
17
|
+
if (u.title) parts.push(u.title)
|
|
18
|
+
if (u.firstname) parts.push(u.firstname)
|
|
19
|
+
if (u.lastname) parts.push(u.lastname)
|
|
20
|
+
|
|
21
|
+
if (parts.length > 0) {
|
|
22
|
+
return parts.join(" ")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return u.email?.split("@")[0] || "Unknown User"
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Initials for avatar fallbacks
|
|
29
|
+
const initials = computed(() => {
|
|
30
|
+
const u = userRef.value
|
|
31
|
+
if (!u) return "?"
|
|
32
|
+
|
|
33
|
+
if (u.firstname && u.lastname) {
|
|
34
|
+
return `${u.firstname[0]}${u.lastname[0]}`.toUpperCase()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (u.firstname) return u.firstname[0].toUpperCase()
|
|
38
|
+
if (u.lastname) return u.lastname[0].toUpperCase()
|
|
39
|
+
if (u.email) return u.email[0].toUpperCase()
|
|
40
|
+
|
|
41
|
+
return "?"
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Full name (more formal)
|
|
45
|
+
const fullName = computed(() => {
|
|
46
|
+
const u = userRef.value
|
|
47
|
+
|
|
48
|
+
if (!u) return ""
|
|
49
|
+
|
|
50
|
+
if (u.firstname && u.lastname) {
|
|
51
|
+
return `${u.firstname} ${u.lastname}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return u.firstname || u.lastname || ""
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Full name with title
|
|
58
|
+
const formalName = computed(() => {
|
|
59
|
+
const u = userRef.value
|
|
60
|
+
if (!u) return ""
|
|
61
|
+
|
|
62
|
+
const parts = []
|
|
63
|
+
if (u.title) parts.push(u.title)
|
|
64
|
+
if (u.firstname) parts.push(u.firstname)
|
|
65
|
+
if (u.lastname) parts.push(u.lastname)
|
|
66
|
+
|
|
67
|
+
return parts.join(" ")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// User status
|
|
71
|
+
const isActive = computed(() => {
|
|
72
|
+
return userRef.value?.active ?? false
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const isBlocked = computed(() => {
|
|
76
|
+
return userRef.value?.blocked ?? false
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const isDeleted = computed(() => {
|
|
80
|
+
return userRef.value?.isDeleted ?? false
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const isEmailVerified = computed(() => {
|
|
84
|
+
return userRef.value?.email_verified ?? false
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// User tags
|
|
88
|
+
const userTags = computed(() => {
|
|
89
|
+
return userRef.value?.tags || []
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Time-based computed properties
|
|
93
|
+
const memberSince = computed(() => {
|
|
94
|
+
const u = userRef.value
|
|
95
|
+
if (!u?.createdAt) return null
|
|
96
|
+
|
|
97
|
+
const date = new Date(u.createdAt)
|
|
98
|
+
return date.toLocaleDateString()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const lastUpdated = computed(() => {
|
|
102
|
+
const u = userRef.value
|
|
103
|
+
if (!u?.updatedAt) return null
|
|
104
|
+
|
|
105
|
+
const date = new Date(u.updatedAt)
|
|
106
|
+
const now = new Date()
|
|
107
|
+
const diffMs = now.getTime() - date.getTime()
|
|
108
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
109
|
+
|
|
110
|
+
if (diffDays === 0) return "Today"
|
|
111
|
+
if (diffDays === 1) return "Yesterday"
|
|
112
|
+
if (diffDays < 7) return `${diffDays} days ago`
|
|
113
|
+
|
|
114
|
+
return date.toLocaleDateString()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const passwordLastUpdated = computed(() => {
|
|
118
|
+
const u = userRef.value
|
|
119
|
+
if (!u?.passwordUpdatedAt) return null
|
|
120
|
+
|
|
121
|
+
const date = new Date(u.passwordUpdatedAt)
|
|
122
|
+
return date.toLocaleDateString()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const hasSignedTerms = computed(() => {
|
|
126
|
+
return !!userRef.value?.signedTermsAt
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const termsSignedAt = computed(() => {
|
|
130
|
+
const u = userRef.value
|
|
131
|
+
if (!u?.signedTermsAt) return null
|
|
132
|
+
|
|
133
|
+
const date = new Date(u.signedTermsAt)
|
|
134
|
+
return date.toLocaleDateString()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Utility methods
|
|
138
|
+
const hasTag = (tag: string) => {
|
|
139
|
+
return userRef.value?.tags?.includes(tag) ?? false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const hasAnyTag = (tags: string[]) => {
|
|
143
|
+
const userTags = userRef.value?.tags || []
|
|
144
|
+
return tags.some((tag) => userTags.includes(tag))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const hasAllTags = (tags: string[]) => {
|
|
148
|
+
const userTags = userRef.value?.tags || []
|
|
149
|
+
return tags.every((tag) => userTags.includes(tag))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const getContactEmail = () => {
|
|
153
|
+
return userRef.value?.email || null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const canLogin = () => {
|
|
157
|
+
const u = userRef.value
|
|
158
|
+
return u?.active && !u?.blocked && !u?.isDeleted
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const needsPasswordReset = () => {
|
|
162
|
+
return !!userRef.value?.resetToken
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const isAccountOwner = () => {
|
|
166
|
+
const u = userRef.value
|
|
167
|
+
return u?.owner === u?._id
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const getAccountStatus = () => {
|
|
171
|
+
const u = userRef.value
|
|
172
|
+
if (!u) return "unknown"
|
|
173
|
+
|
|
174
|
+
if (u.isDeleted) return "deleted"
|
|
175
|
+
if (u.blocked) return "blocked"
|
|
176
|
+
if (!u.active) return "inactive"
|
|
177
|
+
if (!u.email_verified) return "unverified"
|
|
178
|
+
|
|
179
|
+
return "active"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const getAccountStatusColor = () => {
|
|
183
|
+
const status = getAccountStatus()
|
|
184
|
+
const colors = {
|
|
185
|
+
active: "#22c55e",
|
|
186
|
+
inactive: "#eab308",
|
|
187
|
+
blocked: "#ef4444",
|
|
188
|
+
deleted: "#6b7280",
|
|
189
|
+
unverified: "#f97316",
|
|
190
|
+
unknown: "#6b7280",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return colors[status as keyof typeof colors]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Generate a consistent color for the user (useful for avatar backgrounds)
|
|
197
|
+
const userColor = computed(() => {
|
|
198
|
+
const u = userRef.value
|
|
199
|
+
if (!u) return "#gray"
|
|
200
|
+
|
|
201
|
+
const colors = [
|
|
202
|
+
"#ef4444",
|
|
203
|
+
"#f97316",
|
|
204
|
+
"#eab308",
|
|
205
|
+
"#22c55e",
|
|
206
|
+
"#06b6d4",
|
|
207
|
+
"#3b82f6",
|
|
208
|
+
"#8b5cf6",
|
|
209
|
+
"#ec4899",
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
const hash = u._id
|
|
213
|
+
.toString()
|
|
214
|
+
.split("")
|
|
215
|
+
.reduce((acc, char) => {
|
|
216
|
+
return acc + char.charCodeAt(0)
|
|
217
|
+
}, 0)
|
|
218
|
+
|
|
219
|
+
return colors[hash % colors.length]
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
// Computed properties
|
|
224
|
+
displayName,
|
|
225
|
+
initials,
|
|
226
|
+
fullName,
|
|
227
|
+
formalName,
|
|
228
|
+
isActive,
|
|
229
|
+
isBlocked,
|
|
230
|
+
isDeleted,
|
|
231
|
+
isEmailVerified,
|
|
232
|
+
userTags,
|
|
233
|
+
memberSince,
|
|
234
|
+
lastUpdated,
|
|
235
|
+
passwordLastUpdated,
|
|
236
|
+
hasSignedTerms,
|
|
237
|
+
termsSignedAt,
|
|
238
|
+
userColor,
|
|
239
|
+
|
|
240
|
+
// Methods
|
|
241
|
+
hasTag,
|
|
242
|
+
hasAnyTag,
|
|
243
|
+
hasAllTags,
|
|
244
|
+
getContactEmail,
|
|
245
|
+
canLogin,
|
|
246
|
+
needsPasswordReset,
|
|
247
|
+
isAccountOwner,
|
|
248
|
+
getAccountStatus,
|
|
249
|
+
getAccountStatusColor,
|
|
250
|
+
|
|
251
|
+
// Raw user ref for direct access
|
|
252
|
+
user: userRef,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as THREE from "three"
|
|
2
|
+
import FOG from "vanta/dist/vanta.fog.min.js"
|
|
3
|
+
import { onBeforeUnmount, onMounted, ref, type Ref } from "vue"
|
|
4
|
+
|
|
5
|
+
export default (elementRef: Ref<any>) => {
|
|
6
|
+
let vantaEffect: any
|
|
7
|
+
|
|
8
|
+
onMounted(async () => {
|
|
9
|
+
// Now we can access the mounted element
|
|
10
|
+
const element = elementRef.value?.$el || elementRef.value
|
|
11
|
+
|
|
12
|
+
if (!element) {
|
|
13
|
+
console.error("Element not found for Vanta effect")
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const pixelRatio = window.devicePixelRatio || 1
|
|
18
|
+
|
|
19
|
+
vantaEffect = FOG({
|
|
20
|
+
el: element,
|
|
21
|
+
THREE,
|
|
22
|
+
highlightColor: 0xe8bc8b,
|
|
23
|
+
midtoneColor: 0xe6c485,
|
|
24
|
+
lowlightColor: 0xa5a5c3,
|
|
25
|
+
baseColor: 0xede5db,
|
|
26
|
+
blurFactor: 0.4,
|
|
27
|
+
speed: 0.1,
|
|
28
|
+
zoom: 0.3 / pixelRatio, // makes it more evenly looking across screens
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
onBeforeUnmount(() => {
|
|
33
|
+
if (vantaEffect) {
|
|
34
|
+
vantaEffect.destroy()
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PluginMetadata } from "@raclettejs/core"
|
|
2
|
+
import { registeredPlugins } from "@raclettejs/core/frontend"
|
|
3
|
+
import { widgetLoader } from "@raclettejs/core/orchestrator/helpers/widgetLoader"
|
|
4
|
+
import { generatePluginKey } from "@shared/helper/pluginHelper"
|
|
5
|
+
import { forEachObjIndexed } from "ramda"
|
|
6
|
+
import { computed } from "vue"
|
|
7
|
+
|
|
8
|
+
const PLUGIN_SETTINGS_WIDGET_NAME = "PluginSettings"
|
|
9
|
+
|
|
10
|
+
const PLUGIN_METADATA_FILES: { [key: string]: { default: PluginMetadata } } =
|
|
11
|
+
import.meta.glob<{ default: PluginMetadata }>(
|
|
12
|
+
"@racletteCore/../../external-app-plugins/**/raclette.plugin.ts",
|
|
13
|
+
{
|
|
14
|
+
eager: true,
|
|
15
|
+
},
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export default () => {
|
|
19
|
+
const pluginMetadataFiles = computed(() => {
|
|
20
|
+
const pluginFiles: PluginMetadata[] = []
|
|
21
|
+
forEachObjIndexed(({ default: pluginFile }) => {
|
|
22
|
+
const fileContent = pluginFile
|
|
23
|
+
fileContent.pluginKey = generatePluginKey(
|
|
24
|
+
pluginFile.name,
|
|
25
|
+
pluginFile.author,
|
|
26
|
+
)
|
|
27
|
+
pluginFiles.push(fileContent)
|
|
28
|
+
}, PLUGIN_METADATA_FILES)
|
|
29
|
+
return pluginFiles
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const getWorkbenchPlugin = ({ pluginKey }: { pluginKey: string }) => {
|
|
33
|
+
// Always load "PluginSettingsWidget"
|
|
34
|
+
return widgetLoader({
|
|
35
|
+
pluginKey,
|
|
36
|
+
name: PLUGIN_SETTINGS_WIDGET_NAME,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const hasPluginSettingsWidget = (pluginKey: string) =>
|
|
41
|
+
!!registeredPlugins?.[pluginKey]?.widgets?.[PLUGIN_SETTINGS_WIDGET_NAME]
|
|
42
|
+
|
|
43
|
+
return { pluginMetadataFiles, getWorkbenchPlugin, hasPluginSettingsWidget }
|
|
44
|
+
}
|
|
Binary file
|