@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.
Files changed (89) hide show
  1. package/README.md +1 -0
  2. package/config/compositions.js +282 -0
  3. package/config/interactionLinks.js +164 -0
  4. package/i18n/de-DE.json +245 -0
  5. package/i18n/en-EU.json +245 -0
  6. package/i18n/sk-SK.json +245 -0
  7. package/package.json +63 -0
  8. package/packages.json +33 -0
  9. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionConfiguration.vue +140 -0
  10. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionIntegration.vue +3 -0
  11. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionWidgetsLayout.vue +129 -0
  12. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetContextMenu.vue +53 -0
  13. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetIcon.vue +99 -0
  14. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetList.vue +122 -0
  15. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetRenderer.vue +65 -0
  16. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetSettings.vue +96 -0
  17. package/plugins/pacifico__compositions/frontend/widgets/CompositionCreateWidget.vue +51 -0
  18. package/plugins/pacifico__compositions/frontend/widgets/CompositionEditWidget.vue +66 -0
  19. package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +132 -0
  20. package/plugins/pacifico__compositions/raclette.plugin.ts +8 -0
  21. package/plugins/pacifico__core/backend/index.ts +25 -0
  22. package/plugins/pacifico__core/backend/routes.ts +220 -0
  23. package/plugins/pacifico__core/raclette.plugin.ts +9 -0
  24. package/plugins/pacifico__interactionLinks/frontend/components/InteractionLinkConfiguration.vue +145 -0
  25. package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkCreateWidget.vue +48 -0
  26. package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkEditWidget.vue +66 -0
  27. package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +126 -0
  28. package/plugins/pacifico__interactionLinks/raclette.plugin.ts +8 -0
  29. package/plugins/pacifico__plugins/frontend/index.ts +42 -0
  30. package/plugins/pacifico__plugins/frontend/widgets/PluginDetailWidget.vue +26 -0
  31. package/plugins/pacifico__plugins/frontend/widgets/PluginListWidget.vue +59 -0
  32. package/plugins/pacifico__plugins/raclette.plugin.ts +8 -0
  33. package/plugins/pacifico__tags/frontend/components/TagConfiguration.vue +99 -0
  34. package/plugins/pacifico__tags/frontend/widgets/TagCreateWidget.vue +43 -0
  35. package/plugins/pacifico__tags/frontend/widgets/TagEditWidget.vue +64 -0
  36. package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +71 -0
  37. package/plugins/pacifico__tags/raclette.plugin.ts +8 -0
  38. package/plugins/pacifico__users/frontend/components/UserConfiguration.vue +110 -0
  39. package/plugins/pacifico__users/frontend/widgets/UserCreateWidget.vue +48 -0
  40. package/plugins/pacifico__users/frontend/widgets/UserEditWidget.vue +64 -0
  41. package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +86 -0
  42. package/plugins/pacifico__users/raclette.plugin.ts +8 -0
  43. package/raclette.config.js +150 -0
  44. package/services/frontend/.DS_Store +0 -0
  45. package/services/frontend/public/favicon.png +0 -0
  46. package/services/frontend/public/home-icon.png +0 -0
  47. package/services/frontend/public/logo.png +0 -0
  48. package/services/frontend/src/app/.DS_Store +0 -0
  49. package/services/frontend/src/app/components/BaseDataTable.vue +284 -0
  50. package/services/frontend/src/app/components/dynamicForm/DynamicForm.vue +207 -0
  51. package/services/frontend/src/app/components/dynamicForm/DynamicFormTypes.ts +63 -0
  52. package/services/frontend/src/app/components/dynamicForm/DynamicInput.vue +344 -0
  53. package/services/frontend/src/app/components/dynamicForm/TextDivider.vue +13 -0
  54. package/services/frontend/src/app/components/dynamicForm/testing/DynamicFormInputTesting.vue +477 -0
  55. package/services/frontend/src/app/components/dynamicForm/typedInputs/CheckboxInput.vue +16 -0
  56. package/services/frontend/src/app/components/dynamicForm/typedInputs/ColorInput.vue +52 -0
  57. package/services/frontend/src/app/components/dynamicForm/typedInputs/CompositionInput.vue +32 -0
  58. package/services/frontend/src/app/components/dynamicForm/typedInputs/IconInput.vue +40 -0
  59. package/services/frontend/src/app/components/dynamicForm/typedInputs/ListInput.vue +51 -0
  60. package/services/frontend/src/app/components/dynamicForm/typedInputs/LocalizedInput.vue +51 -0
  61. package/services/frontend/src/app/components/dynamicForm/typedInputs/PasswordInput.vue +23 -0
  62. package/services/frontend/src/app/components/dynamicForm/typedInputs/RecordInput.vue +91 -0
  63. package/services/frontend/src/app/components/dynamicForm/typedInputs/SwitchInput.vue +17 -0
  64. package/services/frontend/src/app/components/dynamicForm/typedInputs/TagsInput.vue +58 -0
  65. package/services/frontend/src/app/components/dynamicForm/typedInputs/TriggerInput.vue +159 -0
  66. package/services/frontend/src/app/components/dynamicForm/typedInputs/UserInput.vue +63 -0
  67. package/services/frontend/src/app/components/dynamicForm/typedInputs/mdi-icons.ts +7002 -0
  68. package/services/frontend/src/app/components/dynamicForm/validators.ts +19 -0
  69. package/services/frontend/src/app/components/stepNavigator/StepNavigator.vue +331 -0
  70. package/services/frontend/src/app/components/stepNavigator/StepNavigatorTypes.ts +12 -0
  71. package/services/frontend/src/app/composables/useUserHelper.ts +254 -0
  72. package/services/frontend/src/app/composables/useVantaFog.ts +37 -0
  73. package/services/frontend/src/app/composables/useWorkbenchPlugins.ts +44 -0
  74. package/services/frontend/src/orchestrator/.DS_Store +0 -0
  75. package/services/frontend/src/orchestrator/LoginApp.vue +62 -0
  76. package/services/frontend/src/orchestrator/WelcomeScreen.vue +39 -0
  77. package/services/frontend/src/orchestrator/assets/styles/themes/dark.ts +39 -0
  78. package/services/frontend/src/orchestrator/assets/styles/themes/highContrast.ts +39 -0
  79. package/services/frontend/src/orchestrator/assets/styles/themes/light.ts +39 -0
  80. package/services/frontend/src/orchestrator/components/InitProjectScreen.vue +298 -0
  81. package/services/frontend/src/orchestrator/components/LoginScreen.vue +73 -0
  82. package/services/frontend/src/orchestrator/components/RacletteAdCorner.vue +313 -0
  83. package/services/frontend/src/orchestrator/components/layout/AppButton.vue +109 -0
  84. package/services/frontend/src/orchestrator/components/layout/AppHeadline.vue +46 -0
  85. package/services/frontend/src/orchestrator/components/layout/AppLabelledInput.vue +94 -0
  86. package/services/frontend/src/orchestrator/components/layout/AppModal.vue +28 -0
  87. package/services/frontend/src/orchestrator/components/layout/SvgButtonArrow.vue +14 -0
  88. package/services/frontend/src/orchestrator/composables/useWelcomeScreen.ts +19 -0
  89. 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,12 @@
1
+ export type Step<T = string> = {
2
+ id: T
3
+ title?: string
4
+ subtitle?: string
5
+ validator?: () => boolean
6
+ }
7
+
8
+ export type StepChangeType<T = string> = {
9
+ stepNumber: number
10
+ stepData: Step<T>
11
+ stepIndex: number
12
+ }
@@ -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
+ }