Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/[locale]/notifications/tenders/TendersList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ interface TendersListProps {
deletingId?: number | null;
}

export default TendersList;

export function TendersList({
tenders,
Comment on lines 29 to 33

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export default TendersList; appears before TendersList is declared, which is a compile-time error. If you want a default export, export the function as default (e.g., export default function TendersList(...)) or move the default export to after the function declaration.

Copilot uses AI. Check for mistakes.
locale,
Expand Down
27 changes: 16 additions & 11 deletions app/[locale]/notifications/tenders/TendersPageClient.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
'use client';

import React, { Suspense } from 'react';
import Link from 'next/link';
import { useState } from 'react';

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import React, { Suspense } from 'react' imports React but the identifier isn’t used in this file (new JSX transform), which will trigger @typescript-eslint/no-unused-vars. Combine your React imports into a single named import (e.g. include useState there) and drop the unused default import.

Suggested change
import React, { Suspense } from 'react';
import Link from 'next/link';
import { useState } from 'react';
import { Suspense, useState } from 'react';
import Link from 'next/link';

Copilot uses AI. Check for mistakes.
import { FaPlus } from 'react-icons/fa';

// Removed stray comment
import { Button } from '~/components/buttons';
import {
Comment on lines 7 to 10

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the // Removed stray comment note—this kind of meta-comment adds noise and isn’t useful for future readers.

Copilot uses AI. Check for mistakes.
deleteTenderAction,
type TenderWithStatus,
} from '~/server/actions/tenders';
import type { TendersTranslations } from '~/i18n/translate/tenders';

import { TendersList } from './TendersList';
import TendersList from './TendersList';

interface TendersPageClientProps {
allTenders: TenderWithStatus[];
Expand Down Expand Up @@ -41,7 +43,6 @@ export function TendersPageClient({
// Handle delete - updates state for both tabs
const handleDelete = async (id: number) => {
if (!confirm(text.confirmDelete)) return;

setDeletingId(id);
try {
const result = await deleteTenderAction(id);
Expand All @@ -58,6 +59,8 @@ export function TendersPageClient({
}
};

// Removed duplicate import statement for Link
// import Link from 'next/link';
return (

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the commented-out Link import and the note about removing it. Commented-out code and meta-notes like this tend to rot and make the file harder to scan.

Copilot uses AI. Check for mistakes.
<main className="container mx-auto px-4 py-8">
{/* Header with Add button */}
Expand Down Expand Up @@ -99,15 +102,17 @@ export function TendersPageClient({
</div>

{/* Tenders List */}
<TendersList
tenders={displayedTenders}
locale={locale}
canManage={canManage}
text={text}
isArchived={activeTab === 'archived'}
onDelete={handleDelete}
deletingId={deletingId}
/>
<Suspense fallback={<div>Loading tenders...</div>}>
<TendersList
tenders={displayedTenders}
locale={locale}
canManage={canManage}

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<Suspense> won’t provide lazy-loading here because TendersList is imported synchronously and doesn’t suspend. If the intent is lazy-loading, switch TendersList to a React.lazy(() => import('./TendersList'))/Next dynamic() import; otherwise drop the Suspense wrapper.

Copilot uses AI. Check for mistakes.
text={text}
isArchived={activeTab === 'archived'}
onDelete={handleDelete}
deletingId={deletingId}
/>
</Suspense>
</main>
);
}
46 changes: 46 additions & 0 deletions components/lazy/DynamicLazy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, {
type ComponentType,
lazy,
type LazyExoticComponent,
Suspense,
} from 'react';

export interface DynamicLazyOptions {
fallback?: React.ReactNode;
}

export interface DynamicLazyComponent<P> {
(props: P): JSX.Element;
preload: () => Promise<void>;
}

export function DynamicLazy<P extends React.JSX.IntrinsicAttributes>(
loader: () => Promise<{ default: React.ComponentType<P> }>,
options?: DynamicLazyOptions
): DynamicLazyComponent<P> {
let LoadedComponent: LazyExoticComponent<ComponentType<P>> | null = null;
let loadPromise: Promise<void> | null = null;

function preload(): Promise<void> {
if (!loadPromise) {
loadPromise = loader().then((mod) => {
LoadedComponent = lazy(() => Promise.resolve(mod));
});
}
return loadPromise ?? Promise.resolve();
}

const LazyComponent = (props: P) => {
if (!LoadedComponent) {
LoadedComponent = lazy(loader);
}
const Component = LoadedComponent;
return (
<Suspense fallback={options?.fallback ?? null}>
<Component {...(props as React.PropsWithRef<P>)} />
</Suspense>
);
};
(LazyComponent as DynamicLazyComponent<P>).preload = preload;
return LazyComponent as DynamicLazyComponent<P>;
}
105 changes: 105 additions & 0 deletions components/lazy/LazyLoadBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';

import { useIntersectionObserver } from './useIntersectionObserver';
import { useIdleCallback } from './useIdleCallback';

export type LazyLoadTrigger = 'viewport' | 'interaction' | 'immediate';

export interface LazyLoadBoundaryProps {
trigger?: LazyLoadTrigger;
fallback?: React.ReactNode;
children: React.ReactNode;
rootMargin?: string;
threshold?: number;
preloadOnHover?: boolean;
preloadOnIdle?: boolean;
keepMounted?: boolean;
onPreload?: () => void;
}

export const LazyLoadBoundary: React.FC<LazyLoadBoundaryProps> = ({
trigger = 'viewport',
fallback = null,
children,
rootMargin = '0px',
threshold = 0,
preloadOnHover = false,
preloadOnIdle = false,
keepMounted = false,
onPreload,
}) => {
const [mounted, setMounted] = useState(trigger === 'immediate');
const [hasInteracted, setHasInteracted] = useState(false);
const [ref, inView] = useIntersectionObserver({ rootMargin, threshold });
const hasMountedRef = useRef<boolean>(false);

// Preload on hover
const handleMouseEnter = useCallback(() => {
if (preloadOnHover && onPreload) onPreload();
if (trigger === 'interaction') setHasInteracted(true);
}, [preloadOnHover, onPreload, trigger]);

// Preload on idle
useIdleCallback(() => {
if (preloadOnIdle && onPreload) onPreload();
}, [preloadOnIdle, onPreload]);

// Mount logic
useEffect(() => {
if (trigger === 'immediate') {
setMounted(true);
} else if (trigger === 'viewport' && inView) {
setMounted(true);
} else if (trigger === 'interaction' && hasInteracted) {
setMounted(true);
}
}, [trigger, inView, hasInteracted]);

// Keep mounted
useEffect(() => {
if (mounted && keepMounted) {
hasMountedRef.current = true;
}
}, [mounted, keepMounted]);

// Clean up interaction state if not keepMounted
useEffect(() => {
return () => {
if (!keepMounted) {
setMounted(false);
setHasInteracted(false);
}
};
}, [keepMounted]);

// Render logic
if (mounted || hasMountedRef.current) {
return (
<div
ref={
trigger === 'viewport'
? (ref as React.RefObject<HTMLDivElement>)
: undefined
}
>
{children}
</div>
);
}
return (
<div
ref={
trigger === 'viewport'
? (ref as React.RefObject<HTMLDivElement>)
: undefined
}
onClick={
trigger === 'interaction' ? () => setHasInteracted(true) : undefined
}
onMouseEnter={handleMouseEnter}
style={{ width: '100%', height: '100%' }}
>
{fallback}
</div>
);
};
57 changes: 57 additions & 0 deletions components/lazy/examples.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';

import type { CarouselProps } from '~/components/carousels';

// import type { CarouselProps } from '~/components/carousels';

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s a duplicated (commented-out) CarouselProps import left in the file. Please remove commented-out imports to keep the examples clean and avoid confusion.

Suggested change
// import type { CarouselProps } from '~/components/carousels';

Copilot uses AI. Check for mistakes.
import { LazyLoadBoundary } from './LazyLoadBoundary';
import { DynamicLazy } from './DynamicLazy';

// Match the actual EventItem type from event-section.tsx
interface EventItem {
title: string;
categories?: string[];
startDate: string | Date;
endDate?: string | Date;
time?: string;
description: string;
images: string[];
location?: string;
id: number;
}

interface EventSectionProps extends React.JSX.IntrinsicAttributes {
events: EventItem[];
locale: string;
showViewAll?: boolean;
viewAllText?: string;
viewAllHref?: string;
}

const EventsGallery = DynamicLazy<EventSectionProps>(() =>
import('../events/event-section').then((mod) => ({ default: mod.default }))
);

interface GalleryCarouselProps extends React.JSX.IntrinsicAttributes {
carouselProps?: CarouselProps;
children: React.ReactNode[];
className?: string;
itemClassName?: string;
}

const GalleryBelowFold = DynamicLazy<GalleryCarouselProps>(() =>
import('../carousels/gallery').then((mod) => ({
default: mod.GalleryCarousel,
}))
);
Comment on lines +34 to +45

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GalleryBelowFold is declared but never used, which will trigger @typescript-eslint/no-unused-vars warnings. Either use it in an exported example component or remove the unused constant (and any now-unused types/imports).

Copilot uses AI. Check for mistakes.

export function ExampleEventsGallery() {
return (
<LazyLoadBoundary
trigger="interaction"
fallback={<div>Loading Gallery...</div>}
>
<EventsGallery events={[]} locale="en" />
</LazyLoadBoundary>
);
// All examples above demonstrate different trigger modes and usage patterns.
}
31 changes: 31 additions & 0 deletions components/lazy/useIdleCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect } from 'react';

interface WindowWithIdleCallback extends Window {
requestIdleCallback: (callback: IdleRequestCallback) => number;
cancelIdleCallback: (id: number) => void;
setTimeout: (callback: () => void, ms: number) => number;
clearTimeout: (id: number) => void;
}

export function useIdleCallback(
cb: () => void,
_deps: React.DependencyList = []
) {
Comment on lines +10 to +13

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React.DependencyList is referenced but React isn’t imported, which will fail type-checking. Import type DependencyList from react (or import type React from 'react') and use that type instead.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +13

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_deps is accepted but ignored, and the effect only depends on cb. Since callers typically pass an inline callback, this ends up scheduling/canceling an idle callback on every render. Use the provided deps list as the effect dependencies and/or keep the latest callback in a ref so you don’t resubscribe every render.

Copilot uses AI. Check for mistakes.
useEffect(() => {
let id: number | null = null;
if ('requestIdleCallback' in window) {
id = (window as WindowWithIdleCallback).requestIdleCallback(cb);
} else {
id = (window as WindowWithIdleCallback).setTimeout(cb, 200);
}
return () => {
if (id !== null) {
if ('cancelIdleCallback' in window) {
(window as WindowWithIdleCallback).cancelIdleCallback(id);
} else {
clearTimeout(id);
}
}
};
}, [cb]);
}
Comment on lines +27 to +31

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The effect depends only on [cb] (line with }, [cb]);), which will retrigger when an inline callback is recreated each render, cancelling/rescheduling the idle task repeatedly. Consider depending on the explicit deps list instead and invoking the latest callback via a ref.

Copilot uses AI. Check for mistakes.
41 changes: 41 additions & 0 deletions components/lazy/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useEffect, useRef, useState } from 'react';

export interface UseIntersectionObserverOptions {
root?: Element | null;
rootMargin?: string;
threshold?: number | number[];
freezeOnceVisible?: boolean;
}

export function useIntersectionObserver<T extends Element>(
options: UseIntersectionObserverOptions = {}
): [React.RefObject<T>, boolean] {
const {
Comment on lines +10 to +13

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type uses React.RefObject<T> but this file doesn’t import the React namespace/types, which will fail under TS strict ("Cannot find namespace 'React'"). Prefer importing type RefObject from react and returning RefObject<T | null>, or add import type React from 'react' and ensure the ref type matches null initialization.

Copilot uses AI. Check for mistakes.
root = null,
rootMargin = '0px',
threshold = 0,
freezeOnceVisible = true,
} = options;
const ref = useRef<T>(null);
Comment on lines +12 to +19

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useRef<T>(null) is not type-safe with strictNullChecks (enabled in tsconfig) because null isn’t assignable to T. Use useRef<T | null>(null) (and reflect that in the returned ref type) or initialize with null! if you intentionally want a non-null ref contract.

Suggested change
): [React.RefObject<T>, boolean] {
const {
root = null,
rootMargin = '0px',
threshold = 0,
freezeOnceVisible = true,
} = options;
const ref = useRef<T>(null);
): [React.RefObject<T | null>, boolean] {
const {
root = null,
rootMargin = '0px',
threshold = 0,
freezeOnceVisible = true,
} = options;
const ref = useRef<T | null>(null);

Copilot uses AI. Check for mistakes.
const [isIntersecting, setIntersecting] = useState<boolean>(false);
const frozen = isIntersecting && freezeOnceVisible;

useEffect(() => {
if (frozen) return;
const node = ref.current;
if (!node) return;
let observer: IntersectionObserver | null = null;
observer = new IntersectionObserver(
([entry]) => {
setIntersecting(entry.isIntersecting);
},
{ root, rootMargin, threshold }
);
observer.observe(node);
return () => {
observer && observer.disconnect();
};
}, [root, rootMargin, threshold, frozen]);

return [ref, isIntersecting];
}