feat: Refactor layout and add toast notifications

This commit introduces a major structural refactoring by creating a shared application layout and integrates a toast notification system.

Key changes:
- A new `layout.tsx` file now contains the common Header and Footer components, ensuring a consistent structure across all pages.
- The main `page.tsx` has been simplified to only contain its specific content, with layout elements removed.
- The `react-toastify` library has been added to provide user feedback through toast notifications.
- Custom CSS styles have been applied to the toast notifications for a branded look and feel, including different gradient backgrounds for success, error, warning, and info states.
- The `.gitignore` file was updated to explicitly track the `.env` file, likely for an example configuration.
This commit is contained in:
AmirReza Jamali
2025-10-15 17:12:09 +03:30
parent 36ee28ed84
commit 31d03b57d6
13 changed files with 537 additions and 199 deletions
+57 -192
View File
@@ -1,6 +1,5 @@
import { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
export const metadata: Metadata = {
title: "Opp lens - AI-Powered Tender Management for SMEs | Win More Tenders",
@@ -45,165 +44,65 @@ rarely or never win public tenders? It's time
to change that.`,
},
];
const contactInfo = [
{
icon: "/Message.svg",
contact: "info@opplens.com",
href: "mailto:info@opplens.com",
ariaLabel: "Email us at info@opplens.com",
},
{
icon: "/Phone.svg",
contact: "(+46) - 761581526",
href: "tel:+46761581526",
ariaLabel: "Call us at +46 761581526",
},
{
icon: "/Location.svg",
contact: "Stockholm, Sweden",
ariaLabel: "Our location in Stockholm, Sweden",
},
];
return (
<div className="container px-4 py-4 m-auto">
<header className="flex justify-between items-center border-b md:border-none border-gray-300 pb-4">
<Link
href="/"
aria-label="Opp lens home">
<Image
src="/main-logo.svg"
alt="Opp lens logo"
width={180}
height={30}
priority
/>
</Link>
<div className="flex gap-5 items-center">
<nav
className="hidden md:flex"
aria-label="Main navigation">
<HeaderButtons />
</nav>
<label
htmlFor="language"
className="sr-only">
Select language
</label>
<select
name="language"
id="language"
aria-label="Language selector">
<option value="en">English</option>
</select>
</div>
</header>
<nav
className="block md:hidden"
aria-label="Mobile navigation">
<HeaderButtons />
</nav>
<main className="mt-8 flex flex-col justify-center items-center gap-14">
<section
className="flex"
aria-labelledby="hero-heading">
<div className="flex flex-col">
<h1
className="gradient-text md:!text-left md:!text-8xl md:mb-10"
id="hero-heading">
AI-Powered
</h1>
<Image
className="block md:hidden"
src={"/images/mobile.png"}
alt="Opp lens platform interface on mobile device"
width={520}
height={280}
priority
/>
<div className="w-full flex flex-col gap-6 text-xl">
<h2 className="text-(--primary) font-bold text-5xl md:!mb-10">
Tender Management
</h2>
<h3 className="font-bold">Win more tenders, automatically!</h3>
<p className="text-(--gray-primary)">
Our platform finds tenders tailored to your business, automates
document handling, and sends real-time alerts. Win more
opportunities with less effort and zero manual search.
</p>
</div>
</div>
<Image
className="hidden md:block"
src={"/images/mobile.png"}
alt="Opp lens platform interface on desktop"
width={550}
height={480}
priority
/>
</section>
<section
className="flex flex-col gap-4 text-center md:flex-row"
aria-labelledby="statistics-heading">
<h2
id="statistics-heading"
className="sr-only">
Platform Statistics
</h2>
{centerFrameItems.map((item, index) => (
<CenterFrame
key={index}
header={item.header}
text={item.text}
description={item.description}
/>
))}
</section>
</main>
<footer className="mt-14">
<div className="flex justify-center items-center border-t pt-5 border-gray-300">
<Image
src="/main-logo.svg"
alt="Opp lens logo"
width={180}
height={30}
/>
</div>
<address className="md:flex md:justify-evenly w-full mt-14 px-5 not-italic">
{contactInfo.map((item) => (
<ContactInfo
key={item.contact}
contact={item.contact}
href={item.href}
icon={item.icon}
ariaLabel={item.ariaLabel}
/>
))}
</address>
<div className="flex justify-center items-center">
<p className="text-center text-(--gray-primary) mt-5 leading-10">
Stockholm, Sweden © 2025 Opplens.com by PBL Partners AB. All Rights
Reserved. Democratizing Tender Access for SMEs.
</p>
</div>
</footer>
</div>
);
}
function HeaderButtons() {
return (
<div className="flex justify-between items-center gap-5 mt-4 md:mt-0">
<Link
href={"/contact-us"}
className="text-(--primary)">
Contact us
</Link>
<Link
href={"/login"}
className="bg-(--primary) text-white px-12 py-3 rounded-full">
Login
</Link>
</div>
<main className="mt-8 flex flex-col justify-center items-center gap-14">
<section
className="flex"
aria-labelledby="hero-heading">
<div className="flex flex-col">
<h1
className="gradient-text md:!text-left md:!text-8xl md:mb-10"
id="hero-heading">
AI-Powered
</h1>
<Image
className="block md:hidden"
src={"/images/mobile.png"}
alt="Opp lens platform interface on mobile device"
width={520}
height={280}
priority
/>
<div className="w-full flex flex-col gap-6 text-xl">
<h2 className="text-(--primary) font-bold text-5xl md:!mb-10">
Tender Management
</h2>
<h3 className="font-bold">Win more tenders, automatically!</h3>
<p className="text-(--gray-primary)">
Our platform finds tenders tailored to your business, automates
document handling, and sends real-time alerts. Win more
opportunities with less effort and zero manual search.
</p>
</div>
</div>
<Image
className="hidden md:block"
src={"/images/mobile.png"}
alt="Opp lens platform interface on desktop"
width={550}
height={480}
priority
/>
</section>
<section
className="flex flex-col gap-4 text-center md:flex-row"
aria-labelledby="statistics-heading">
<h2
id="statistics-heading"
className="sr-only">
Platform Statistics
</h2>
{centerFrameItems.map((item, index) => (
<CenterFrame
key={index}
header={item.header}
text={item.text}
description={item.description}
/>
))}
</section>
</main>
);
}
@@ -228,37 +127,3 @@ function CenterFrame({
</article>
);
}
function ContactInfo({
icon,
contact,
href,
ariaLabel,
}: {
icon: string;
contact: string;
href?: string;
ariaLabel: string;
}) {
return (
<div className="flex gap-3 my-3">
<Image
src={icon}
width={24}
height={24}
alt=""
aria-hidden="true"
/>
{href ? (
<a
href={href}
className="cursor-pointer"
aria-label={ariaLabel}>
{contact}
</a>
) : (
<p>{contact}</p>
)}
</div>
);
}