chore: marketing updates and video player improvement
This commit is contained in:
@@ -10,6 +10,7 @@ import { get } from 'svelte/store';
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { handleLocaleChange } from '$lib/utils/functions/translations';
|
import { handleLocaleChange } from '$lib/utils/functions/translations';
|
||||||
import { identifyPosthogUser } from '$lib/utils/services/posthog';
|
import { identifyPosthogUser } from '$lib/utils/services/posthog';
|
||||||
|
import { identifyUserJotUser } from '$lib/utils/services/userjot';
|
||||||
import { isOrgStudent } from '$lib/utils/store/app';
|
import { isOrgStudent } from '$lib/utils/store/app';
|
||||||
import { isPublicRoute } from '$lib/utils/functions/routes/isPublicRoute';
|
import { isPublicRoute } from '$lib/utils/functions/routes/isPublicRoute';
|
||||||
import { licenseApi } from '$features/license/api/license.svelte';
|
import { licenseApi } from '$features/license/api/license.svelte';
|
||||||
@@ -231,6 +232,13 @@ class AppInitApi extends BaseApi {
|
|||||||
email: profileStore.email,
|
email: profileStore.email,
|
||||||
name: profileStore.fullname
|
name: profileStore.fullname
|
||||||
});
|
});
|
||||||
|
|
||||||
|
identifyUserJotUser({
|
||||||
|
id: profileStore.id,
|
||||||
|
email: profileStore.email,
|
||||||
|
fullname: profileStore.fullname,
|
||||||
|
avatarUrl: profileStore.avatarUrl
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
|||||||
+14
-4
@@ -24,13 +24,19 @@
|
|||||||
|
|
||||||
<Dialog.Root
|
<Dialog.Root
|
||||||
bind:open={$lessonVideoUpload.isModalOpen}
|
bind:open={$lessonVideoUpload.isModalOpen}
|
||||||
onOpenChange={(details) => {
|
onOpenChange={(open) => {
|
||||||
if (!details.open && $lessonVideoUpload.isUploading) return;
|
if (!open) onClose();
|
||||||
if (!details.open) onClose();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog.Content
|
<Dialog.Content
|
||||||
class="flex max-h-[680px] max-w-2xl! flex-col overflow-hidden"
|
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) => {
|
onCloseAutoFocus={(e) => {
|
||||||
if ($lessonVideoUpload.isUploading) e.preventDefault();
|
if ($lessonVideoUpload.isUploading) e.preventDefault();
|
||||||
}}
|
}}
|
||||||
@@ -42,7 +48,11 @@
|
|||||||
<UnderlineTabs.Root bind:value={currentTab} class="">
|
<UnderlineTabs.Root bind:value={currentTab} class="">
|
||||||
<UnderlineTabs.List>
|
<UnderlineTabs.List>
|
||||||
{#each tabs as item (item.value)}
|
{#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}
|
{#if $isFreePlan && item.value === 3}
|
||||||
<ZapIcon size={16} class="filled" />
|
<ZapIcon size={16} class="filled" />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -581,12 +581,22 @@
|
|||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table.Root>
|
</Table.Root>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
|
|
||||||
<Dialog.Root bind:open={isBulkActionModalOpen} onOpenChange={handleBulkActionModalChange}>
|
<Dialog.Root bind:open={isBulkActionModalOpen} onOpenChange={handleBulkActionModalChange}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
||||||
import { initPosthog, type PosthogBootstrapUser } from '$lib/utils/services/posthog';
|
import { initPosthog, type PosthogBootstrapUser } from '$lib/utils/services/posthog';
|
||||||
import { initUmami } from '$lib/utils/services/umami';
|
import { initUmami } from '$lib/utils/services/umami';
|
||||||
|
import { initUserJot } from '$lib/utils/services/userjot';
|
||||||
import { licenseApi } from '$features/license/api/license.svelte';
|
import { licenseApi } from '$features/license/api/license.svelte';
|
||||||
|
|
||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
@@ -11,6 +12,7 @@ export function setupAnalytics(user?: PosthogBootstrapUser) {
|
|||||||
|
|
||||||
initPosthog(user);
|
initPosthog(user);
|
||||||
initUmami();
|
initUmami();
|
||||||
|
initUserJot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks if this is cloud deployment and initializes analytics */
|
/** 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": {
|
"actions": {
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"where_used": "Where used",
|
"where_used": "Where used",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"manage_thumbnails": "Manage thumbnails"
|
||||||
},
|
},
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
@@ -1061,6 +1062,21 @@
|
|||||||
"thumbnail_alt": "Asset thumbnail",
|
"thumbnail_alt": "Asset thumbnail",
|
||||||
"status": "Status"
|
"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": {
|
"usage": {
|
||||||
"title": "Where Used",
|
"title": "Where Used",
|
||||||
"description": "Current usage references for",
|
"description": "Current usage references for",
|
||||||
@@ -1597,6 +1613,7 @@
|
|||||||
"create_course": "Create Course",
|
"create_course": "Create Course",
|
||||||
"view_site": "View Site",
|
"view_site": "View Site",
|
||||||
"visit_site": "Visit Website",
|
"visit_site": "Visit Website",
|
||||||
|
"open_academy": "Open Academy",
|
||||||
"current_lesson": "Currently Learning",
|
"current_lesson": "Currently Learning",
|
||||||
"currently_learning": "Currently Learning",
|
"currently_learning": "Currently Learning",
|
||||||
"learner": "Learner",
|
"learner": "Learner",
|
||||||
@@ -2957,6 +2974,7 @@
|
|||||||
"no_active_cycle": "No active compliance cycle",
|
"no_active_cycle": "No active compliance cycle",
|
||||||
"empty_history": "No compliance history has been recorded yet.",
|
"empty_history": "No compliance history has been recorded yet.",
|
||||||
"empty_overview": "No learner compliance records are available 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",
|
"not_available": "Not available",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
@@ -3868,6 +3886,10 @@
|
|||||||
"thumbnail_upload_failed": "Failed to upload thumbnail",
|
"thumbnail_upload_failed": "Failed to upload thumbnail",
|
||||||
"thumbnail_too_large": "Thumbnail image is too large",
|
"thumbnail_too_large": "Thumbnail image is too large",
|
||||||
"thumbnail_invalid": "Invalid thumbnail file",
|
"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_started": "Transcription started",
|
||||||
"transcription_start_failed": "Could not start transcription",
|
"transcription_start_failed": "Could not start transcription",
|
||||||
"transcription_completed": "Transcription finished",
|
"transcription_completed": "Transcription finished",
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ export function baseOptions(): BaseLayoutProps {
|
|||||||
url: 'https://dub.sh/ciodemo',
|
url: 'https://dub.sh/ciodemo',
|
||||||
external: true
|
external: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
Feedback <SquareArrowOutUpRight />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
url: 'https://feedback.classroomio.com',
|
||||||
|
external: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: (
|
text: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -176,8 +176,11 @@
|
|||||||
<p class="ui:text-foreground mb-4 text-sm font-semibold">Company</p>
|
<p class="ui:text-foreground mb-4 text-sm font-semibold">Company</p>
|
||||||
<ul class="space-y-3">
|
<ul class="space-y-3">
|
||||||
<li>
|
<li>
|
||||||
<a href="/roadmap" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
|
<a
|
||||||
>Roadmap</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>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -135,6 +135,16 @@
|
|||||||
<title>Compliance Training | ClassroomIO</title>
|
<title>Compliance Training | ClassroomIO</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
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."
|
content="Turn policies and SOPs into certified training. Track compliance, renewals, grace periods, and audit evidence in one place."
|
||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
@@ -154,8 +164,8 @@
|
|||||||
Compliance Training
|
Compliance Training
|
||||||
</Badge>
|
</Badge>
|
||||||
<h1 class="text-[clamp(2rem,3.4vw,2.8rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
|
<h1 class="text-[clamp(2rem,3.4vw,2.8rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
|
||||||
Audit-ready training,
|
Keep your employees compliant and
|
||||||
<em class="text-blue-700 not-italic">without the spreadsheets.</em>
|
<em class="text-blue-700 not-italic">track who's falling behind.</em>
|
||||||
</h1>
|
</h1>
|
||||||
<HeroSubtitle class="mx-auto">
|
<HeroSubtitle class="mx-auto">
|
||||||
Turn policies, SOPs, docs, and videos into assigned courses with quizzes, certificates, renewal rules, and
|
Turn policies, SOPs, docs, and videos into assigned courses with quizzes, certificates, renewal rules, and
|
||||||
@@ -341,7 +351,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<PageSignupCTA
|
<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."
|
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"
|
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');
|
||||||
|
};
|
||||||
@@ -69,8 +69,10 @@
|
|||||||
<p class="mt-10 w-[80%] text-start text-slate-700 md:w-[40%] md:text-center">
|
<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
|
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
|
ideas that we're considering, check out our <a
|
||||||
href="https://github.com/classroomio/classroomio/discussion"
|
href="https://feedback.classroomio.com"
|
||||||
class="underline">GitHub Discussions</a
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="underline">feedback portal</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|||||||
@@ -153,17 +153,26 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</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}
|
{#if isYouTube && youtubeVideoId}
|
||||||
<div
|
<div
|
||||||
bind:this={containerElement}
|
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-provider="youtube"
|
||||||
data-plyr-embed-id={youtubeVideoId}
|
data-plyr-embed-id={youtubeVideoId}
|
||||||
></div>
|
></div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- svelte-ignore a11y_media_has_caption -->
|
<!-- 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)}
|
{#each tracks as track (track.src + track.srclang)}
|
||||||
<track
|
<track
|
||||||
kind={track.kind ?? 'captions'}
|
kind={track.kind ?? 'captions'}
|
||||||
|
|||||||
Reference in New Issue
Block a user