chore: marketing updates and video player improvement

This commit is contained in:
Rotimi Best
2026-05-22 01:28:18 +01:00
parent f0fad9512c
commit 4fdd4115b9
13 changed files with 211 additions and 17 deletions
@@ -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() {
@@ -24,13 +24,19 @@
<Dialog.Root
bind:open={$lessonVideoUpload.isModalOpen}
onOpenChange={(details) => {
if (!details.open && $lessonVideoUpload.isUploading) return;
if (!details.open) onClose();
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<Dialog.Content
class="flex max-h-[680px] max-w-2xl! flex-col overflow-hidden"
showCloseButton={!$lessonVideoUpload.isUploading}
onEscapeKeydown={(e) => {
if ($lessonVideoUpload.isUploading) e.preventDefault();
}}
onInteractOutside={(e) => {
if ($lessonVideoUpload.isUploading) e.preventDefault();
}}
onCloseAutoFocus={(e) => {
if ($lessonVideoUpload.isUploading) e.preventDefault();
}}
@@ -42,7 +48,11 @@
<UnderlineTabs.Root bind:value={currentTab} class="">
<UnderlineTabs.List>
{#each tabs as item (item.value)}
<UnderlineTabs.Trigger value={String(item.value)} class="ui:flex ui:items-center ui:gap-2">
<UnderlineTabs.Trigger
value={String(item.value)}
disabled={$lessonVideoUpload.isUploading && currentTab !== String(item.value)}
class="ui:flex ui:items-center ui:gap-2"
>
{#if $isFreePlan && item.value === 3}
<ZapIcon size={16} class="filled" />
{:else}
@@ -581,12 +581,22 @@
</Table.Body>
</Table.Root>
{:else}
<Empty title={$t('course.navItem.compliance.empty_overview')} icon={ShieldCheckIcon} variant="page" />
<Empty
title={$t('course.navItem.compliance.empty_overview')}
description={$t('course.navItem.compliance.empty_overview_description')}
icon={ShieldCheckIcon}
variant="page"
/>
{/if}
</Tabs.Content>
</Tabs.Root>
{:else}
<Empty title={$t('course.navItem.compliance.empty_overview')} icon={ShieldCheckIcon} variant="page" />
<Empty
title={$t('course.navItem.compliance.empty_overview')}
description={$t('course.navItem.compliance.empty_overview_description')}
icon={ShieldCheckIcon}
variant="page"
/>
{/if}
<Dialog.Root bind:open={isBulkActionModalOpen} onOpenChange={handleBulkActionModalChange}>
@@ -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 */
@@ -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
});
}
@@ -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 {};
@@ -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",
+9
View File
@@ -30,6 +30,15 @@ export function baseOptions(): BaseLayoutProps {
url: 'https://dub.sh/ciodemo',
external: true
},
{
text: (
<div className="flex items-center gap-2">
Feedback <SquareArrowOutUpRight />
</div>
),
url: 'https://feedback.classroomio.com',
external: true
},
{
text: (
<div className="flex items-center gap-2">
@@ -176,8 +176,11 @@
<p class="ui:text-foreground mb-4 text-sm font-semibold">Company</p>
<ul class="space-y-3">
<li>
<a href="/roadmap" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>Roadmap</a
<a
href="https://feedback.classroomio.com"
target="_blank"
rel="noopener"
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Roadmap</a
>
</li>
<li>
@@ -135,6 +135,16 @@
<title>Compliance Training | ClassroomIO</title>
<meta
name="description"
content="Keep your employees compliant and track who's falling behind. Turn policies and SOPs into certified training with renewals, grace periods, and audit evidence."
/>
<meta property="og:title" content="Keep your employees compliant and track who's falling behind." />
<meta
property="og:description"
content="Turn policies and SOPs into certified training. Track compliance, renewals, grace periods, and audit evidence in one place."
/>
<meta name="twitter:title" content="Keep your employees compliant and track who's falling behind." />
<meta
name="twitter:description"
content="Turn policies and SOPs into certified training. Track compliance, renewals, grace periods, and audit evidence in one place."
/>
<script
@@ -154,8 +164,8 @@
Compliance Training
</Badge>
<h1 class="text-[clamp(2rem,3.4vw,2.8rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
Audit-ready training,
<em class="text-blue-700 not-italic">without the spreadsheets.</em>
Keep your employees compliant and
<em class="text-blue-700 not-italic">track who's falling behind.</em>
</h1>
<HeroSubtitle class="mx-auto">
Turn policies, SOPs, docs, and videos into assigned courses with quizzes, certificates, renewal rules, and
@@ -341,7 +351,7 @@
</section>
<PageSignupCTA
header="Stop chasing compliance manually."
header="Keep your employees compliant and track who's falling behind."
subText="Build certified training from your existing policies, assign it to the right people, and keep proof of completion in one place."
btnLabel="Book a Demo"
/>
@@ -0,0 +1,13 @@
import { redirect } from '@sveltejs/kit';
import { client } from '$lib/utils/posthog';
export const prerender = false;
export const load = ({ request }) => {
client.capture({
distinctId: request.headers.get('x-forwarded-for') || new Date().getTime().toString(),
event: 'feedback page visited'
});
redirect(307, 'https://feedback.classroomio.com');
};
+4 -2
View File
@@ -69,8 +69,10 @@
<p class="mt-10 w-[80%] text-start text-slate-700 md:w-[40%] md:text-center">
This is a live document, and will be updated regularly. To submit an idea, or to view a list of feature requests and
ideas that we're considering, check out our <a
href="https://github.com/classroomio/classroomio/discussion"
class="underline">GitHub Discussions</a
href="https://feedback.classroomio.com"
target="_blank"
rel="noopener"
class="underline">feedback portal</a
>
</p>
</PageHeader>
@@ -153,17 +153,26 @@
});
</script>
<div class="ui:relative" style="max-height: {maxHeight}; min-height: {options.minHeight}; height: {options.height};">
<div
class="ui:relative ui:aspect-video ui:w-full ui:overflow-hidden ui:bg-muted"
style="max-height: {maxHeight}; min-height: {options.minHeight}; height: {options.height};"
>
{#if isYouTube && youtubeVideoId}
<div
bind:this={containerElement}
class="plyr__video-embed"
class="plyr__video-embed ui:absolute ui:inset-0 ui:h-full ui:w-full"
data-plyr-provider="youtube"
data-plyr-embed-id={youtubeVideoId}
></div>
{:else}
<!-- svelte-ignore a11y_media_has_caption -->
<video bind:this={videoElement} {src} {poster} {playsinline} class="plyr-player">
<video
bind:this={videoElement}
{src}
{poster}
{playsinline}
class="plyr-player ui:absolute ui:inset-0 ui:h-full ui:w-full ui:object-contain"
>
{#each tracks as track (track.src + track.srclang)}
<track
kind={track.kind ?? 'captions'}