feat(dependencies): add Lottie animation libraries and refactor NotFoundPage
- Added `@lottiefiles/dotlottie-react` and `lottie-react` dependencies for enhanced animation capabilities. - Refactored the NotFoundPage component to utilize a dedicated NotFoundContent component, improving code organization and readability. - Updated package.json and pnpm-lock.yaml to reflect new dependencies and their versions.
This commit is contained in:
@@ -1,37 +1,5 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import NotFoundContent from "@/components/not-found-content";
|
||||
|
||||
const NotFoundPage = () => {
|
||||
return (
|
||||
<div className="mx-auto flex w-6/12 flex-col gap-12">
|
||||
<section className="flex w-full items-center justify-center">
|
||||
<Image src={"/404.svg"} alt="404" width={700} height={1000} />
|
||||
</section>
|
||||
<section>
|
||||
<div>
|
||||
<Image
|
||||
src={"/cable.svg"}
|
||||
width={2000}
|
||||
height={3000}
|
||||
alt="not connected cable"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className="mt-10 flex flex-col items-center justify-center gap-10">
|
||||
<h1 className="text-2xl font-bold">Page not found</h1>
|
||||
<p className="text-[16px] font-semibold text-[#77707f]">
|
||||
Sorry, the page you're looking for does not exist or has been
|
||||
removed.
|
||||
<br />
|
||||
You can navigate to Home using
|
||||
<Link href="/" className="mx-1 text-primary">
|
||||
This
|
||||
</Link>
|
||||
link.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFoundPage;
|
||||
export default function NotFoundPage() {
|
||||
return <NotFoundContent />;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { GlobeIcon } from "@/assets/icons";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { Button } from "@/components/ui-elements/button";
|
||||
import Status from "@/components/ui/Status";
|
||||
import { Languages } from "@/constants/languages";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useRef } from "react";
|
||||
import type { FieldValues } from "react-hook-form";
|
||||
import { TENDER_HEADER_TITLE_CLASS } from "../constants";
|
||||
import { useStackTenderHeaderActions } from "../hooks/use-stack-tender-header-actions";
|
||||
|
||||
type LanguageSelectOnChange = NonNullable<
|
||||
ComponentProps<typeof Select<FieldValues>>["onChange"]
|
||||
@@ -19,7 +17,6 @@ type TenderDetailsHeaderProps = {
|
||||
title: string;
|
||||
selectedLanguage: string;
|
||||
onLanguageChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
onTranslate: () => void;
|
||||
isTranslating: boolean;
|
||||
status: string | undefined;
|
||||
};
|
||||
@@ -28,94 +25,69 @@ export function TenderDetailsHeader({
|
||||
title,
|
||||
selectedLanguage,
|
||||
onLanguageChange,
|
||||
onTranslate,
|
||||
isTranslating,
|
||||
status,
|
||||
}: TenderDetailsHeaderProps) {
|
||||
const headerLayoutContainerRef = useRef<HTMLDivElement>(null);
|
||||
const headerTitleProbeSizerRef = useRef<HTMLDivElement>(null);
|
||||
const stackActionsBelow = useStackTenderHeaderActions(
|
||||
title,
|
||||
headerLayoutContainerRef,
|
||||
headerTitleProbeSizerRef,
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={headerLayoutContainerRef} className="relative">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 top-0 -z-10 max-h-0 overflow-hidden opacity-0"
|
||||
aria-hidden
|
||||
>
|
||||
<div ref={headerTitleProbeSizerRef}>
|
||||
<div className="relative w-full">
|
||||
<div className="flex w-full min-w-0 flex-col gap-5">
|
||||
<div className="w-full min-w-0 space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-primary">
|
||||
Tender
|
||||
</p>
|
||||
<Status status={status ?? "Unknown"}>{status}</Status>
|
||||
</div>
|
||||
<h1 className={TENDER_HEADER_TITLE_CLASS}>{title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-5",
|
||||
!stackActionsBelow &&
|
||||
"lg:flex-row lg:items-start lg:justify-between lg:gap-8",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 space-y-2",
|
||||
!stackActionsBelow && "lg:flex-1",
|
||||
stackActionsBelow && "lg:w-full",
|
||||
)}
|
||||
>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-primary">
|
||||
Tender
|
||||
</p>
|
||||
<h1 className={TENDER_HEADER_TITLE_CLASS}>{title}</h1>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full min-w-0 shrink-0 flex-col gap-3",
|
||||
!stackActionsBelow && "lg:max-w-2xl lg:items-end",
|
||||
stackActionsBelow && "lg:w-full lg:items-stretch",
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cn(
|
||||
"text-[11px] font-semibold uppercase tracking-[0.18em] text-dark-5 dark:text-dark-6 lg:w-full",
|
||||
stackActionsBelow ? "lg:text-start" : "lg:text-end",
|
||||
)}
|
||||
>
|
||||
Language & translation
|
||||
</p>
|
||||
<div className="flex w-full min-w-0 shrink-0 flex-col">
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center",
|
||||
stackActionsBelow
|
||||
? "sm:justify-start lg:justify-start"
|
||||
: "sm:justify-end lg:justify-end",
|
||||
"relative overflow-hidden rounded-[1.35rem] border border-stroke/55 bg-white/75 p-4 shadow-[0_12px_42px_-14px_rgba(15,23,42,0.14),0_0_0_1px_rgba(87,80,241,0.05)] backdrop-blur-md dark:border-white/[0.09] dark:bg-gradient-to-br dark:from-dark-2/95 dark:via-dark-3/55 dark:to-violet-600/[0.12] dark:shadow-[0_18px_50px_-18px_rgba(0,0,0,0.5),inset_0_1px_0_0_rgba(255,255,255,0.04)] sm:p-5",
|
||||
)}
|
||||
>
|
||||
<Select<FieldValues>
|
||||
name="lang"
|
||||
label="Display language"
|
||||
items={Languages}
|
||||
className={cn(
|
||||
"w-full shrink-0 space-y-0 sm:w-[min(100%,11.5rem)]",
|
||||
"[&_label]:sr-only",
|
||||
)}
|
||||
value={selectedLanguage}
|
||||
onChange={onLanguageChange as LanguageSelectOnChange}
|
||||
<div
|
||||
className="pointer-events-none absolute -right-6 -top-10 h-28 w-28 rounded-full bg-gradient-to-br from-primary/25 to-violet-500/10 blur-2xl dark:from-violet-500/30 dark:to-primary/5"
|
||||
aria-hidden
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
shape="full"
|
||||
className="w-full shrink-0 whitespace-nowrap sm:min-h-[3.375rem] sm:w-auto"
|
||||
label={
|
||||
isTranslating ? "Translating..." : "Translate metadata"
|
||||
}
|
||||
disabled={isTranslating}
|
||||
onClick={onTranslate}
|
||||
/>
|
||||
<div className="flex shrink-0 items-center sm:min-h-[3.375rem]">
|
||||
<Status status={status ?? "Unknown"}>{status}</Status>
|
||||
<div className="relative space-y-4">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-primary/20 to-violet-600/10 text-primary shadow-inner ring-1 ring-primary/15 dark:from-primary/30 dark:to-violet-500/15 dark:ring-white/10">
|
||||
<GlobeIcon className="h-5 w-5 opacity-95" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 pt-0.5">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-dark-5 dark:text-dark-6">
|
||||
Language & translation
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-relaxed text-dark-5/90 dark:text-dark-6/95">
|
||||
Changing language updates the URL, translates metadata, and
|
||||
reloads this view.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select<FieldValues>
|
||||
name="lang"
|
||||
label="Display language"
|
||||
items={Languages}
|
||||
disabled={isTranslating}
|
||||
prefixIcon={
|
||||
<GlobeIcon className="h-[1.05rem] w-[1.05rem] text-primary dark:text-primary/90" />
|
||||
}
|
||||
className={cn(
|
||||
"w-full shrink-0 space-y-0",
|
||||
"[&_label]:sr-only",
|
||||
"[&_select]:h-11 [&_select]:min-h-[2.75rem] [&_select]:rounded-full [&_select]:border-stroke/60 [&_select]:bg-white/95 [&_select]:py-0 [&_select]:text-sm [&_select]:shadow-sm dark:[&_select]:border-white/10 dark:[&_select]:bg-dark-2/92",
|
||||
)}
|
||||
value={selectedLanguage}
|
||||
onChange={onLanguageChange as LanguageSelectOnChange}
|
||||
/>
|
||||
{isTranslating ? (
|
||||
<p className="text-xs font-medium text-primary dark:text-primary/90">
|
||||
Translating…
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/** Matches the tender title `<h1>` in the header — keep in sync for layout probe */
|
||||
/** Matches the tender title `<h1>` in the header */
|
||||
export const TENDER_HEADER_TITLE_CLASS =
|
||||
"text-balance text-xl font-bold leading-tight text-dark dark:text-white sm:text-2xl lg:text-[1.65rem]";
|
||||
|
||||
/** Reserved width for the actions column when deciding side‑by‑side vs stacked (lg+) */
|
||||
export const TENDER_HEADER_ACTIONS_RESERVE_PX = 360;
|
||||
|
||||
export const TENDER_SECTION_IDS = {
|
||||
procurement: "tender-section-procurement",
|
||||
lots: "tender-section-lots",
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { useLayoutEffect, useState, type RefObject } from "react";
|
||||
import { TENDER_HEADER_ACTIONS_RESERVE_PX } from "../constants";
|
||||
|
||||
export function useStackTenderHeaderActions(
|
||||
title: string,
|
||||
containerRef: RefObject<HTMLDivElement | null>,
|
||||
probeSizerRef: RefObject<HTMLDivElement | null>,
|
||||
) {
|
||||
const [stackBelow, setStackBelow] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const probeSizer = probeSizerRef.current;
|
||||
if (!container || !probeSizer) return;
|
||||
|
||||
const run = () => {
|
||||
if (!window.matchMedia("(min-width: 1024px)").matches) {
|
||||
setStackBelow(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = window.getComputedStyle(container);
|
||||
const gapRaw = parseFloat(styles.columnGap || styles.gap);
|
||||
const gap = Number.isFinite(gapRaw) ? gapRaw : 32;
|
||||
const containerW = container.getBoundingClientRect().width;
|
||||
const titleColumnW = Math.max(
|
||||
160,
|
||||
containerW - TENDER_HEADER_ACTIONS_RESERVE_PX - gap,
|
||||
);
|
||||
probeSizer.style.width = `${titleColumnW}px`;
|
||||
|
||||
const probeTitle = probeSizer.querySelector("h1");
|
||||
if (!probeTitle) return;
|
||||
|
||||
const titleStyles = window.getComputedStyle(probeTitle);
|
||||
const lineHeightRaw = parseFloat(titleStyles.lineHeight);
|
||||
const fontSize = parseFloat(titleStyles.fontSize);
|
||||
const lineHeight =
|
||||
Number.isFinite(lineHeightRaw) && lineHeightRaw > 0
|
||||
? lineHeightRaw
|
||||
: fontSize * 1.25;
|
||||
const lines = probeTitle.scrollHeight / lineHeight;
|
||||
const next = lines > 1.35;
|
||||
setStackBelow((p) => (p === next ? p : next));
|
||||
};
|
||||
|
||||
run();
|
||||
const ro = new ResizeObserver(run);
|
||||
ro.observe(container);
|
||||
window.addEventListener("resize", run);
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener("resize", run);
|
||||
};
|
||||
}, [title, containerRef, probeSizerRef]);
|
||||
|
||||
return stackBelow;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const TenderDetails = ({ params }: IProps) => {
|
||||
const selectedLanguage = searchParams.get("lang") || "en";
|
||||
const { mutate: translateTender, isPending: isTranslating } =
|
||||
useTranslateTenderMutation();
|
||||
const { data, isLoading, isError, refetch } = useTenderDetailQuery(details, {
|
||||
const { data, isLoading, isError } = useTenderDetailQuery(details, {
|
||||
lang: selectedLanguage,
|
||||
});
|
||||
|
||||
@@ -65,29 +65,21 @@ const TenderDetails = ({ params }: IProps) => {
|
||||
const activeSectionId = useTenderSectionScrollSpy(sectionIds);
|
||||
const handleLanguageChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const nextParams = new URLSearchParams(searchParams.toString());
|
||||
const nextLanguage = event.target.value;
|
||||
if (nextLanguage) {
|
||||
nextParams.set("lang", nextLanguage);
|
||||
} else {
|
||||
nextParams.delete("lang");
|
||||
}
|
||||
if (!nextLanguage) return;
|
||||
|
||||
const prevLanguage = searchParams.get("lang") || "en";
|
||||
if (nextLanguage === prevLanguage) return;
|
||||
|
||||
const nextParams = new URLSearchParams(searchParams.toString());
|
||||
nextParams.set("lang", nextLanguage);
|
||||
const queryString = nextParams.toString();
|
||||
router.replace(queryString ? `${pathname}?${queryString}` : pathname);
|
||||
},
|
||||
[pathname, router, searchParams],
|
||||
);
|
||||
|
||||
const handleTranslate = useCallback(() => {
|
||||
translateTender(
|
||||
{ id: details, language: selectedLanguage },
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await refetch();
|
||||
},
|
||||
},
|
||||
);
|
||||
}, [details, refetch, selectedLanguage, translateTender]);
|
||||
translateTender({ id: details, language: nextLanguage });
|
||||
},
|
||||
[details, pathname, router, searchParams, translateTender],
|
||||
);
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
if (isError) {
|
||||
@@ -106,7 +98,6 @@ const TenderDetails = ({ params }: IProps) => {
|
||||
title={tender.title}
|
||||
selectedLanguage={selectedLanguage}
|
||||
onLanguageChange={handleLanguageChange}
|
||||
onTranslate={handleTranslate}
|
||||
isTranslating={isTranslating}
|
||||
status={tender.status}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user