feat: lint codebase and run precommit hook

This commit is contained in:
Rotimi Best
2025-12-25 09:01:51 +00:00
parent 334c67f22a
commit b31ccf3037
214 changed files with 6191 additions and 2880 deletions
@@ -20,37 +20,28 @@
}: Props = $props();
</script>
<Dialog.Root
bind:open
>
<Dialog.Root bind:open>
<Dialog.Content class="w-96">
<Dialog.Header>
<Dialog.Title>
{$t('community.delete.title')} {isQuestion
? $t('community.delete.question')
: $t('community.delete.comment')}
{$t('community.delete.title')}
{isQuestion ? $t('community.delete.question') : $t('community.delete.comment')}
</Dialog.Title>
</Dialog.Header>
<div>
<p class="mt-0 text-base dark:text-white">
{$t('community.delete.sure')}
{isQuestion ? $t('community.delete.question') : $t('community.delete.comment')}?
</p>
<p class="mt-0 text-base dark:text-white">
{$t('community.delete.sure')}
{isQuestion ? $t('community.delete.question') : $t('community.delete.comment')}?
</p>
<div class="mt-5 flex items-center justify-between">
<Button
variant="outline"
onclick={onCancel}
>
{$t('community.delete.no')}
</Button>
<Button
onclick={onDelete}
loading={isDeleting}
>
{$t('community.delete.yes')}
</Button>
<div class="mt-5 flex items-center justify-between">
<Button variant="outline" onclick={onCancel}>
{$t('community.delete.no')}
</Button>
<Button onclick={onDelete} loading={isDeleting}>
{$t('community.delete.yes')}
</Button>
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Root>
@@ -7,4 +7,3 @@ export interface CurrentCommunityQuestion {
export const currentCommunityQuestion = writable<CurrentCommunityQuestion>({
title: null
});
@@ -20,43 +20,41 @@
});
</script>
<Dialog.Root
bind:open={$copyCourseModal.open}
>
<Dialog.Root bind:open={$copyCourseModal.open}>
<Dialog.Content class="w-96">
<Dialog.Header>
<Dialog.Title>{$t('courses.copy_course.title')}</Dialog.Title>
</Dialog.Header>
<form onsubmit={preventDefault(createCourse)}>
<InputField
label={$t('courses.copy_course.course_name_label')}
bind:value={$copyCourseModal.title}
autoFocus={true}
placeholder={$t('courses.copy_course.course_name_placeholder')}
className="mb-4"
isRequired={true}
autoComplete={false}
errorMessage={courseCloneApi.errors.title}
/>
<InputField
label={$t('courses.copy_course.course_name_label')}
bind:value={$copyCourseModal.title}
autoFocus={true}
placeholder={$t('courses.copy_course.course_name_placeholder')}
className="mb-4"
isRequired={true}
autoComplete={false}
errorMessage={courseCloneApi.errors.title}
/>
<TextareaField
label={$t('courses.copy_course.course_description_label')}
bind:value={$copyCourseModal.description}
placeholder={$t('courses.copy_course.course_description_placeholder')}
className="mb-4"
rows={4}
errorMessage={courseCloneApi.errors.description}
/>
<TextareaField
label={$t('courses.copy_course.course_description_label')}
bind:value={$copyCourseModal.description}
placeholder={$t('courses.copy_course.course_description_placeholder')}
className="mb-4"
rows={4}
errorMessage={courseCloneApi.errors.description}
/>
{#if courseCloneApi.errors.general}
<div class="mb-4 text-sm text-red-600">{courseCloneApi.errors.general}</div>
{/if}
{#if courseCloneApi.errors.general}
<div class="mb-4 text-sm text-red-600">{courseCloneApi.errors.general}</div>
{/if}
<div class="mt-5 flex flex-row-reverse items-center">
<Button type="submit" loading={$copyCourseModal.isSaving}>
{$t('courses.copy_course.create_button')}
</Button>
</div>
</form>
<div class="mt-5 flex flex-row-reverse items-center">
<Button type="submit" loading={$copyCourseModal.isSaving}>
{$t('courses.copy_course.create_button')}
</Button>
</div>
</form>
</Dialog.Content>
</Dialog.Root>
@@ -233,4 +233,3 @@
</div>
{/if}
</section>
@@ -109,4 +109,3 @@
variant="page"
/>
{/if}
@@ -55,4 +55,3 @@
{/if}
</div>
</section>
@@ -2,9 +2,9 @@ import type { Readable } from 'svelte/store';
import { browser } from '$app/environment';
export interface DOMProtectionCleanup {
protectionInterval?: ReturnType<typeof setInterval>;
domObserver?: MutationObserver;
cleanup: () => void;
protectionInterval?: ReturnType<typeof setInterval>;
domObserver?: MutationObserver;
cleanup: () => void;
}
/**
@@ -14,192 +14,190 @@ export interface DOMProtectionCleanup {
* @param isEmailVerified - A readable store or getter function that returns whether email is verified
* @returns Cleanup object with intervals, observers, and a cleanup function
*/
export function setupDOMProtection(
isEmailVerified: Readable<boolean> | (() => boolean | null)
): DOMProtectionCleanup {
if (!browser) {
return {
cleanup: () => {}
};
}
export function setupDOMProtection(isEmailVerified: Readable<boolean> | (() => boolean | null)): DOMProtectionCleanup {
if (!browser) {
return {
cleanup: () => {}
};
}
let protectionInterval: ReturnType<typeof setInterval> | undefined;
let domObserver: MutationObserver | undefined;
let protectionInterval: ReturnType<typeof setInterval> | undefined;
let domObserver: MutationObserver | undefined;
// Helper to get current verification status
const getIsVerified = () => {
if (typeof isEmailVerified === 'function') {
return isEmailVerified() ?? false;
}
// For stores, we need to subscribe temporarily - this is a limitation
// In practice, the component should pass a getter function
let value = false;
const unsubscribe = isEmailVerified.subscribe((v) => {
value = v ?? false;
});
unsubscribe();
return value;
};
// Helper to get current verification status
const getIsVerified = () => {
if (typeof isEmailVerified === 'function') {
return isEmailVerified() ?? false;
}
// For stores, we need to subscribe temporarily - this is a limitation
// In practice, the component should pass a getter function
let value = false;
const unsubscribe = isEmailVerified.subscribe((v) => {
value = v ?? false;
});
unsubscribe();
return value;
};
// Function to disable all interactive elements
function disableInteractions() {
if (getIsVerified()) return;
// Function to disable all interactive elements
function disableInteractions() {
if (getIsVerified()) return;
// Disable all buttons except verification-related ones
const buttons = document.querySelectorAll('button, [role="button"], a[href], input[type="submit"]');
buttons.forEach((button) => {
const el = button as HTMLElement;
// Disable all buttons except verification-related ones
const buttons = document.querySelectorAll('button, [role="button"], a[href], input[type="submit"]');
buttons.forEach((button) => {
const el = button as HTMLElement;
// Skip if already protected or is verification-related
if (
el.dataset.verificationProtected === 'true' ||
el.closest('[data-verification-modal]') ||
el.textContent?.toLowerCase().includes('verify') ||
el.textContent?.toLowerCase().includes('resend')
) {
return;
}
// Skip if already protected or is verification-related
if (
el.dataset.verificationProtected === 'true' ||
el.closest('[data-verification-modal]') ||
el.textContent?.toLowerCase().includes('verify') ||
el.textContent?.toLowerCase().includes('resend')
) {
return;
}
el.dataset.verificationProtected = 'true';
el.dataset.originalPointerEvents = el.style.pointerEvents || 'auto';
el.style.pointerEvents = 'none';
el.style.opacity = '0.5';
el.style.cursor = 'not-allowed';
el.setAttribute('title', 'Email verification required');
el.dataset.verificationProtected = 'true';
el.dataset.originalPointerEvents = el.style.pointerEvents || 'auto';
el.style.pointerEvents = 'none';
el.style.opacity = '0.5';
el.style.cursor = 'not-allowed';
el.setAttribute('title', 'Email verification required');
// Add click prevention
el.addEventListener('click', preventInteraction, true);
el.addEventListener('mousedown', preventInteraction, true);
});
// Add click prevention
el.addEventListener('click', preventInteraction, true);
el.addEventListener('mousedown', preventInteraction, true);
});
// Disable all form inputs
const inputs = document.querySelectorAll('input, textarea, select');
inputs.forEach((input) => {
const el = input as HTMLInputElement;
// Disable all form inputs
const inputs = document.querySelectorAll('input, textarea, select');
inputs.forEach((input) => {
const el = input as HTMLInputElement;
// Skip if in verification modal
if (el.closest('[data-verification-modal]')) return;
// Skip if in verification modal
if (el.closest('[data-verification-modal]')) return;
if (!el.dataset.verificationProtected) {
el.dataset.verificationProtected = 'true';
el.dataset.originalDisabled = el.disabled ? 'true' : 'false';
el.disabled = true;
el.style.opacity = '0.5';
el.addEventListener('focus', preventInteraction, true);
el.addEventListener('input', preventInteraction, true);
}
});
if (!el.dataset.verificationProtected) {
el.dataset.verificationProtected = 'true';
el.dataset.originalDisabled = el.disabled ? 'true' : 'false';
el.disabled = true;
el.style.opacity = '0.5';
el.addEventListener('focus', preventInteraction, true);
el.addEventListener('input', preventInteraction, true);
}
});
// Disable contenteditable elements
const editables = document.querySelectorAll('[contenteditable="true"]');
editables.forEach((el) => {
const element = el as HTMLElement;
if (!element.dataset.verificationProtected) {
element.dataset.verificationProtected = 'true';
element.contentEditable = 'false';
element.style.opacity = '0.5';
}
});
}
// Disable contenteditable elements
const editables = document.querySelectorAll('[contenteditable="true"]');
editables.forEach((el) => {
const element = el as HTMLElement;
if (!element.dataset.verificationProtected) {
element.dataset.verificationProtected = 'true';
element.contentEditable = 'false';
element.style.opacity = '0.5';
}
});
}
function preventInteraction(event: Event) {
if (!getIsVerified()) {
event.preventDefault();
event.stopImmediatePropagation();
function preventInteraction(event: Event) {
if (!getIsVerified()) {
event.preventDefault();
event.stopImmediatePropagation();
// Show warning
if (event.target) {
const target = event.target as HTMLElement;
target.setAttribute('title', 'Please verify your email to use this feature');
}
// Show warning
if (event.target) {
const target = event.target as HTMLElement;
target.setAttribute('title', 'Please verify your email to use this feature');
}
// Optional: Show alert
console.warn('🔒 Email verification required to interact with this element');
return false;
}
}
// Optional: Show alert
console.warn('🔒 Email verification required to interact with this element');
return false;
}
}
// Function to restore interactions when verified
function enableInteractions() {
const protectedElements = document.querySelectorAll('[data-verification-protected="true"]');
protectedElements.forEach((el) => {
const element = el as HTMLElement;
// Function to restore interactions when verified
function enableInteractions() {
const protectedElements = document.querySelectorAll('[data-verification-protected="true"]');
protectedElements.forEach((el) => {
const element = el as HTMLElement;
// Restore styles
if (element.dataset.originalPointerEvents) {
element.style.pointerEvents = element.dataset.originalPointerEvents;
} else {
element.style.pointerEvents = '';
}
element.style.opacity = '';
element.style.cursor = '';
element.removeAttribute('title');
// Restore styles
if (element.dataset.originalPointerEvents) {
element.style.pointerEvents = element.dataset.originalPointerEvents;
} else {
element.style.pointerEvents = '';
}
element.style.opacity = '';
element.style.cursor = '';
element.removeAttribute('title');
// Restore functionality for inputs
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT') {
const input = element as HTMLInputElement;
input.disabled = input.dataset.originalDisabled === 'true';
}
// Restore functionality for inputs
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT') {
const input = element as HTMLInputElement;
input.disabled = input.dataset.originalDisabled === 'true';
}
// Restore contenteditable
if (element.hasAttribute('contenteditable')) {
element.contentEditable = 'true';
}
// Restore contenteditable
if (element.hasAttribute('contenteditable')) {
element.contentEditable = 'true';
}
// Remove event listeners
element.removeEventListener('click', preventInteraction, true);
element.removeEventListener('mousedown', preventInteraction, true);
element.removeEventListener('focus', preventInteraction, true);
element.removeEventListener('input', preventInteraction, true);
// Remove event listeners
element.removeEventListener('click', preventInteraction, true);
element.removeEventListener('mousedown', preventInteraction, true);
element.removeEventListener('focus', preventInteraction, true);
element.removeEventListener('input', preventInteraction, true);
// Clean up data attributes
delete element.dataset.verificationProtected;
delete element.dataset.originalPointerEvents;
delete element.dataset.originalDisabled;
});
}
// Clean up data attributes
delete element.dataset.verificationProtected;
delete element.dataset.originalPointerEvents;
delete element.dataset.originalDisabled;
});
}
// Set up mutation observer to protect new elements
if (window.MutationObserver) {
domObserver = new MutationObserver((mutations) => {
let shouldReapply = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
shouldReapply = true;
}
});
// Set up mutation observer to protect new elements
if (window.MutationObserver) {
domObserver = new MutationObserver((mutations) => {
let shouldReapply = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
shouldReapply = true;
}
});
if (shouldReapply && !getIsVerified()) {
setTimeout(disableInteractions, 100);
}
});
if (shouldReapply && !getIsVerified()) {
setTimeout(disableInteractions, 100);
}
});
domObserver.observe(document.body, {
childList: true,
subtree: true
});
}
domObserver.observe(document.body, {
childList: true,
subtree: true
});
}
// Run protection every second
protectionInterval = setInterval(() => {
if (getIsVerified()) {
enableInteractions();
cleanup();
} else {
disableInteractions();
}
}, 1000);
// Run protection every second
protectionInterval = setInterval(() => {
if (getIsVerified()) {
enableInteractions();
cleanup();
} else {
disableInteractions();
}
}, 1000);
// Initial protection
if (!getIsVerified()) {
disableInteractions();
}
// Initial protection
if (!getIsVerified()) {
disableInteractions();
}
// Add global CSS to prevent any missed interactions
if (document) {
const style = document.createElement('style');
style.id = 'email-verification-protection';
style.textContent = `
// Add global CSS to prevent any missed interactions
if (document) {
const style = document.createElement('style');
style.id = 'email-verification-protection';
style.textContent = `
body:not(.email-verified) button:not([data-verification-modal] button),
body:not(.email-verified) a:not([data-verification-modal] a),
body:not(.email-verified) input:not([data-verification-modal] input),
@@ -210,45 +208,44 @@ export function setupDOMProtection(
user-select: none !important;
}
`;
document.head.appendChild(style);
}
document.head.appendChild(style);
}
function cleanup() {
if (protectionInterval) {
clearInterval(protectionInterval);
protectionInterval = undefined;
}
if (domObserver) {
domObserver.disconnect();
domObserver = undefined;
}
function cleanup() {
if (protectionInterval) {
clearInterval(protectionInterval);
protectionInterval = undefined;
}
if (domObserver) {
domObserver.disconnect();
domObserver = undefined;
}
// Clean up protection styles
if (browser) {
const style = document?.getElementById('email-verification-protection');
if (style) style.remove();
}
}
// Clean up protection styles
if (browser) {
const style = document?.getElementById('email-verification-protection');
if (style) style.remove();
}
}
return {
protectionInterval,
domObserver,
cleanup
};
return {
protectionInterval,
domObserver,
cleanup
};
}
/**
* Updates the body class based on email verification status
*/
export function updateBodyClass(isEmailVerified: boolean | null) {
if (!browser) {
return;
}
if (!browser) {
return;
}
if (isEmailVerified) {
document.body.classList.add('email-verified');
} else {
document.body.classList.remove('email-verified');
}
if (isEmailVerified) {
document.body.classList.add('email-verified');
} else {
document.body.classList.remove('email-verified');
}
}
@@ -41,30 +41,29 @@
<Dialog.Title>{$t('add_org.create_org')}</Dialog.Title>
</Dialog.Header>
<form onsubmit={preventDefault(createNewOrg)} class="px-2">
<Field.Group>
<Field.Field>
<Field.Label>{$t('add_org.name')}</Field.Label>
<Input bind:value={orgName} placeholder="e.g Pepsi Co" autofocus />
{#if orgApi.errors.name || orgApi.errors.orgName}
<Field.Error>{orgApi.errors.name || orgApi.errors.orgName}</Field.Error>
{/if}
</Field.Field>
<Field.Group>
<Field.Field>
<Field.Label>{$t('add_org.name')}</Field.Label>
<Input bind:value={orgName} placeholder="e.g Pepsi Co" autofocus />
{#if orgApi.errors.name || orgApi.errors.orgName}
<Field.Error>{orgApi.errors.name || orgApi.errors.orgName}</Field.Error>
{/if}
</Field.Field>
<Field.Field>
<Field.Label>{$t('add_org.org_sitename')}</Field.Label>
<DomainInput bind:value={siteName} placeholder="myschool" prefix="https://" suffix=".classroomio.com" />
{#if orgApi.errors.siteName || orgApi.errors.general}
<Field.Error>{orgApi.errors.siteName || orgApi.errors.general}</Field.Error>
{/if}
</Field.Field>
<Field.Field>
<Field.Label>{$t('add_org.org_sitename')}</Field.Label>
<DomainInput bind:value={siteName} placeholder="myschool" prefix="https://" suffix=".classroomio.com" />
{#if orgApi.errors.siteName || orgApi.errors.general}
<Field.Error>{orgApi.errors.siteName || orgApi.errors.general}</Field.Error>
{/if}
</Field.Field>
<Field.Field orientation="horizontal">
<Button type="submit" loading={orgApi.isLoading} class="ml-auto">
{$t('add_org.create')}
</Button>
</Field.Field>
</Field.Group>
</form>
<Field.Field orientation="horizontal">
<Button type="submit" loading={orgApi.isLoading} class="ml-auto">
{$t('add_org.create')}
</Button>
</Field.Field>
</Field.Group>
</form>
</Dialog.Content>
</Dialog.Root>
@@ -27,4 +27,3 @@
<p class="text-sm text-red-600">{$t('courses.course_card.error_message')}</p>
</div>
{/await}
@@ -25,4 +25,3 @@
</div>
</div>
</section>
@@ -24,4 +24,3 @@
<CourseCardLoader />
</div>
</section>
@@ -3,4 +3,3 @@ import { writable } from 'svelte/store';
export const newOrgModal = writable({
open: false
});
@@ -43,29 +43,29 @@
</Dialog.Title>
</Dialog.Header>
<div class="my-10 flex w-full flex-col items-center">
<h1 class="text-center text-lg dark:text-white">
{#if $deleteModal.isQuestion}
{$t('components.quiz.delete_question')}
{:else}
{$t('components.quiz.delete_quiz')}
{/if}
{$t('components.quiz.lose_content')}
</h1>
<h1 class="text-center text-lg dark:text-white">
{#if $deleteModal.isQuestion}
{$t('components.quiz.delete_question')}
{:else}
{$t('components.quiz.delete_quiz')}
{/if}
{$t('components.quiz.lose_content')}
</h1>
<div class="mt-5">
<Button
onclick={() => {
if ($deleteModal.isQuestion) {
onDelete();
$deleteModal.open = false;
} else {
deleteQuiz();
}
}}
>
{$t('components.quiz.delete')}
</Button>
<div class="mt-5">
<Button
onclick={() => {
if ($deleteModal.isQuestion) {
onDelete();
$deleteModal.open = false;
} else {
deleteQuiz();
}
}}
>
{$t('components.quiz.delete')}
</Button>
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Root>
@@ -114,28 +114,27 @@
<Dialog.Content class="w-4/5 md:w-2/5">
<Dialog.Header>
<Dialog.Title>
{$createQuizModal.openEdit ? $t('components.quiz.update') : $t('components.quiz.create')} {$t(
'components.quiz.a_quiz'
)}
{$createQuizModal.openEdit ? $t('components.quiz.update') : $t('components.quiz.create')}
{$t('components.quiz.a_quiz')}
</Dialog.Title>
</Dialog.Header>
<form onsubmit={preventDefault(createQuiz)}>
<InputField
label={$t('components.quiz.quiz_title')}
bind:value={$createQuizModal.title}
autofocus={true}
placeholder={$t('components.quiz.placehoolder')}
className="mb-4"
isRequired={true}
errorMessage={errors.title}
autoComplete={false}
/>
<InputField
label={$t('components.quiz.quiz_title')}
bind:value={$createQuizModal.title}
autofocus={true}
placeholder={$t('components.quiz.placehoolder')}
className="mb-4"
isRequired={true}
errorMessage={errors.title}
autoComplete={false}
/>
<div class="mt-5 flex items-center justify-end">
<Button type="submit" disabled={isLoading} loading={isLoading}>
{$createQuizModal.openEdit ? $t('components.quiz.save') : $t('components.quiz.continue')}
</Button>
</div>
</form>
<div class="mt-5 flex items-center justify-end">
<Button type="submit" disabled={isLoading} loading={isLoading}>
{$createQuizModal.openEdit ? $t('components.quiz.save') : $t('components.quiz.continue')}
</Button>
</div>
</form>
</Dialog.Content>
</Dialog.Root>
@@ -53,10 +53,7 @@
errorMessage={optionHasError(option.id, currentError.options) && $t('components.quiz.label')}
/>
<div class="absolute right-2 top-2 flex justify-end">
<IconButton
onclick={() => (option.isCorrect = !option.isCorrect)}
class={option.isCorrect ? 'success' : ''}
>
<IconButton onclick={() => (option.isCorrect = !option.isCorrect)} class={option.isCorrect ? 'success' : ''}>
<CircleCheckIcon size={16} filled={option.isCorrect} />
</IconButton>
</div>
@@ -114,4 +114,3 @@ export const landingPageSettings: Writable<OrgLandingPageJson> = writable({
show: true
}
});
@@ -38,4 +38,3 @@
</p>
</div>
</div>
@@ -44,4 +44,3 @@
</div>
</div>
</div>
@@ -10,4 +10,3 @@
<Skeleton class="h-40 w-[270px]" />
</div>
</div>
@@ -23,27 +23,18 @@
<Dialog.Title>{$t('delete_modal.label')}</Dialog.Title>
</Dialog.Header>
<div>
<p class="text-center text-xl dark:text-white">
{$t('delete_modal.content')}
</p>
<p class="text-center text-xl dark:text-white">
{$t('delete_modal.content')}
</p>
<div class="mt-8 flex items-center justify-between">
<Button
variant="outline"
onclick={() => (open = false)}
disabled={isLoading}
>
{$t('delete_modal.no')}
</Button>
<Button
variant="destructive"
onclick={onDelete}
loading={isLoading}
>
{$t('delete_modal.yes')}
</Button>
<div class="mt-8 flex items-center justify-between">
<Button variant="outline" onclick={() => (open = false)} disabled={isLoading}>
{$t('delete_modal.no')}
</Button>
<Button variant="destructive" onclick={onDelete} loading={isLoading}>
{$t('delete_modal.yes')}
</Button>
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Root>
@@ -15,4 +15,3 @@
<line x1="17" y1="7" x2="7" y2="17" /><polyline points="8 7 17 7 17 16" />
</svg>
</span>
@@ -51,4 +51,3 @@
fill: none; /* Ensure the checkmark path itself is not filled */
}
</style>
@@ -41,4 +41,3 @@
stroke-linejoin="bevel"
/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -15,4 +15,3 @@
stroke-width="5"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -183,4 +183,3 @@
fill="#E4E4E4"
/>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

@@ -183,4 +183,3 @@
fill="#5C6065"
/>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

@@ -24,4 +24,3 @@
fill="#EB4132"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -15,4 +15,3 @@
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -46,12 +46,12 @@ export interface BreadcrumbConfig {
* Can be a string or a function that extracts the prefix dynamically
*/
pathPrefix: string | ((pathname: string) => string);
/**
* Base path for generating hrefs (e.g., '/lms', '/org/myorg')
*/
basePath: string | ((pathname: string) => string);
/**
* Resolver for dynamic segment titles
* Can resolve from page.data, stores, or API calls
@@ -62,12 +62,12 @@ export interface BreadcrumbConfig {
fullPathname: string,
context?: unknown
) => string | Promise<string> | null | undefined;
/**
* Optional: Cache breadcrumbs for better performance
*/
enableCache?: boolean;
/**
* Optional: Maximum breadcrumb depth (prevents infinite recursion)
*/
@@ -107,7 +107,7 @@ function getCachedOrGenerate(
}
clearExpiredCache();
const cached = breadcrumbCache.get(cacheKey);
if (cached) {
return cached;
@@ -122,31 +122,25 @@ function getCachedOrGenerate(
/**
* Extract path prefix from pathname
*/
function extractPathPrefix(
pathname: string,
prefixConfig: string | ((pathname: string) => string)
): string {
function extractPathPrefix(pathname: string, prefixConfig: string | ((pathname: string) => string)): string {
if (typeof prefixConfig === 'function') {
return prefixConfig(pathname);
}
// Handle dynamic prefixes like '/org/[slug]'
if (prefixConfig.includes('[')) {
const pattern = prefixConfig.replace(/\[.*?\]/g, '[^/]+');
const match = pathname.match(new RegExp(`^${pattern}`));
return match ? match[0] : '';
}
return pathname.startsWith(prefixConfig) ? prefixConfig : '';
}
/**
* Get base path for href generation
*/
function getBasePath(
pathname: string,
basePathConfig: string | ((pathname: string) => string)
): string {
function getBasePath(pathname: string, basePathConfig: string | ((pathname: string) => string)): string {
if (typeof basePathConfig === 'function') {
return basePathConfig(pathname);
}
@@ -228,19 +222,16 @@ async function resolveDynamicTitle(
/**
* Extract parameters from pathname (e.g., { slug: 'my-post' })
*/
function extractParamsFromPath(
pathname: string,
parentPath: string
): Record<string, string> {
function extractParamsFromPath(pathname: string, parentPath: string): Record<string, string> {
const params: Record<string, string> = {};
const relativePath = pathname.replace(parentPath, '').replace(/^\//, '');
const segments = relativePath.split('/').filter(Boolean);
// Simple extraction - can be enhanced for more complex patterns
segments.forEach((segment, index) => {
params[`param${index}`] = segment;
});
return params;
}
@@ -272,7 +263,7 @@ async function generateNestedBreadcrumbs(
// Check for static nested route
if (navItem.nestedRoutes) {
const nestedRoute = navItem.nestedRoutes.find((route) => route.path === currentSegment);
if (nestedRoute) {
breadcrumbs.push({
label: nestedRoute.titleKey,
@@ -300,16 +291,11 @@ async function generateNestedBreadcrumbs(
}
// Handle dynamic segment
if (navItem.supportsDynamicSegment ||
navItem.nestedRoutes?.some((route) => route.supportsDynamicSegment && route.path !== currentSegment)) {
const dynamicTitle = await resolveDynamicTitle(
currentSegment,
currentPath,
fullPathname,
navItem,
config,
context
);
if (
navItem.supportsDynamicSegment ||
navItem.nestedRoutes?.some((route) => route.supportsDynamicSegment && route.path !== currentSegment)
) {
const dynamicTitle = await resolveDynamicTitle(currentSegment, currentPath, fullPathname, navItem, config, context);
if (dynamicTitle) {
breadcrumbs.push({
@@ -323,7 +309,7 @@ async function generateNestedBreadcrumbs(
const supportingNestedRoute = navItem.nestedRoutes?.find(
(route) => route.supportsDynamicSegment && route.nestedRoutes
);
if (supportingNestedRoute) {
const nestedBreadcrumbs = await generateNestedBreadcrumbs(
remainingSegments,
@@ -356,7 +342,7 @@ export async function generateBreadcrumbs(
context?: unknown
): Promise<BreadcrumbItem[]> {
const cacheKey = `${pathname}${searchParams}`;
return getCachedOrGenerate(
cacheKey,
() => generateBreadcrumbsSync(pathname, searchParams, navItems, config, context),
@@ -381,7 +367,7 @@ export function generateBreadcrumbsSync(
// Extract path prefix and base path
const pathPrefix = extractPathPrefix(pathname, config.pathPrefix);
const basePath = getBasePath(pathname, config.basePath);
// Get relative path after removing prefix
const relativePath = pathname.replace(pathPrefix, '').replace(/^\//, '') || '';
const pathSegments = relativePath.split('/').filter(Boolean);
@@ -400,10 +386,8 @@ export function generateBreadcrumbsSync(
}
// Add parent breadcrumb
const parentHref = matchedNavItem.useHashUrl
? `${basePath}${matchedNavItem.path}`
: matchedNavItem.url;
const parentHref = matchedNavItem.useHashUrl ? `${basePath}${matchedNavItem.path}` : matchedNavItem.url;
breadcrumbs.push({
label: matchedNavItem.breadcrumbLabel || matchedNavItem.title,
href: parentHref
@@ -411,10 +395,8 @@ export function generateBreadcrumbsSync(
// Handle query params (settings tabs, etc.)
if (matchedNavItem.items && searchParams) {
const subItem = matchedNavItem.items.find((sub) =>
isActive(pathWithQuery, sub.url, undefined, true)
);
const subItem = matchedNavItem.items.find((sub) => isActive(pathWithQuery, sub.url, undefined, true));
if (subItem) {
breadcrumbs.push({
label: subItem.breadcrumbLabel || subItem.title,
@@ -428,7 +410,7 @@ export function generateBreadcrumbsSync(
if (pathSegments.length > 1) {
const remainingSegments = pathSegments.slice(1);
const parentPath = `${basePath}${matchedNavItem.path}`;
// Note: For async resolvers, you'd need to use generateBreadcrumbs instead
const nestedBreadcrumbs = generateNestedBreadcrumbsSync(
remainingSegments,
@@ -440,7 +422,7 @@ export function generateBreadcrumbsSync(
config,
context
);
breadcrumbs.push(...nestedBreadcrumbs);
}
@@ -475,7 +457,7 @@ function generateNestedBreadcrumbsSync(
// Check for static nested route
if (navItem.nestedRoutes) {
const nestedRoute = navItem.nestedRoutes.find((route) => route.path === currentSegment);
if (nestedRoute) {
breadcrumbs.push({
label: nestedRoute.titleKey,
@@ -506,14 +488,9 @@ function generateNestedBreadcrumbsSync(
if (navItem.supportsDynamicSegment) {
// Try to resolve title synchronously (from context if available)
let dynamicTitle: string | null = null;
if (config.dynamicTitleResolver) {
const result = config.dynamicTitleResolver(
currentSegment,
currentPath,
fullPathname,
context
);
const result = config.dynamicTitleResolver(currentSegment, currentPath, fullPathname, context);
// Only use if it's not a promise
if (typeof result === 'string') {
dynamicTitle = result;
@@ -531,7 +508,7 @@ function generateNestedBreadcrumbsSync(
const supportingNestedRoute = navItem.nestedRoutes?.find(
(route) => route.supportsDynamicSegment && route.nestedRoutes
);
if (supportingNestedRoute?.nestedRoutes) {
const nestedBreadcrumbs = generateNestedBreadcrumbsSync(
remainingSegments,
@@ -560,4 +537,3 @@ export function clearBreadcrumbCache() {
breadcrumbCache.clear();
cacheTimestamps.clear();
}
@@ -105,4 +105,3 @@ export function generateLmsBreadcrumbs(
return breadcrumbs;
}
@@ -15,7 +15,7 @@
// Get breadcrumb title from store for LMS community posts
const breadcrumbTitle = $derived($currentCommunityQuestion.title);
const breadcrumbs = $derived(
generateLmsBreadcrumbs(
page.url.pathname,
@@ -98,4 +98,3 @@
{/if}
</Breadcrumb.List>
</Breadcrumb.Root>
@@ -60,4 +60,3 @@
{/if}
</div>
</header>
@@ -14,4 +14,3 @@
</script>
<div class="senja-embed" data-id={id} data-lazyload="false" data-spinner="false"></div>
@@ -75,4 +75,3 @@
{/each}
</Sidebar.Menu>
</Sidebar.Group>
@@ -108,78 +108,84 @@
<Dialog.Title>{$t('course.navItem.landing_page.upload_widget.title')}</Dialog.Title>
</Dialog.Header>
<div class="w-full bg-white p-5 dark:bg-inherit">
<UnderlineTabs.Root bind:value={currentTab}>
<UnderlineTabs.List>
{#each tabs as tab}
<UnderlineTabs.Trigger value={tab.value}>
{tab.label}
</UnderlineTabs.Trigger>
{/each}
</UnderlineTabs.List>
<UnderlineTabs.Content value={tabs[1].value}>
<!-- Your Upload content here -->
<div class="w-full">
<input
type="file"
style="display: none;"
bind:this={fileInput}
onchange={onFileSelected}
disabled={isUploading}
/>
<Button onclick={handleUpload} loading={isUploading}>
{label}
</Button>
<p class="my-2 text-center text-sm text-gray-500">
{$t('course.navItem.landing_page.upload_widget.width')}
</p>
<p class="text-center text-sm text-gray-500">
{$t('course.navItem.landing_page.upload_widget.size')}
</p>
</div>
</UnderlineTabs.Content>
<UnderlineTabs.Content value={tabs[0].value}>
<!-- Your Images content here -->
<div class="h-full overflow-y-scroll">
<form onsubmit={preventDefault(handleSubmit)} class="mt-1 flex gap-2 pb-3">
<input type="text" bind:value={searchQuery} name="" id="" class="ml-2 w-[85%] rounded-lg dark:text-black" />
<button type="submit" class="rounded-lg border border-gray-500 bg-white px-3 py-1 text-black"
>{$t('course.navItem.landing_page.upload_widget.submit')}</button
>
</form>
{#if unsplashImages && unsplashImages.length > 0}
<div class="hide-scrollbar flex max-h-[300px] flex-row flex-wrap items-center gap-2 px-[10px]">
{#each unsplashImages as unsplashImages (unsplashImages.id)}
<div>
<div class="relative h-[130px] w-[195px] overflow-hidden">
<button onclick={() => handleImageClick(unsplashImages.urls.regular)}>
<img
src={unsplashImages.urls.regular}
alt={unsplashImages.alt_description}
class="h-full w-full cursor-pointer rounded-md object-cover hover:opacity-80"
/>
</button>
</div>
{#if unsplashImages.user.name}
<p class="mt-1 text-center text-xs font-light text-gray-500">
By <a
href={`https://unsplash.com/@${unsplashImages.user.username}`}
target="_blank"
class="hover:text-red-700">{unsplashImages.user.name}</a
>
</p>
{/if}
</div>
{/each}
</div>
{:else}
<p class="pt-7 text-center">
{$t('course.navItem.landing_page.upload_widget.no_images')}
<UnderlineTabs.Root bind:value={currentTab}>
<UnderlineTabs.List>
{#each tabs as tab}
<UnderlineTabs.Trigger value={tab.value}>
{tab.label}
</UnderlineTabs.Trigger>
{/each}
</UnderlineTabs.List>
<UnderlineTabs.Content value={tabs[1].value}>
<!-- Your Upload content here -->
<div class="w-full">
<input
type="file"
style="display: none;"
bind:this={fileInput}
onchange={onFileSelected}
disabled={isUploading}
/>
<Button onclick={handleUpload} loading={isUploading}>
{label}
</Button>
<p class="my-2 text-center text-sm text-gray-500">
{$t('course.navItem.landing_page.upload_widget.width')}
</p>
{/if}
</div>
</UnderlineTabs.Content>
</UnderlineTabs.Root>
</div>
<p class="text-center text-sm text-gray-500">
{$t('course.navItem.landing_page.upload_widget.size')}
</p>
</div>
</UnderlineTabs.Content>
<UnderlineTabs.Content value={tabs[0].value}>
<!-- Your Images content here -->
<div class="h-full overflow-y-scroll">
<form onsubmit={preventDefault(handleSubmit)} class="mt-1 flex gap-2 pb-3">
<input
type="text"
bind:value={searchQuery}
name=""
id=""
class="ml-2 w-[85%] rounded-lg dark:text-black"
/>
<button type="submit" class="rounded-lg border border-gray-500 bg-white px-3 py-1 text-black"
>{$t('course.navItem.landing_page.upload_widget.submit')}</button
>
</form>
{#if unsplashImages && unsplashImages.length > 0}
<div class="hide-scrollbar flex max-h-[300px] flex-row flex-wrap items-center gap-2 px-[10px]">
{#each unsplashImages as unsplashImages (unsplashImages.id)}
<div>
<div class="relative h-[130px] w-[195px] overflow-hidden">
<button onclick={() => handleImageClick(unsplashImages.urls.regular)}>
<img
src={unsplashImages.urls.regular}
alt={unsplashImages.alt_description}
class="h-full w-full cursor-pointer rounded-md object-cover hover:opacity-80"
/>
</button>
</div>
{#if unsplashImages.user.name}
<p class="mt-1 text-center text-xs font-light text-gray-500">
By <a
href={`https://unsplash.com/@${unsplashImages.user.username}`}
target="_blank"
class="hover:text-red-700">{unsplashImages.user.name}</a
>
</p>
{/if}
</div>
{/each}
</div>
{:else}
<p class="pt-7 text-center">
{$t('course.navItem.landing_page.upload_widget.no_images')}
</p>
{/if}
</div>
</UnderlineTabs.Content>
</UnderlineTabs.Root>
</div>
</Dialog.Content>
</Dialog.Root>
@@ -16,4 +16,3 @@
<ArrowUpIcon size={16} />
<Chip {value} />
</Button>