feat: certificates, landing pages, ai tutor, website update, compliance

This commit is contained in:
Rotimi Best
2026-05-18 03:47:22 +01:00
parent c31ad63955
commit ac3a2c14a7
644 changed files with 56050 additions and 5439 deletions
@@ -0,0 +1,82 @@
<script lang="ts">
interface IconProps {
color?: string;
size?: number;
strokeWidth?: number;
class?: string;
}
let { color = 'currentColor', size = 24, strokeWidth = 2, class: className = '' }: IconProps = $props();
let isShaking = $state(false);
function handleMouseEnter() {
if (isShaking) return;
isShaking = true;
setTimeout(() => {
isShaking = false;
}, 600);
}
</script>
<div class={className} aria-label="bot-off" role="img" onmouseenter={handleMouseEnter}>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
stroke-width={strokeWidth}
stroke-linecap="round"
stroke-linejoin="round"
class="bot-off"
class:animate={isShaking}
>
<path d="M13.67 8H18a2 2 0 0 1 2 2v4.33" />
<path d="M2 14h2" />
<path d="M20 14h2" />
<path d="M22 22 2 2" />
<path d="M8 8H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 1.414-.586" />
<path d="M9 13v2" />
<path d="M9.67 4H12v2.33" />
</svg>
</div>
<style>
div {
display: inline-block;
}
.bot-off {
overflow: visible;
transform: translateX(0);
transition:
transform 0.6s ease-in-out,
stroke 220ms ease-out;
}
.bot-off.animate {
animation: groupShake 0.6s ease-in-out;
}
@keyframes groupShake {
0% {
transform: translateX(0);
}
16.67% {
transform: translateX(-7%);
}
33.33% {
transform: translateX(7%);
}
50% {
transform: translateX(-7%);
}
66.67% {
transform: translateX(7%);
}
100% {
transform: translateX(0);
}
}
</style>
@@ -0,0 +1,159 @@
<script lang="ts">
interface IconProps {
color?: string;
size?: number;
strokeWidth?: number;
class?: string;
/** When provided, the bot's eyes track the cursor as it moves within this element. */
trackElement?: HTMLElement | null;
/** Maximum eye offset in SVG units. Larger = eyes move further when looking around. */
maxEyeOffset?: number;
/** Distance in px from the bot at which eye offset saturates. */
trackRadius?: number;
}
let {
color = 'currentColor',
size = 24,
strokeWidth = 2,
class: className = '',
trackElement = null,
maxEyeOffset = 1.6,
trackRadius = 240
}: IconProps = $props();
let eyeY1 = $state(13);
let eyeY2 = $state(15);
let eyeOffsetX = $state(0);
let eyeOffsetY = $state(0);
let isBlinking = $state(false);
let svgEl = $state<SVGSVGElement | null>(null);
function animateEyes(
startY1: number,
startY2: number,
endY1: number,
endY2: number,
duration: number,
delay = 0
): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
const startTime = performance.now();
const step = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
eyeY1 = startY1 + (endY1 - startY1) * eased;
eyeY2 = startY2 + (endY2 - startY2) * eased;
if (progress < 1) {
requestAnimationFrame(step);
} else {
resolve();
}
};
requestAnimationFrame(step);
}, delay);
});
}
function blink() {
if (isBlinking) return;
isBlinking = true;
animateEyes(13, 15, 14, 14, 250, 200).then(() =>
animateEyes(14, 14, 13, 15, 250).then(() => {
isBlinking = false;
})
);
}
function handleTrackMove(event: MouseEvent) {
if (!svgEl) return;
const rect = svgEl.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = event.clientX - cx;
const dy = event.clientY - cy;
const distance = Math.hypot(dx, dy);
if (distance === 0) {
eyeOffsetX = 0;
eyeOffsetY = 0;
return;
}
const magnitude = Math.min(distance / trackRadius, 1);
eyeOffsetX = (dx / distance) * maxEyeOffset * magnitude;
eyeOffsetY = (dy / distance) * maxEyeOffset * magnitude;
}
function handleTrackLeave() {
eyeOffsetX = 0;
eyeOffsetY = 0;
}
$effect(() => {
if (!trackElement) return;
trackElement.addEventListener('mousemove', handleTrackMove);
trackElement.addEventListener('mouseleave', handleTrackLeave);
return () => {
trackElement.removeEventListener('mousemove', handleTrackMove);
trackElement.removeEventListener('mouseleave', handleTrackLeave);
};
});
</script>
<div class={className} aria-label="bot" role="img" onmouseenter={blink}>
<svg
bind:this={svgEl}
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
stroke-width={strokeWidth}
stroke-linecap="round"
stroke-linejoin="round"
class="bot-icon"
>
<path d="M12 8V4H8" />
<rect width="16" height="12" x="4" y="8" rx="2" />
<path d="M2 14h2" />
<path d="M20 14h2" />
<line
x1={15 + eyeOffsetX}
y1={eyeY1 + eyeOffsetY}
x2={15 + eyeOffsetX}
y2={eyeY2 + eyeOffsetY}
class="eye eye-right"
/>
<line
x1={9 + eyeOffsetX}
y1={eyeY1 + eyeOffsetY}
x2={9 + eyeOffsetX}
y2={eyeY2 + eyeOffsetY}
class="eye eye-left"
/>
</svg>
</div>
<style>
div {
display: inline-block;
}
.bot-icon {
overflow: visible;
transition: stroke 220ms ease-out;
}
.eye {
transition:
transform 120ms ease-out,
x 120ms ease-out,
y 120ms ease-out;
}
</style>
@@ -0,0 +1,99 @@
<script lang="ts">
interface IconProps {
color?: string;
size?: number;
strokeWidth?: number;
isHovered?: boolean;
class?: string;
}
let {
color = 'currentColor',
size = 24,
strokeWidth = 2,
isHovered: isParentHovered = false,
class: className = ''
}: IconProps = $props();
let localMotion = $state(false);
const showMotion = $derived(isParentHovered || localMotion);
function handleMouseEnter() {
if (isParentHovered || localMotion) return;
localMotion = true;
setTimeout(() => {
localMotion = false;
}, 700);
}
</script>
<div class={className} aria-label="chart-column" role="img" onmouseenter={handleMouseEnter}>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
stroke-width={strokeWidth}
stroke-linecap="round"
stroke-linejoin="round"
class="chart-column-icon"
class:animate={showMotion}
>
<path d="M3 3v16a2 2 0 0 0 2 2h16" class="frame" />
<path d="M18 17V9" class="column column-2" />
<path d="M13 17V5" class="column column-1" />
<path d="M8 17v-3" class="column column-0" />
</svg>
</div>
<style>
div {
display: inline-block;
}
.chart-column-icon {
overflow: visible;
}
.column {
stroke-dasharray: 12;
stroke-dashoffset: 0;
transition:
stroke-dashoffset 0.3s ease,
opacity 0.3s ease;
}
.chart-column-icon.animate .column {
animation: columnAnimation 0.6s ease forwards;
}
.chart-column-icon.animate .column-0 {
animation-delay: 0s;
}
.chart-column-icon.animate .column-1 {
animation-delay: 0.1s;
}
.chart-column-icon.animate .column-2 {
animation-delay: 0.2s;
}
@keyframes columnAnimation {
0% {
stroke-dashoffset: 0;
opacity: 1;
}
50% {
stroke-dashoffset: 12;
opacity: 0;
}
100% {
stroke-dashoffset: 0;
opacity: 1;
}
}
</style>
@@ -2,7 +2,10 @@ export { default as AnalyticsIcon } from './analytics.svelte';
export { default as ApiIcon } from './api.svelte';
export { default as AttendanceIcon } from './attendance.svelte';
export { default as AttachmentIcon } from './attachment.svelte';
export { default as BotIcon } from './bot.svelte';
export { default as BotOffIcon } from './bot-off.svelte';
export { default as CertificateIcon } from './certificate.svelte';
export { default as ChartColumnIcon } from './chart-column.svelte';
export { default as CloseIcon } from './close.svelte';
export { default as CommunityIcon } from './community.svelte';
export { default as ContentIcon } from './content.svelte';