feat: Refactor contact form and improve 404 page

This commit refactors the contact form for better reusability and moves it from the global footer to a dedicated section on the homepage. Additionally, it enhances the user experience on the 404 page.

Key changes:
- The `ContactUsForm` component is now configurable with props for grid layout (`columnsPerRow`) and button text, making it more versatile.
- The contact form has been removed from the shared footer in the main layout and is now explicitly included only on the homepage.
- The 404 "Not Found" page has been updated to include a direct link to the homepage for easier navigation.
- Minor style adjustments were made to the 404 page layout and input field padding for improved visual consistency.
This commit is contained in:
AmirReza Jamali
2025-10-25 16:11:23 +03:30
parent f57373b476
commit 71094f6c86
14 changed files with 620 additions and 21 deletions
+49
View File
@@ -0,0 +1,49 @@
import ContactUsForm from "@/app/_contact-us/form";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Opp lens - Contact Us",
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",
},
};
const ContactUsPage = () => {
return (
<>
<div className="flex flex-col gap-6 items-center">
<p className="bg-(--primary)/20 w-fit px-2.5 py-1.5 text-(--primary) rounded-full">
Contact Us
</p>
<h1 className="font-extrabold text-5xl">Let's Get In Touch</h1>
<p className="text-[16px] font-normal">
Or just reach out manually to
<a
href="mailto:info@opplens.com"
className="text-(--primary) mx-1">
info@opplens.com
</a>
</p>
</div>
<div className="flex flex-col items-center mt-16 px-8 ">
<section className="border border-gray-300 rounded-4xl w-full md:max-w-2/3 flex flex-col items-center bg-white">
<ContactUsForm columnsPerRow={1} />
</section>
</div>
</>
);
};
export default ContactUsPage;
+172
View File
@@ -0,0 +1,172 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Image from "next/image";
import Link from "next/link";
import { ToastContainer, Zoom } from "react-toastify";
import NavigationBar from "../_components/NavigationBar";
import "../globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
icons: {
icon: "/fav-icon.svg",
},
};
function ContactInfo({
icon,
contact,
href,
ariaLabel,
contactIconSrc,
}: {
icon: string;
contact: string;
href?: string;
ariaLabel: string;
contactIconSrc?: string;
}) {
return (
<div 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>
)}
</div>
);
}
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
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",
contactIconSrc: "/sweden.svg",
},
];
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col bg-[url('/header-bg.svg')] bg-top-right bg-no-repeat bg-contain md:bg-cover`}>
<header className="flex justify-center pt-8 items-center pb-4 container px-4 py-4 m-auto md:px-28 md:justify-between flex-wrap gap-5">
<section>
<Link
href="/"
aria-label="Opp lens home">
<Image
src="/main-logo.svg"
alt="Opp lens logo"
width={180}
height={30}
priority
/>
</Link>
</section>
<NavigationBar />
</header>
<ToastContainer
position="top-right"
autoClose={2500}
theme={"colored"}
transition={Zoom}
hideProgressBar
newestOnTop
closeOnClick
pauseOnFocusLoss
draggable
pauseOnHover
/>
<main className="container px-4 py-4 m-auto flex-grow">{children}</main>
<footer className="relative h-full bg-[url('/footer-bg.svg')] bg-cover bg-no-repeat mt-40">
<div className="relative z-10 container px-4 py-4 m-auto flex flex-col justify-center items-center mt-24 ">
<div className="flex justify-between flex-col md:flex-row gap-14 w-full md: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">
Democratizing Tender Access for SMEs.
</p>
<p className="text-white my-2">
© 2025 Opplens.com by PBL Partners AB. All Rights Reserved.
</p>
</div>
</div>
<div>
{contactInfo.map((contact, index) => (
<ContactInfo
key={index}
icon={contact.icon}
contact={contact.contact}
href={contact.href}
ariaLabel={contact.ariaLabel}
contactIconSrc={contact.contactIconSrc}
/>
))}
</div>
</div>
</div>
</footer>
</body>
</html>
);
}
+100
View File
@@ -0,0 +1,100 @@
import { Metadata } from "next";
import Image from "next/image";
export const metadata: Metadata = {
title: "Opp lens - AI-Powered Tender Management for SMEs | Win More Tenders",
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",
},
};
export default function Home() {
const advItems = [
{
title: "user",
amount: 200,
description:
"lacking the time, dedicated staff, and expertise to constantly monitor the market and prepare high-quality bids.",
},
{
title: "contract",
amount: 520,
description:
"Navigating complex requirements and processes designed for large corporations with dedicated tender departments.",
},
{
title: "tenders",
amount: 1500,
description:
"Did you know 68% of SMEs in Sweden rarely or never win public tenders? It's time to change that.",
},
];
return (
<>
<section className="md:flex md:flex-row justify-around items-center">
<div className=" md:ml-28 w-full md:w-1/2 flex flex-col gap-10">
<h1 className="font-black text-8xl text-[#013280]">AI-Powered</h1>
<h2 className="font-black text-(--primary) text-5xl leading-14">
Tender
<br />
Management
</h2>
<div className="flex flex-col gap-6 ">
<h3 className="font-black text-3xl text-[#222]">
Win more tenders, automatically!
</h3>
<p className="font-normal text-[#777]">
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>
<div className="m-auto mt-5 w-xs -z-20 relative flex justify-center h-[650px] md:h-[750px] md:w-auto">
<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 md:p-14 -mt-16 z-20 md:ml-28">
<hgroup className="md:flex gap-5 text-center">
{advItems.map((item) => (
<div
key={item.title}
className="flex flex-col gap-4">
<h3 className="font-black text-[90px]">{item.amount}</h3>
<h4 className="font-semibold text-3xl capitalize text-[#99dde5]">
{item.title}
</h4>
<p className="font-normal text-[16px]">{item.description}</p>
</div>
))}
</hgroup>
</section>
</>
);
}
+3 -8
View File
@@ -127,14 +127,9 @@ export default async function RootLayout({
/> />
<main className="container px-4 py-4 m-auto flex-grow">{children}</main> <main className="container px-4 py-4 m-auto flex-grow">{children}</main>
<footer className="relative h-full bg-[url('/footer-bg.svg')] bg-cover bg-no-repeat mt-96"> <footer className="relative h-full bg-[url('/footer-bg.svg')] bg-cover bg-no-repeat mt-20">
<div <div className="relative z-10 container px-4 py-4 m-auto flex flex-col justify-center items-center">
className="absolute w-full -top-[355px] md:-top-72 h-[600px] z-[-10] " <div className="flex justify-between flex-col md:flex-row gap-14 w-full md:w-1/2 mt-20">
aria-hidden="true"
/>
<div className="relative z-10 container px-4 py-4 m-auto flex flex-col justify-center items-center md:mt-36 ">
<div className="flex justify-between flex-col md:flex-row gap-14 w-full md:w-1/2 mt-40">
<div className="flex flex-col gap-10"> <div className="flex flex-col gap-10">
<Image <Image
src={"/footer-logo.svg"} src={"/footer-logo.svg"}
+11 -2
View File
@@ -1,4 +1,5 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
interface IProps {} interface IProps {}
@@ -9,7 +10,7 @@ const NotFoundPage = ({}: IProps) => {
<Image <Image
src={"/404.svg"} src={"/404.svg"}
alt="404" alt="404"
width={1000} width={700}
height={1000} height={1000}
/> />
</section> </section>
@@ -26,7 +27,15 @@ const NotFoundPage = ({}: IProps) => {
<section className="flex items-center flex-col justify-center gap-10 mt-10"> <section className="flex items-center flex-col justify-center gap-10 mt-10">
<h1 className="font-bold text-2xl">Page not found</h1> <h1 className="font-bold text-2xl">Page not found</h1>
<p className="font-semibold text-[16px] text-[#77707f]"> <p className="font-semibold text-[16px] text-[#77707f]">
Sorry, the page you're looking for does not exist or has been removed Sorry, the page you're looking for does not exist or has been removed.{" "}
<br />
You can navigate to Home using
<Link
href="/"
className="text-(--primary) mx-1">
This
</Link>
link.
</p> </p>
</section> </section>
</div> </div>
-4
View File
@@ -1,10 +1,6 @@
"use client";
import { usePathname } from "next/navigation";
import ContactUs from "../_contact-us/page"; import ContactUs from "../_contact-us/page";
const FooterForm = () => { const FooterForm = () => {
const pathName = usePathname();
if (pathName !== "/") return null;
return ( return (
<div className="absolute w-full -top-[420px] md:-top-72 z-10"> <div className="absolute w-full -top-[420px] md:-top-72 z-10">
<ContactUs /> <ContactUs />
+1 -1
View File
@@ -38,7 +38,7 @@ function InputGroup({
: isFocused : isFocused
? "border-(--primary)" ? "border-(--primary)"
: "border-gray-200" : "border-gray-200"
} p-2 rounded-lg w-full outline-none`} } p-2 py-3 rounded-lg w-full outline-none`}
id={id} id={id}
{...rest} {...rest}
onChange={handleChange} onChange={handleChange}
+36
View File
@@ -0,0 +1,36 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
const NavigationBar = () => {
const pathname = usePathname();
return (
<nav>
<ul className="flex flex-row-reverse items-center gap-12">
<li className="bg-(--primary) text-white py-5 px-12 rounded-full">
Login
</li>
<li>
<Link
href="/contact-us"
className={`py-5 rounded-full ${
pathname === "/contact-us" && "text-(--primary) font-semibold"
}`}>
Contact us
</Link>
</li>
<li>
<Link
href="/"
className={`py-5 rounded-full ${
pathname === "/" && "text-(--primary) font-semibold"
}`}>
Home
</Link>
</li>
</ul>
</nav>
);
};
export default NavigationBar;
+30 -4
View File
@@ -3,18 +3,29 @@ import api from "@/service/api";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import InputGroup from "../_components/InputGroup"; import InputGroup from "../_components/InputGroup";
type TContactUsForm = { type TContactUsForm = {
full_name: string; full_name: string;
company_name: string; company_name: string;
work_email: string; work_email: string;
phone_number: string; phone_number: string;
}; };
const ContactUsForm = () => {
type ContactUsFormProps = {
columnsPerRow?: 1 | 2 | 3 | 4;
buttonText?: string;
};
const ContactUsForm = ({
columnsPerRow = 2,
buttonText = "Submit",
}: ContactUsFormProps) => {
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
} = useForm<TContactUsForm>(); } = useForm<TContactUsForm>();
const onSubmit = async (data: TContactUsForm) => { const onSubmit = async (data: TContactUsForm) => {
try { try {
const response = (await api.post("inquiries", { ...data })).data; const response = (await api.post("inquiries", { ...data })).data;
@@ -25,9 +36,24 @@ const ContactUsForm = () => {
throw error; throw error;
} }
}; };
const gridColsClass = {
1: "md:grid-cols-1",
2: "md:grid-cols-2",
3: "md:grid-cols-3",
4: "md:grid-cols-4",
}[columnsPerRow];
const colSpanClass = {
1: "md:col-span-1",
2: "md:col-span-2",
3: "md:col-span-3",
4: "md:col-span-4",
}[columnsPerRow];
return ( return (
<form <form
className="w-full px-4 md:px-16 py-10 grid md:grid-cols-2 gap-4 " className={`w-full px-4 md:px-16 py-10 grid ${gridColsClass} gap-4`}
onSubmit={handleSubmit(onSubmit)}> onSubmit={handleSubmit(onSubmit)}>
<InputGroup <InputGroup
id="full_name" id="full_name"
@@ -60,9 +86,9 @@ const ContactUsForm = () => {
}, },
}} }}
/> />
<div className="md:col-span-2 flex justify-end"> <div className={`${colSpanClass} flex justify-end`}>
<button className="bg-(--primary) w-fit text-white rounded-full py-4 px-6"> <button className="bg-(--primary) w-fit text-white rounded-full py-4 px-6">
Register Now {buttonText}
</button> </button>
</div> </div>
</form> </form>
@@ -1,8 +1,9 @@
import ApexChart from "@/app/_components/ApexChart";
import { Metadata } from "next"; import { Metadata } from "next";
import Image from "next/image"; import Image from "next/image";
import ApexChart from "../_components/ApexChart";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Opp lens - AI-Powered Tender Management for SMEs | Win More Tenders", title: "Opp lens - Marketing",
description: description:
"Automated tender management platform for SMEs. Find tailored tenders, automate document handling, and get real-time alerts. Win more opportunities with less effort.", "Automated tender management platform for SMEs. Find tailored tenders, automate document handling, and get real-time alerts. Win more opportunities with less effort.",
keywords: keywords:
+215
View File
@@ -0,0 +1,215 @@
import { Metadata } from "next";
import Image from "next/image";
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",
},
};
export default function Home() {
const centerFrames = [
{
src: "/magnifier.svg",
title: "1. Discover with AI",
alt: "Magnifier",
description:
"Our intelligent platform scours thousands of sources to find the perfect, most relevant tender opportunities for your specific business and expertise",
},
{
src: "/clipboard.svg",
title: "2. Prepare with Experts",
alt: "Clipboard",
description:
"Gain access to expert support and powerful tools to craft compelling, competitive, and high-quality bids that stand out from the competition.",
},
{
src: "/thumbs-up.svg",
title: "3. Win with a Partner",
alt: "thumbs-up",
description:
"We operate on a partnership model. We only succeed when you win. This aligns our goals and makes us a true partner in your growth.",
},
];
return (
<>
<section className="flex flex-col justify-center md:justify-between md:flex-row items-center">
<section className="text-left md:w-1/2 flex flex-col gap-6 bg-gradient-to-b rounded-[56px] relative p-6 md:p-14 z-20 md:ml-28">
<hgroup>
<h1 className="capitalize font-extrabold text-5xl mb-6">
democratizing the world of tenders
</h1>
<p className="text-[16px]">
Opplens is your dedicated AI-powered partner, leveling the playing
field so you can win the public and private contracts you deserve.
<br />
Compete on your true strengths.
</p>
</hgroup>
<div className="flex justify-start text-white">
<button className="w-2/3 md:w-fit capitalize bg-(--primary) rounded-4xl py-4 px-6">
Get early access
</button>
</div>
</section>
<div className="m-auto w-xs md:m-0 md:w-auto -z-20 relative flex justify-center h-[650px] md:h-[750px] md:mr-28">
<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>
<article className="mt-10">
<hgroup>
<h4 className="capitalize font-bold text-2xl md:text-4xl">
your in-house tender department, powered by
<span className="uppercase">ai</span>
</h4>
<small className="text-gray-600 mt-3">
Opplens is the end-to-end solution that gives you the power to
compete and win.
</small>
</hgroup>
</article>
<section className="flex flex-col gap-4 md: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">
<Image
src={frame.src}
alt={frame.alt}
width={36}
height={36}
/>
<h3 className="text-2xl font-bold">{frame.title}</h3>
<p className="text-gray-600">{frame.description}</p>
</article>
))}
</section>
<section className="mt-36 bg-[#F7FAFF] px-4 py-6 rounded-4xl flex flex-col gap-9 md:p-16">
<div className="flex flex-col gap-4">
<h2 className=" font-bold text-4xl">
Tired of Complex Tenders & Lost Opportunities?
</h2>
<p className="font-normal text-sm">
The procurement market is tough for SMEs. We get it. You're facing
an uphill battle.
</p>
</div>
<ul className="flex flex-col gap-4 md:flex-row md:justify-between">
<li className="flex flex-col gap-4 md:w-80">
<div className="p-4">
<Image
src={"/clock.svg"}
alt="clock icon"
width={36}
height={36}
/>
</div>
<h2 className="font-bold">Resource Drain</h2>
<p>
Lacking the time, dedicated staff, and expertise to constantly
monitor the market and prepare high-quality bids.
</p>
</li>
<li className="flex flex-col gap-4 md:w-80">
<div className="p-4">
<Image
src={"/clipboard.svg"}
alt="clipboard icon"
width={36}
height={36}
/>
</div>
<h2 className="font-bold">Overwhelming Complexity</h2>
<p>
Navigating complex requirements and processes designed for large
corporations with dedicated tender departments.
</p>
</li>
<li className="flex flex-col gap-4 md:w-80">
<div className="p-4">
<Image
src={"/arrow-up.svg"}
alt="arrow-up icon"
width={36}
height={36}
/>
</div>
<h2 className="font-bold">Low Win Rates</h2>
<p>
Did you know 68% of SMEs in Sweden rarely or never win public
tenders? It's time to change that.
</p>
</li>
</ul>
</section>
<section className="flex flex-col mt-36 gap-10 px-2 md:w-3/4 m-auto">
<div>
<h2 className="font-bold text-2xl md:text-4xl">
The Opplens Advantage
</h2>
</div>
<section className="flex flex-col gap-10 md:flex-row">
<div className="flex flex-col gap-7 bg-[#FFF7EB] p-10 rounded-3xl md:pb-20">
<Image
src={"people.svg"}
width={48}
height={48}
alt="people icon"
/>
<div className="flex flex-col gap-4">
<h2 className="font-bold text-xl">A True Partnership Model</h2>
<p>
Lower your risk with our performance-based options. Our success
is directly tied to yours, making us a dedicated partner
invested in your growth, not just a software provider.
</p>
</div>
</div>
<div className="flex flex-col gap-7 bg-[#E5EFFF] p-10 rounded-3xl md:pb-20">
<Image
src={"shield-tick.svg"}
width={48}
height={48}
alt="shield icon"
/>
<div className="flex flex-col gap-4">
<h2 className="font-bold text-xl">Unmatched Data Security</h2>
<p>
Your business data is critical. Our unique on premise AI engine
ensures your information remains 100% private and secure, unlike
cloud based services. Fully GDPR compliant by design.
</p>
</div>
</div>
</section>
</section>
</>
);
}
View File
BIN
View File
Binary file not shown.