feat: add sequential progression and video watch lesson completion
Gate student content by completion order, track per-video watch progress with seek enforcement, and auto-complete lessons at the configured threshold. Show ring progress in the lesson header, keep watch progress on rewatch, and sync progression locks client-side so the next item unlocks after completion.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { CertificateDesign, CertificateRenderData } from '../types';
|
||||
import type { CertificateDesign, CertificateRenderData, CertificateSignatory } from '../types';
|
||||
|
||||
export function escapeHtml(input: unknown): string {
|
||||
return String(input ?? '').replace(/[&<>"']/g, (char) => {
|
||||
@@ -53,12 +53,100 @@ export interface TemplateRenderOutput {
|
||||
|
||||
export type TemplateRenderer = (args: TemplateRenderArgs) => TemplateRenderOutput;
|
||||
|
||||
export interface SignatoryBlockOptions {
|
||||
wrapperClass?: string;
|
||||
nameClass: string;
|
||||
roleClass: string;
|
||||
roleFirst?: boolean;
|
||||
}
|
||||
|
||||
export function renderSignatoryBlock(sig: CertificateSignatory, options: SignatoryBlockOptions): string {
|
||||
if (sig.enabled === false) return '';
|
||||
|
||||
const sigImg = sig.signatureUrl ? `<img class="sig-img" src="${escapeHtml(sig.signatureUrl)}" alt="" />` : '';
|
||||
|
||||
const nameEl = `<div class="${options.nameClass}">${escapeHtml(sig.name)}</div>`;
|
||||
const roleEl = `<div class="${options.roleClass}">${escapeHtml(sig.role)}</div>`;
|
||||
const content = options.roleFirst ? `${roleEl}${nameEl}` : `${nameEl}${roleEl}`;
|
||||
const wrapperClass = options.wrapperClass ? ` class="${options.wrapperClass}"` : '';
|
||||
|
||||
return `
|
||||
<div${wrapperClass}>
|
||||
<div class="sig-content">
|
||||
<div class="sig-img-slot">${sigImg}</div>
|
||||
<div class="sig-text">${content}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export interface FooterMetaBlockOptions {
|
||||
wrapperClass?: string;
|
||||
labelClass: string;
|
||||
valueClass: string;
|
||||
}
|
||||
|
||||
export function renderFooterMetaBlock(label: string, value: string, options: FooterMetaBlockOptions): string {
|
||||
const wrapperClass = options.wrapperClass ? ` class="${options.wrapperClass}"` : '';
|
||||
|
||||
return `
|
||||
<div${wrapperClass}>
|
||||
<div class="sig-content">
|
||||
<div class="sig-img-slot"></div>
|
||||
<div class="sig-text">
|
||||
<div class="${options.labelClass}">${escapeHtml(label)}</div>
|
||||
<div class="${options.valueClass}">${escapeHtml(value)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export const SIGNATURE_IMAGE_STYLES = `
|
||||
.sig-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.sig-img-slot {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
margin-bottom: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sig-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.sig-img {
|
||||
display: block;
|
||||
max-height: 48px;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
background: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FONTS_LINK_HREF =
|
||||
'https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Cinzel:wght@400;500;600;700&family=DM+Mono:wght@400;500&family=Bodoni+Moda:ital,wght@0,400;0,700;1,400&family=Archivo+Black&family=JetBrains+Mono:wght@400;500&family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&family=Space+Grotesk:wght@400;500;700&display=swap';
|
||||
|
||||
export const BASE_STYLES = `
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { width: 1100px; height: 780px; background: transparent; }
|
||||
body { -webkit-font-smoothing: antialiased; }
|
||||
.cert { width: 1100px; height: 780px; position: relative; overflow: hidden; }
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
.cert { width: 1100px; height: 780px; position: relative; overflow: hidden; box-shadow: inset 0 0 0 3px rgba(0,0,0,0.12); }
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user