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 { 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() {
|
||||
|
||||
+14
-4
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user