diff --git a/apps/dashboard/src/lib/features/app/init.svelte.ts b/apps/dashboard/src/lib/features/app/init.svelte.ts index 5f1f5333..df283f10 100644 --- a/apps/dashboard/src/lib/features/app/init.svelte.ts +++ b/apps/dashboard/src/lib/features/app/init.svelte.ts @@ -10,6 +10,7 @@ import { get } from 'svelte/store'; import { goto } from '$app/navigation'; import { handleLocaleChange } from '$lib/utils/functions/translations'; import { identifyPosthogUser } from '$lib/utils/services/posthog'; +import { identifyUserJotUser } from '$lib/utils/services/userjot'; import { isOrgStudent } from '$lib/utils/store/app'; import { isPublicRoute } from '$lib/utils/functions/routes/isPublicRoute'; import { licenseApi } from '$features/license/api/license.svelte'; @@ -231,6 +232,13 @@ class AppInitApi extends BaseApi { email: profileStore.email, name: profileStore.fullname }); + + identifyUserJotUser({ + id: profileStore.id, + email: profileStore.email, + fullname: profileStore.fullname, + avatarUrl: profileStore.avatarUrl + }); } reset() { diff --git a/apps/dashboard/src/lib/features/course/components/lesson/video/add-video-modal.svelte b/apps/dashboard/src/lib/features/course/components/lesson/video/add-video-modal.svelte index 8c9e5c9c..6251a787 100644 --- a/apps/dashboard/src/lib/features/course/components/lesson/video/add-video-modal.svelte +++ b/apps/dashboard/src/lib/features/course/components/lesson/video/add-video-modal.svelte @@ -24,13 +24,19 @@ { - if (!details.open && $lessonVideoUpload.isUploading) return; - if (!details.open) onClose(); + onOpenChange={(open) => { + if (!open) onClose(); }} > { + if ($lessonVideoUpload.isUploading) e.preventDefault(); + }} + onInteractOutside={(e) => { + if ($lessonVideoUpload.isUploading) e.preventDefault(); + }} onCloseAutoFocus={(e) => { if ($lessonVideoUpload.isUploading) e.preventDefault(); }} @@ -42,7 +48,11 @@ {#each tabs as item (item.value)} - + {#if $isFreePlan && item.value === 3} {:else} diff --git a/apps/dashboard/src/lib/features/course/pages/compliance.svelte b/apps/dashboard/src/lib/features/course/pages/compliance.svelte index 2cb88d7c..eb1d8a27 100644 --- a/apps/dashboard/src/lib/features/course/pages/compliance.svelte +++ b/apps/dashboard/src/lib/features/course/pages/compliance.svelte @@ -581,12 +581,22 @@ {:else} - + {/if} {:else} - + {/if} diff --git a/apps/dashboard/src/lib/utils/functions/appSetup.ts b/apps/dashboard/src/lib/utils/functions/appSetup.ts index a4e8fa22..eab92c86 100644 --- a/apps/dashboard/src/lib/utils/functions/appSetup.ts +++ b/apps/dashboard/src/lib/utils/functions/appSetup.ts @@ -1,6 +1,7 @@ import { PUBLIC_IS_SELFHOSTED } from '$env/static/public'; import { initPosthog, type PosthogBootstrapUser } from '$lib/utils/services/posthog'; import { initUmami } from '$lib/utils/services/umami'; +import { initUserJot } from '$lib/utils/services/userjot'; import { licenseApi } from '$features/license/api/license.svelte'; let isInitialized = false; @@ -11,6 +12,7 @@ export function setupAnalytics(user?: PosthogBootstrapUser) { initPosthog(user); initUmami(); + initUserJot(); } /** Checks if this is cloud deployment and initializes analytics */ diff --git a/apps/dashboard/src/lib/utils/services/userjot/index.ts b/apps/dashboard/src/lib/utils/services/userjot/index.ts new file mode 100644 index 00000000..a5c7d0ce --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/userjot/index.ts @@ -0,0 +1,77 @@ +import { dev } from '$app/environment'; +import { get } from 'svelte/store'; +import { globalStore } from '$lib/utils/store/app'; +import { licenseApi } from '$features/license/api/license.svelte'; + +const USERJOT_APP_ID = 'cm4a6vcmp00jpmdb5n66rmkzz'; + +let isInitialized = false; + +function isWidgetAllowed(): boolean { + if (dev) return false; + if (licenseApi.hasAccess('no-tracking')) return false; + if (get(globalStore).isOrgSite) return false; + + return true; +} + +function ensureSdkLoaded(): void { + if (window.uj) return; + + window.$ujq = window.$ujq || []; + window.uj = + window.uj || + (new Proxy({} as Window['uj'], { + get: + (_, prop) => + (...args: unknown[]) => + window.$ujq.push([prop, ...args]) + }) as Window['uj']); + + const script = document.createElement('script'); + script.type = 'module'; + script.async = true; + script.src = 'https://cdn.userjot.com/sdk/v2/uj.js'; + const nonce = document.querySelector('script[nonce]')?.getAttribute('nonce'); + if (nonce) script.setAttribute('nonce', nonce); + document.head.appendChild(script); +} + +export function initUserJot(): void { + if (isInitialized) return; + if (!isWidgetAllowed()) return; + + ensureSdkLoaded(); + + window.uj.init(USERJOT_APP_ID, { + widget: true, + position: 'right', + theme: 'auto' + }); + + isInitialized = true; +} + +type UserJotIdentity = { + id: string; + email?: string; + fullname?: string | null; + avatarUrl?: string | null; +}; + +export function identifyUserJotUser({ id, email, fullname, avatarUrl }: UserJotIdentity): void { + if (!isWidgetAllowed()) return; + + ensureSdkLoaded(); + + const [firstName, ...rest] = (fullname ?? '').trim().split(/\s+/); + const lastName = rest.join(' '); + + window.uj.identify({ + id, + email, + firstName: firstName || undefined, + lastName: lastName || undefined, + avatar: avatarUrl ?? undefined + }); +} diff --git a/apps/dashboard/src/lib/utils/services/userjot/types.d.ts b/apps/dashboard/src/lib/utils/services/userjot/types.d.ts new file mode 100644 index 00000000..89176d56 --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/userjot/types.d.ts @@ -0,0 +1,19 @@ +declare global { + interface Window { + $ujq: unknown[]; + uj: { + init: ( + appId: string, + options?: { + widget?: boolean; + position?: 'left' | 'right'; + theme?: 'auto' | 'light' | 'dark'; + } + ) => void; + identify: (user: { id: string; email?: string; firstName?: string; lastName?: string; avatar?: string }) => void; + [method: string]: (...args: unknown[]) => void; + }; + } +} + +export {}; diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index c26020aa..dc163ad9 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -1035,7 +1035,8 @@ "actions": { "edit": "Edit", "where_used": "Where used", - "download": "Download" + "download": "Download", + "manage_thumbnails": "Manage thumbnails" }, "pagination": { "previous": "Previous", @@ -1061,6 +1062,21 @@ "thumbnail_alt": "Asset thumbnail", "status": "Status" }, + "thumbnails": { + "manage_title": "Manage thumbnails", + "manage_description": "Preview the video, pick one of the auto-generated frames, or upload your own.", + "preview_unavailable": "Preview unavailable", + "generated": "Generated thumbnails", + "slot_start": "Start", + "slot_middle": "Middle", + "slot_end": "End", + "none_generated_warning": "No thumbnails generated yet. Try regenerating or upload a custom image.", + "upload_custom": "Upload custom thumbnail", + "custom_selected_hint": "Custom upload will be saved when you save your selection.", + "custom_preview_alt": "Custom thumbnail preview", + "regenerate": "Regenerate", + "save_selection": "Save thumbnail" + }, "usage": { "title": "Where Used", "description": "Current usage references for", @@ -1597,6 +1613,7 @@ "create_course": "Create Course", "view_site": "View Site", "visit_site": "Visit Website", + "open_academy": "Open Academy", "current_lesson": "Currently Learning", "currently_learning": "Currently Learning", "learner": "Learner", @@ -2957,6 +2974,7 @@ "no_active_cycle": "No active compliance cycle", "empty_history": "No compliance history has been recorded yet.", "empty_overview": "No learner compliance records are available yet.", + "empty_overview_description": "Keep your employees compliant and track who is behind.", "not_available": "Not available", "tabs": { "overview": "Overview", @@ -3868,6 +3886,10 @@ "thumbnail_upload_failed": "Failed to upload thumbnail", "thumbnail_too_large": "Thumbnail image is too large", "thumbnail_invalid": "Invalid thumbnail file", + "thumbnail_updated": "Thumbnail updated", + "thumbnail_update_failed": "Failed to update thumbnail", + "thumbnail_regen_started": "Regenerating thumbnails…", + "thumbnail_regen_failed": "Failed to regenerate thumbnails", "transcription_started": "Transcription started", "transcription_start_failed": "Could not start transcription", "transcription_completed": "Transcription finished", diff --git a/apps/docs/app/lib/layout.shared.tsx b/apps/docs/app/lib/layout.shared.tsx index 0d8c496b..b73f552e 100644 --- a/apps/docs/app/lib/layout.shared.tsx +++ b/apps/docs/app/lib/layout.shared.tsx @@ -30,6 +30,15 @@ export function baseOptions(): BaseLayoutProps { url: 'https://dub.sh/ciodemo', external: true }, + { + text: ( +
+ Feedback +
+ ), + url: 'https://feedback.classroomio.com', + external: true + }, { text: (
diff --git a/apps/website/src/lib/components/footer.svelte b/apps/website/src/lib/components/footer.svelte index 36c4d73d..c5fd0270 100644 --- a/apps/website/src/lib/components/footer.svelte +++ b/apps/website/src/lib/components/footer.svelte @@ -176,8 +176,11 @@

Company

  • - RoadmapRoadmap
  • diff --git a/apps/website/src/routes/(marketing)/compliance-training/+page.svelte b/apps/website/src/routes/(marketing)/compliance-training/+page.svelte index fac93e4b..555423a4 100644 --- a/apps/website/src/routes/(marketing)/compliance-training/+page.svelte +++ b/apps/website/src/routes/(marketing)/compliance-training/+page.svelte @@ -135,6 +135,16 @@ Compliance Training | ClassroomIO + + + + -
    +
    {#if isYouTube && youtubeVideoId}
    {:else} -