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.
207 lines
5.4 KiB
TypeScript
207 lines
5.4 KiB
TypeScript
import {
|
|
escapeHtml,
|
|
getYear,
|
|
renderFooterMetaBlock,
|
|
renderSignatoryBlock,
|
|
SIGNATURE_IMAGE_STYLES,
|
|
type TemplateRenderer
|
|
} from './shared';
|
|
|
|
export const renderPoster: TemplateRenderer = ({ design, data }) => {
|
|
const accent = design.accentColor;
|
|
const subtitle = design.subtitle ?? '';
|
|
const description = design.descriptionOverride || data.courseDescription;
|
|
const [signatoryOne, signatoryTwo] = design.signatories;
|
|
const [firstTitleWord, ...restTitleWords] = data.courseName.split(' ');
|
|
const titleEmphasis = restTitleWords.join(' ');
|
|
|
|
const body = `
|
|
<div class="cert t-poster">
|
|
<div class="blob blob-1"></div>
|
|
<div class="blob blob-2"></div>
|
|
<div class="blob blob-3"></div>
|
|
<div class="content">
|
|
<div class="top">
|
|
<span class="pill">${escapeHtml(data.orgName)}</span>
|
|
<span>${escapeHtml(data.certificateId)} / ${escapeHtml(data.date)}</span>
|
|
</div>
|
|
<div class="title">${escapeHtml(firstTitleWord || 'Award')} <em>${escapeHtml(titleEmphasis)}</em></div>
|
|
<div class="of">${escapeHtml(subtitle)}</div>
|
|
<div class="recipient-area">
|
|
<div class="lbl">Awarded To</div>
|
|
<div class="recipient">${escapeHtml(data.recipientName)}</div>
|
|
</div>
|
|
<div class="description">${escapeHtml(description)}</div>
|
|
<div class="bottom">
|
|
${renderSignatoryBlock(signatoryOne, { nameClass: 'v', roleClass: 'k', roleFirst: true })}
|
|
${renderSignatoryBlock(signatoryTwo, { nameClass: 'v', roleClass: 'k', roleFirst: true })}
|
|
${renderFooterMetaBlock('Issued', data.date, { labelClass: 'k', valueClass: 'v' })}
|
|
</div>
|
|
</div>
|
|
<div class="corner-num">${getYear(data.date)}</div>
|
|
</div>
|
|
`;
|
|
|
|
const styles = `
|
|
.t-poster {
|
|
background: #fef2dc;
|
|
color: #1a1a1a;
|
|
padding: 0;
|
|
font-family: 'Space Grotesk', sans-serif;
|
|
overflow: hidden;
|
|
}
|
|
.t-poster .blob {
|
|
position: absolute;
|
|
width: 550px;
|
|
height: 550px;
|
|
border-radius: 50%;
|
|
filter: blur(2px);
|
|
opacity: 0.95;
|
|
}
|
|
.t-poster .blob-1 {
|
|
background: ${accent};
|
|
top: -180px;
|
|
right: -120px;
|
|
}
|
|
.t-poster .blob-2 {
|
|
background: #1e3a8a;
|
|
bottom: -200px;
|
|
left: -150px;
|
|
width: 480px;
|
|
height: 480px;
|
|
}
|
|
.t-poster .blob-3 {
|
|
background: #fbbf24;
|
|
top: 40%;
|
|
left: 55%;
|
|
width: 200px;
|
|
height: 200px;
|
|
}
|
|
.t-poster .content {
|
|
position: relative;
|
|
z-index: 2;
|
|
height: 100%;
|
|
padding: 50px 55px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.t-poster .top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 11px;
|
|
letter-spacing: 0.2em;
|
|
text-transform: uppercase;
|
|
margin-bottom: 30px;
|
|
}
|
|
.t-poster .top .pill {
|
|
background: ${accent};
|
|
color: #fef2dc;
|
|
padding: 5px 12px;
|
|
border-radius: 100px;
|
|
}
|
|
.t-poster .title {
|
|
font-family: 'Playfair Display', serif;
|
|
font-size: 140px;
|
|
font-weight: 900;
|
|
line-height: 0.85;
|
|
letter-spacing: -0.04em;
|
|
color: #1a1a1a;
|
|
}
|
|
.t-poster .title em {
|
|
font-style: italic;
|
|
font-weight: 400;
|
|
color: ${accent};
|
|
}
|
|
.t-poster .of {
|
|
font-family: 'Playfair Display', serif;
|
|
font-size: 62px;
|
|
font-style: italic;
|
|
font-weight: 400;
|
|
line-height: 1;
|
|
margin-top: -4px;
|
|
color: #1a1a1a;
|
|
}
|
|
.t-poster .recipient-area {
|
|
margin-top: 40px;
|
|
background: ${accent};
|
|
color: #fef2dc;
|
|
padding: 24px 30px;
|
|
align-self: flex-start;
|
|
max-width: 75%;
|
|
transform: rotate(-1deg);
|
|
box-shadow: 8px 8px 0 #1a1a1a;
|
|
}
|
|
.t-poster .recipient-area .lbl {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 0.22em;
|
|
color: #fef2dc;
|
|
opacity: 0.8;
|
|
text-transform: uppercase;
|
|
margin-bottom: 6px;
|
|
}
|
|
.t-poster .recipient {
|
|
font-family: 'Playfair Display', serif;
|
|
font-size: 54px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
.t-poster .description {
|
|
font-size: 15px;
|
|
line-height: 1.55;
|
|
margin-top: 20px;
|
|
max-width: 580px;
|
|
color: #1a1a1a;
|
|
font-weight: 500;
|
|
}
|
|
.t-poster .bottom {
|
|
margin-top: auto;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
align-items: end;
|
|
gap: 24px;
|
|
border-top: 2px solid #1a1a1a;
|
|
padding-top: 18px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
.t-poster .bottom .sig-content,
|
|
.t-poster .bottom .sig-text {
|
|
align-items: flex-start;
|
|
text-align: left;
|
|
}
|
|
.t-poster .bottom .sig-img-slot {
|
|
justify-content: flex-start;
|
|
}
|
|
.t-poster .bottom .k {
|
|
font-size: 9px;
|
|
letter-spacing: 0.2em;
|
|
text-transform: uppercase;
|
|
color: #666;
|
|
margin-bottom: 3px;
|
|
}
|
|
.t-poster .bottom .v {
|
|
font-family: 'Playfair Display', serif;
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
color: #1a1a1a;
|
|
}
|
|
.t-poster .corner-num {
|
|
position: absolute;
|
|
bottom: 36px;
|
|
right: 50px;
|
|
font-family: 'Playfair Display', serif;
|
|
font-size: 80px;
|
|
font-weight: 900;
|
|
font-style: italic;
|
|
color: ${accent};
|
|
z-index: 3;
|
|
line-height: 0.8;
|
|
}
|
|
${SIGNATURE_IMAGE_STYLES}
|
|
`;
|
|
|
|
return { body, styles };
|
|
};
|