feat: lint codebase and run precommit hook
@@ -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>
|
||||
|
||||
|
||||