Files
tm-landing/app/marketing/[id]/page.tsx
T
AmirReza Jamali 3e5e135b47 feat(security): Add XSS protection with DOMPurify sanitization
- Add sanitize.ts utility module with sanitizeText(), sanitizeUrl(), and sanitizeHtml() functions
- Implement server-side HTML entity escaping and client-side DOMPurify sanitization
- Add comprehensive test suite for sanitization functions in lib/__tests__/sanitize.test.ts
- Secure marketing pages by sanitizing all CMS data (hero, features, challenges, advantages, footer)
- Secure inquiries form by sanitizing field labels, placeholders, and button text
- Add SECURITY.md documentation outlining XSS protection measures and best practices
- Add USAGE_EXAMPLES.md with detailed examples of sanitization function usage
- Add XSS_PROTECTION_SUMMARY.md with implementation overview
- Update package.json to include dompurify (^3.3.0) dependency
- Protects against common XSS attack vectors including script injection, event handlers, and malicious URLs
2025-11-30 11:14:42 +03:30

315 lines
11 KiB
TypeScript

import ApexChart from "@/app/_components/ApexChart";
import FooterForm from "@/app/_components/FooterForm";
import ImageWithFallback from "@/app/_components/ImageWithFallback";
import { cmsService } from "@/hooks/useCmsQueries";
import { sanitizeText, sanitizeUrl } from "@/lib/sanitize";
import { CmsResponse } from "@/types/TCms";
import { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
export const metadata: Metadata = {
title: "Opp lens - Marketing",
description:
"Automated tender management platform for SMEs. Find tailored tenders, automate document handling, and get real-time alerts. Win more opportunities with less effort.",
keywords:
"tender management, AI tender search, SME tenders, public procurement, automated bidding, tender alerts",
authors: [{ name: "PBL Partners AB" }],
openGraph: {
title: "Opp lens - AI-Powered Tender Management",
description: "Win more tenders automatically with our AI-powered platform",
type: "website",
locale: "en_US",
siteName: "Opp lens",
},
icons: {
icon: "/fav-icon.svg",
},
};
interface IProps {
params: { id: string };
}
export default async function Home({ params }: IProps) {
const { id } = params;
let cmsData: CmsResponse | null = null;
try {
cmsData = await cmsService.getCms(id);
} catch (error) {
console.error("Error fetching CMS data:", error);
}
console.log(cmsData);
const centerFrames = cmsData?.data.features.cards ?? [];
return (
<>
<header className="flex justify-center pt-8 lg:justify-between items-center pb-4 container px-4 py-4 m-auto">
<Link
href="/"
aria-label="Opp lens home">
<Image
src="/main-logo.svg"
alt="Opp lens logo"
width={180}
height={30}
priority
/>
</Link>
</header>
<main className="container px-4 py-4 m-auto flex-grow">
<section className="block lg:flex justify-around items-center">
<div className="flex gap-4 w-full lg:w-1/2 font-bold text-4xl text-center flex-col bg-gray-400/10 p-10 rounded-[40px] h-fit mb-16 backdrop-blur-2xl">
<div className="flex flex-col gap-8">
<h2>
<span>$</span>
<span>{cmsData?.data.chart.missed_amount ?? 1672}</span>
</h2>
<h2 className="capitalize text-2xl font-bold lg:text-3xl">
total tender amount you missed today
</h2>
</div>
<ApexChart cmsData={cmsData?.data} />
</div>
<div className="m-auto w-xs lg:m-0 lg:w-auto -z-20 relative flex justify-center h-[650px] lg:h-[750px]">
<Image
src={"/dynamic-island.svg"}
alt="dynamic island"
width={75}
height={22}
className="absolute top-4 left-1/2 -translate-x-1/2"
/>
<Image
className="-z-10 border-8 border-black rounded-4xl overflow-hidden h-full w-full"
src="/mobile.gif"
width={300}
height={920}
loading="lazy"
alt="phone"
/>
</div>
</section>
<section className="text-left flex flex-col gap-6 bg-gradient-to-b from-[#011132] to-[#012B80] text-white rounded-[56px] relative p-6 lg:p-14 -mt-16 z-20 lg:ml-28 ">
<hgroup>
<h1 className="capitalize font-extrabold text-4xl mb-6 md:text-5xl">
{sanitizeText(cmsData?.data.hero.title)}
</h1>
<p className="text-[16px] text-[#dadada]">
{sanitizeText(cmsData?.data.hero.description)}
</p>
</hgroup>
<div className="flex justify-end">
<Link
className="w-full lg:w-fit capitalize bg-(--primary) rounded-4xl py-4 px-6"
href={sanitizeUrl(cmsData?.data.hero.button_link) || "#"}>
{sanitizeText(cmsData?.data.hero.button_text)}
</Link>
</div>
</section>
<article className="mt-40">
<hgroup>
<h4 className="capitalize font-bold text-2xl lg:text-4xl">
{sanitizeText(cmsData?.data.features.title)}
</h4>
<small className="text-gray-600 mt-3">
{sanitizeText(cmsData?.data.features.description)}
</small>
</hgroup>
</article>
<section className="flex flex-col gap-4 lg:flex-row">
{centerFrames.map((frame, index) => (
<article
key={frame.description}
className="bg-[#F7FAFF] border border-(--primary)/30 rounded-3xl my-4 py-10 px-6 flex flex-col gap-5 transition duration-300 transform scale-90 hover:bg-[#0164FF30] hover:scale-110">
<ImageWithFallback
src={sanitizeUrl(frame.icon) || "/magnifier.svg"}
alt={sanitizeText(frame.title)}
width={36}
height={36}
unoptimized
/>
<h3 className="text-2xl font-bold">
{sanitizeText(frame.title)}
</h3>
<p className="text-gray-600">{sanitizeText(frame.description)}</p>
</article>
))}
</section>
<section className="mt-36 bg-[#F7FAFF] px-4 py-6 rounded-4xl flex flex-col gap-9 lg:p-16">
<div className="flex flex-col gap-4">
<h2 className=" font-bold text-4xl">
{sanitizeText(cmsData?.data.challenges.title)}
</h2>
<p className="font-normal text-sm">
{sanitizeText(cmsData?.data.challenges.description)}
</p>
</div>
<ul className="flex flex-col gap-4 lg:flex-row lg:justify-between">
{cmsData?.data.challenges.cards.map((card) => (
<li
className="flex flex-col gap-4 lg:w-80"
key={card.title}>
<div className="p-4">
<ImageWithFallback
src={sanitizeUrl(card.icon) || "/clock.svg"}
alt={sanitizeText(card.title)}
width={36}
height={36}
unoptimized
/>
</div>
<h2 className="font-bold">{sanitizeText(card.title)}</h2>
<p>{sanitizeText(card.description)}</p>
</li>
))}
</ul>
</section>
<section className="flex flex-col mt-36 gap-10 px-2 lg:w-3/4 m-auto">
<div>
<h2 className="font-bold text-2xl lg:text-4xl">
{sanitizeText(cmsData?.data.advantages.title)}
</h2>
</div>
<section className="flex flex-col gap-10 lg:flex-row">
{cmsData?.data.advantages.cards.map((card) => (
<div
className="flex flex-col gap-7 bg-[#FFF7EB] p-10 rounded-3xl lg:pb-20"
key={card.title}>
<ImageWithFallback
src={sanitizeUrl(card.icon) || "/people.svg"}
width={48}
height={48}
alt={sanitizeText(card.title)}
unoptimized
/>
<div className="flex flex-col gap-4">
<h2 className="font-bold text-xl">
{sanitizeText(card.title)}
</h2>
<p>{sanitizeText(card.description)}</p>
</div>
</div>
))}
</section>
</section>
</main>
<footer className="relative h-full bg-[url('/footer-bg.svg')] bg-cover bg-no-repeat mt-96">
<div
className="absolute w-full -top-[355px] lg:-top-72 h-[600px] z-[-10] bg-[url('/bottom-bg.svg')] bg-contain bg-no-repeat bg-right"
aria-hidden="true"
/>
<FooterForm contactData={cmsData?.data.contact} />
<div className="relative z-10 container px-4 py-4 m-auto flex flex-col justify-center items-center mt-80 ">
<div className="flex justify-between flex-col lg:flex-row gap-14 w-full lg:w-1/2">
<div className="flex flex-col gap-10">
<Image
src={"/footer-logo.svg"}
alt="Opp lens logo"
width={180}
height={30}
priority
/>
<div>
<p className="text-white my-2">
{sanitizeText(cmsData?.data.footer.tagline)}
</p>
<small className="text-white my-2">
{sanitizeText(cmsData?.data.footer.copyright)}
</small>
</div>
</div>
<address className="not-italic">
<ul>
<li className="flex gap-3 my-3 text-white">
<Image
src={"/Phone.svg"}
width={24}
height={24}
alt=""
aria-hidden="true"
/>
<a href={`tel:${sanitizeText(cmsData?.data.footer.phone)}`}>
{sanitizeText(cmsData?.data.footer.phone)}
</a>
</li>
<li className="flex gap-3 my-3 text-white">
<Image
src={"/Message.svg"}
width={24}
height={24}
alt=""
aria-hidden="true"
/>
<a
href={`mailto:${sanitizeText(cmsData?.data.footer.email)}`}>
{sanitizeText(cmsData?.data.footer.email)}
</a>
</li>
<li className="flex gap-3 my-3 text-white">
<Image
src={"/Location.svg"}
width={24}
height={24}
alt=""
aria-hidden="true"
/>
<p>{sanitizeText(cmsData?.data.footer.location)}</p>
</li>
</ul>
</address>
</div>
</div>
</footer>
</>
);
}
function ContactInfo({
icon,
contact,
href,
ariaLabel,
contactIconSrc,
}: {
icon: string;
contact: string;
href?: string;
ariaLabel: string;
contactIconSrc?: string;
}) {
return (
<li className="flex gap-3 my-3 text-white">
<Image
src={icon}
width={24}
height={24}
alt=""
aria-hidden="true"
/>
{href ? (
<a
href={href}
className="cursor-pointer min-w-fit"
aria-label={ariaLabel}>
{contact}
</a>
) : (
<p className="flex gap-3">
{!!contactIconSrc && (
<Image
src={contactIconSrc}
width={20}
height={20}
alt="flag"
/>
)}
{contact}
</p>
)}
</li>
);
}