feat: redesign homepage and early access page

This commit introduces a comprehensive redesign of the homepage and refactors the "Contact Us" page into a dedicated "Get Early Access" page.

Key changes include:
- **Homepage:** A complete overhaul with new sections, including a hero, feature highlights, and a problem/solution section to better communicate the product's value proposition.
- **Early Access Page:** The former "Contact Us" page is now repurposed for early access sign-ups, with updated copy and a more focused call-to-action.
- **Component Refactoring:**
    - The `InputGroup` component has been extracted from the form into a reusable component (`app/_components/InputGroup.tsx`) for better modularity.
    - The `CenterFrame` component has been updated to support images and new props (`src`, `title`, `alt`) to fit the new homepage design.
- **Layout Improvements:** The early access form now uses a two-column grid layout on larger screens for improved user experience.
This commit is contained in:
AmirReza Jamali
2025-10-18 20:44:30 +03:30
parent 31d03b57d6
commit aad539e1a6
25 changed files with 775 additions and 263 deletions
+106
View File
@@ -0,0 +1,106 @@
"use client";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { ApexOptions } from "apexcharts";
const ReactApexChart = dynamic(() => import("react-apexcharts"), { ssr: false });
interface ChartState {
series: {
name: string;
data: number[];
}[];
options: ApexOptions;
}
const ApexChart = () => {
const [state, setState] = useState<ChartState | null>(null);
useEffect(() => {
const initialState: ChartState = {
series: [
{
name: "Sales",
data: [
182, 192, 230, 198, 243, 234, 244, 210, 274, 220, 225, 289, 265,
304,
],
},
],
options: {
chart: {
toolbar: { show: false },
height: 350,
type: "line",
},
forecastDataPoints: {
count: 0,
},
stroke: {
width: 6,
curve: "smooth",
},
fill: {
type: "gradient",
gradient: {
shade: "dark",
type: "horizontal",
shadeIntensity: 0.5,
gradientToColors: ["#99DDE5"],
inverseColors: false,
opacityFrom: 1,
opacityTo: 1,
stops: [0, 100],
},
},
colors: ["#0164FF"],
xaxis: {
type: "datetime",
categories: [
"1/11/2024",
"2/11/2024",
"3/11/2024",
"5/11/2024",
"6/11/2024",
"7/11/2024",
"8/11/2024",
"9/11/2024",
"10/11/2024",
"11/11/2024",
"12/11/2024",
"1/11/2025",
"2/11/2025",
"3/11/2025",
],
tickAmount: 0,
labels: {
formatter: function (value: any, timestamp: any, opts: any) {
return opts.dateFormatter(new Date(timestamp), "dd MMM");
},
},
},
},
};
setState(initialState);
}, []);
if (!state) {
return <div>Loading...</div>;
}
return (
<div>
<div id="chart">
<ReactApexChart
options={state.options}
series={state.series}
type="line"
height={350}
/>
</div>
<div id="html-dist"></div>
</div>
);
};
export default ApexChart;
+64
View File
@@ -0,0 +1,64 @@
"use client";
import { HTMLInputTypeAttribute, useState } from "react";
import { RegisterOptions, UseFormRegister } from "react-hook-form";
function InputGroup({
id,
label,
register,
type,
validation,
error,
}: {
label: string;
id: string;
register: UseFormRegister<any>;
type: HTMLInputTypeAttribute;
validation?: RegisterOptions;
error?: string;
}) {
const [isFocused, setIsFocused] = useState(false);
const [hasValue, setHasValue] = useState(false);
const { onChange, ...rest } = register(id, validation);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setHasValue(e.target.value !== "");
onChange(e);
};
return (
<div>
<div className="relative w-full my-3">
<input
type={type}
className={`border ${
error
? "border-red-500"
: isFocused
? "border-(--primary)"
: "border-gray-200"
} p-2 rounded-lg w-full outline-none`}
id={id}
{...rest}
onChange={handleChange}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
<label
htmlFor={id}
className={`absolute left-3 transition-all duration-200 pointer-events-none bg-white px-1 ${
isFocused || hasValue
? "-top-2 text-xs text-gray-600"
: "top-1/2 -translate-y-1/2 text-base text-gray-400"
}`}>
{label}
<span className="text-red-400">*</span>
</label>
</div>
{error && <p className="text-red-500 text-sm my-3">{error}</p>}
</div>
);
}
export default InputGroup;
+8 -69
View File
@@ -1,8 +1,8 @@
"use client";
import api from "@/service/api";
import { HTMLInputTypeAttribute } from "react";
import { RegisterOptions, useForm, UseFormRegister } from "react-hook-form";
import { useForm } from "react-hook-form";
import { toast } from "react-toastify";
import InputGroup from "../_components/InputGroup";
type TContactUsForm = {
full_name: string;
company_name: string;
@@ -27,37 +27,29 @@ const ContactUsForm = () => {
};
return (
<form
className="w-full px-4 md:px-60 py-10"
className="w-full px-4 md:px-16 py-10 grid md:grid-cols-2 gap-4 "
onSubmit={handleSubmit(onSubmit)}>
<InputGroup
icon={UserIcon}
id="full_name"
label="Full name"
placeholder="Enter Full name"
register={register}
type="text"
/>
<InputGroup
icon={UserIcon}
id="company_name"
label="Company name"
placeholder="Enter Company Name"
register={register}
type="text"
/>
<InputGroup
icon={UserIcon}
id="work_email"
label="Work email"
placeholder="Enter Work Email"
register={register}
type="email"
/>
<InputGroup
icon={UserIcon}
id="phone_number"
label="Phone number"
placeholder="Enter Phone Number"
register={register}
type="text"
error={errors.phone_number?.message}
@@ -68,66 +60,13 @@ const ContactUsForm = () => {
},
}}
/>
<button className="bg-(--primary) w-full text-white rounded-full py-4">
Submit
</button>
<div className="md:col-span-2 flex justify-end">
<button className="bg-(--primary) w-fit text-white rounded-full py-4 px-6">
Register Now
</button>
</div>
</form>
);
};
export default ContactUsForm;
function InputGroup({
icon,
id,
label,
placeholder,
register,
type,
validation,
error,
}: {
label: string;
id: string;
icon: () => React.JSX.Element;
placeholder: string;
register: UseFormRegister<any>;
type: HTMLInputTypeAttribute;
validation?: RegisterOptions;
error?: string;
}) {
return (
<>
<label htmlFor={id}>{label}</label>
<div className="relative w-full my-3">
<input
type={type}
className={`border ${
error ? "border-red-500" : "border-gray-200"
} p-2 px-10 rounded-lg w-full`}
id={id}
{...register(id, validation)}
placeholder={placeholder}
/>
<span className="absolute left-0 bottom-0 top-0 flex items-center pl-2">
{icon()}
</span>
</div>
{error && <p className="text-red-500 text-sm my-3">{error}</p>}
</>
);
}
function UserIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M16 6H15.25C15.25 7.79493 13.7949 9.25 12 9.25V10V10.75C14.6234 10.75 16.75 8.62335 16.75 6H16ZM12 10V9.25C10.2051 9.25 8.75 7.79493 8.75 6H8H7.25C7.25 8.62335 9.37665 10.75 12 10.75V10ZM8 6H8.75C8.75 4.20507 10.2051 2.75 12 2.75V2V1.25C9.37665 1.25 7.25 3.37665 7.25 6H8ZM12 2V2.75C13.7949 2.75 15.25 4.20507 15.25 6H16H16.75C16.75 3.37665 14.6234 1.25 12 1.25V2ZM9 13V13.75H15V13V12.25H9V13ZM15 21V20.25H9V21V21.75H15V21ZM9 21V20.25C7.20507 20.25 5.75 18.7949 5.75 17H5H4.25C4.25 19.6234 6.37665 21.75 9 21.75V21ZM19 17H18.25C18.25 18.7949 16.7949 20.25 15 20.25V21V21.75C17.6234 21.75 19.75 19.6234 19.75 17H19ZM15 13V13.75C16.7949 13.75 18.25 15.2051 18.25 17H19H19.75C19.75 14.3766 17.6234 12.25 15 12.25V13ZM9 13V12.25C6.37665 12.25 4.25 14.3766 4.25 17H5H5.75C5.75 15.2051 7.20507 13.75 9 13.75V13Z"
fill="#9E9E9E"
/>
</svg>
);
}
+2 -19
View File
@@ -2,25 +2,8 @@ import ContactUsForm from "./form";
const ContactUs = () => {
return (
<div className="flex flex-col items-center mt-20">
<div className="flex flex-col items-center mb-16">
<h6 className="text-lg w-fit font-semibold text-(--primary) bg-(--primary)/20 opacity-80 p-2 rounded-full">
Contact Us
</h6>
<h1 className="font-extrabold my-6 text-4xl">Let's get in touch</h1>
<div className=" text-center block md:flex gap-1">
<p className="font-extralight text-xl">
Or just reach out manually to
</p>
<a
href="mailto:info@opples.com"
className="text-(--primary) font-extralight text-xl">
info@opplens.com
</a>
</div>
</div>
<section className="border border-gray-300 rounded-4xl w-10/12 flex flex-col items-center">
<div className="flex flex-col items-center mt-20 px-4">
<section className="border border-gray-300 rounded-4xl w-full max-w-4xl flex flex-col items-center bg-white">
<ContactUsForm />
</section>
</div>
+62 -92
View File
@@ -3,7 +3,7 @@ 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 HeaderButtons from "./_components/HeaderButtons";
import ContactUs from "./contact-us/page";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -32,7 +32,7 @@ function ContactInfo({
ariaLabel: string;
}) {
return (
<div className="flex gap-3 my-3">
<div className="flex gap-3 my-3 text-white">
<Image
src={icon}
width={24}
@@ -82,101 +82,71 @@ export default async function RootLayout({
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<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>
<div className="hidden absolute -z-20 left-0 md:block">
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen bg-[url('/main-bg.svg')] bg-cover bg-no-repeat`}>
<header className="flex justify-between items-center pb-4 container px-4 py-4 m-auto">
<Link
href="/"
aria-label="Opp lens home">
<Image
src={"/side-shallow.svg"}
alt="Shallow"
width={235}
height={235}
src="/main-logo.svg"
alt="Opp lens logo"
width={180}
height={30}
priority
/>
</Link>
</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-96">
<div className="absolute w-full -top-[420px] md:-top-72">
<ContactUs />
</div>
<ToastContainer
position="top-right"
autoClose={2500}
theme={"colored"}
transition={Zoom}
hideProgressBar
newestOnTop
closeOnClick
pauseOnFocusLoss
draggable
pauseOnHover
/>
{children}
<div className="hidden absolute -z-20 right-0 rotate-y-180 md:block">
<Image
src={"/side-shallow.svg"}
alt="Shallow"
width={235}
height={235}
/>
</div>
<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}
<div className="container px-4 py-4 m-auto flex flex-col justify-center items-center mt-72 ">
<div className="flex justify-between flex-col md:flex-row gap-14 w-full md:w-1/2">
<div>
<Image
src={"/footer-logo.svg"}
alt="Opp lens logo"
width={180}
height={30}
priority
/>
))}
</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>
<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>
{contactInfo.map((contact, index) => (
<ContactInfo
key={index}
icon={contact.icon}
contact={contact.contact}
href={contact.href}
ariaLabel={contact.ariaLabel}
/>
))}
</div>
</div>
</footer>
</div>
</div>
</footer>
</body>
</html>
);
+181 -82
View File
@@ -1,6 +1,6 @@
import { Metadata } from "next";
import Image from "next/image";
import ApexChart from "./_components/ApexChart";
export const metadata: Metadata = {
title: "Opp lens - AI-Powered Tender Management for SMEs | Win More Tenders",
description:
@@ -21,109 +21,208 @@ export const metadata: Metadata = {
};
export default function Home() {
const centerFrameItems = [
const centerFrames = [
{
header: 200,
text: "Users",
description: `Lacking the time, dedicated staff, and
expertise to constantly monitor the market
and prepare high-quality bids.`,
src: "/magnifier.svg",
title: "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",
},
{
header: 520,
text: "Contracts",
description: `Navigating complex requirements and
processes designed for large corporations
with dedicated tender departments.`,
src: "/clipboard.svg",
title: "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.",
},
{
header: 1500,
text: "Tenders",
description: `Did you know 68% of SMEs in Sweden
rarely or never win public tenders? It's time
to change that.`,
src: "/thumbs-up.svg",
title: "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 (
<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>
<>
<section className="block md:flex justify-around items-center">
<div className="flex gap-4 w-full md:w-1/2 font-bold text-4xl text-center flex-col bg-gray-800/5 p-10 rounded-[40px] h-[500px] mb-16">
<h2>
<span>$</span>
<span>12,345,678</span>
</h2>
<h2 className="capitalize">total tender amount you missed today</h2>
<ApexChart />
</div>
<div className="mr-8">
<Image
className="-z-10"
src="/mobile.svg"
width={300}
height={820}
alt="phone"
/>
</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) => (
<div className="text-left flex flex-col gap-6 bg-gradient-to-b from-[#011132] to-[#012B80] text-white rounded-[56px] p-6 md:p-14 -mt-16 z-10 md:ml-28">
<h1 className="capitalize font-extrabold text-5xl ">
democratizing the world of tenders
</h1>
<p className="text-xs">
Opplens is your dedicated AI-powered partner, leveling the playing
field so you can win the public and private contracts you deserve.
Compete on your true strengths.
</p>
<div className="flex justify-end">
<button className="bg-(--primary) rounded-4xl py-4 px-6">
Get early access
</button>
</div>
</div>
<article className="mt-40">
<h4 className="capitalize font-bold 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>
</article>
<section className="flex flex-col gap-4 md:flex-row">
{centerFrames.map((frame, index) => (
<CenterFrame
key={index}
header={item.header}
text={item.text}
description={item.description}
key={frame.description}
src={frame.src}
title={frame.title}
description={frame.description}
alt={frame.alt}
/>
))}
</section>
</main>
<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>
<div className="flex flex-col gap-4 md:flex-row md:justify-between">
<div 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>
</div>
<div 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>
</div>
<div 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>
</div>
</div>
</section>
<section className="flex flex-col mt-36 gap-10 w-3/4 m-auto">
<div>
<h2 className="font-bold text-4xl">The Opplens Advantage</h2>
</div>
<div className="flex flex-col gap-10 md:flex-row">
<div className="flex flex-col gap-7 md:flex-row 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 md:flex-row 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>
</div>
</section>
</>
);
}
function CenterFrame({
header,
text,
src,
title,
description,
alt,
}: {
header: number;
text: string;
src: string;
title: string;
description: string;
alt: string;
}) {
return (
<article className="flex flex-col justify-center items-center gap-4">
<p
className="gradient-text"
aria-label={`${header} ${text}`}>
<strong>{header}</strong>
</p>
<h3 className="text-2xl font-semibold">{text}</h3>
<p className="text-(--gray-primary)">{description}</p>
</article>
<div className="bg-[#F7FAFF] border border-(--primary)/30 rounded-3xl my-4 py-10 px-6">
<Image
src={src}
alt={alt}
width={36}
height={36}
/>
<h3 className="text-2xl font-bold">{title}</h3>
<p className="text-gray-600">{description}</p>
</div>
);
}