From 31d03b57d656291fdc9f3392dd7742330eff6847 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Wed, 15 Oct 2025 17:12:09 +0330 Subject: [PATCH] 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. --- .env | 1 + .gitignore | 1 + app/_components/HeaderButtons.tsx | 21 +++ app/contact-us/form.tsx | 133 ++++++++++++++++ app/contact-us/page.tsx | 30 ++++ app/globals.css | 38 +++++ app/layout.tsx | 159 ++++++++++++++++++- app/page.tsx | 249 +++++++----------------------- assets/css/style.css | 39 +++++ package-lock.json | 42 ++++- package.json | 4 +- public/side-shallow.svg | 9 ++ service/api.ts | 10 ++ 13 files changed, 537 insertions(+), 199 deletions(-) create mode 100644 .env create mode 100644 app/_components/HeaderButtons.tsx create mode 100644 app/contact-us/form.tsx create mode 100644 app/contact-us/page.tsx create mode 100644 public/side-shallow.svg create mode 100644 service/api.ts diff --git a/.env b/.env new file mode 100644 index 0000000..fe58093 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=https://opplens.com/api/v1/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5ef6a52..5f4f88b 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +!.env \ No newline at end of file diff --git a/app/_components/HeaderButtons.tsx b/app/_components/HeaderButtons.tsx new file mode 100644 index 0000000..197168f --- /dev/null +++ b/app/_components/HeaderButtons.tsx @@ -0,0 +1,21 @@ +"use client"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +export default function HeaderButtons() { + const pathname = usePathname(); + return ( +
+ + {pathname.includes("contact-us") ? "Home" : "Contact us"} + + + Login + +
+ ); +} diff --git a/app/contact-us/form.tsx b/app/contact-us/form.tsx new file mode 100644 index 0000000..4fe70a2 --- /dev/null +++ b/app/contact-us/form.tsx @@ -0,0 +1,133 @@ +"use client"; +import api from "@/service/api"; +import { HTMLInputTypeAttribute } from "react"; +import { RegisterOptions, useForm, UseFormRegister } from "react-hook-form"; +import { toast } from "react-toastify"; +type TContactUsForm = { + full_name: string; + company_name: string; + work_email: string; + phone_number: string; +}; +const ContactUsForm = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + const onSubmit = async (data: TContactUsForm) => { + try { + const response = (await api.post("inquiries", { ...data })).data; + toast.success(response.message); + } catch (error) { + console.log(error); + toast.error("Something went wrong"); + throw error; + } + }; + return ( +
+ + + + + + + ); +}; + +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; + type: HTMLInputTypeAttribute; + validation?: RegisterOptions; + error?: string; +}) { + return ( + <> + +
+ + + {icon()} + +
+ {error &&

{error}

} + + ); +} +function UserIcon() { + return ( + + + + ); +} diff --git a/app/contact-us/page.tsx b/app/contact-us/page.tsx new file mode 100644 index 0000000..5bb4a81 --- /dev/null +++ b/app/contact-us/page.tsx @@ -0,0 +1,30 @@ +import ContactUsForm from "./form"; + +const ContactUs = () => { + return ( +
+
+
+ Contact Us +
+ +

Let's get in touch

+
+

+ Or just reach out manually to +

+ + info@opplens.com + +
+
+
+ +
+
+ ); +}; + +export default ContactUs; diff --git a/app/globals.css b/app/globals.css index 1d41861..f3e3d78 100644 --- a/app/globals.css +++ b/app/globals.css @@ -14,3 +14,41 @@ .gradient-text { @apply text-transparent bg-clip-text bg-[radial-gradient(circle,rgba(1,100,255,1)_0%,rgba(111,255,255,1)_100%)] w-fit text-6xl font-extrabold; } + +.Toastify__toast { + padding: 10px !important; + border-radius: 24px !important; + font-family: inherit !important; + display: flex !important; + align-items: center !important; + + box-shadow: 0px 12px 34px 0px rgba(13, 10, 44, 0.05) !important; +} + +.dark .Toastify__toast { + box-shadow: 0 20px 25px -5px rgb(255 255 255 / 0.07), + 0 8px 10px -6px rgb(255 255 255 / 0.05) !important; +} + +.Toastify__close-button { + display: none; +} +.Toastify__close-button { + display: none; +} + +.Toastify__toast--success { + background: linear-gradient(to bottom right, #82e6ac, #1a8245) !important; +} + +.Toastify__toast--error { + background: linear-gradient(to bottom right, #f89090, #e10e0e) !important; +} + +.Toastify__toast--warning { + background: linear-gradient(to bottom right, #f59e0b, #d97706) !important; +} + +.Toastify__toast--info { + background: linear-gradient(to bottom right, #8099ec, #1c3fb7) !important; +} diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..e12e3c4 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,10 @@ 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 HeaderButtons from "./_components/HeaderButtons"; import "./globals.css"; - const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], @@ -17,17 +20,163 @@ export const metadata: Metadata = { description: "Generated by create next app", }; -export default function RootLayout({ +function ContactInfo({ + icon, + contact, + href, + ariaLabel, +}: { + icon: string; + contact: string; + href?: string; + ariaLabel: string; +}) { + return ( +
+ + {href ? ( + + {contact} + + ) : ( +

{contact}

+ )} +
+ ); +} + +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", + }, + ]; + return ( - {children} + className={`${geistSans.variable} ${geistMono.variable} antialiased`}> +
+
+ + Opp lens logo + +
+ + + +
+
+ +
+ Shallow +
+ + {children} +
+ Shallow +
+
+
+ Opp lens logo +
+
+ {contactInfo.map((item) => ( + + ))} +
+
+

+ Stockholm, Sweden © 2025 Opplens.com by PBL Partners AB. All + Rights Reserved. Democratizing Tender Access for SMEs. +

+
+
+
); diff --git a/app/page.tsx b/app/page.tsx index 4ce78d3..a91813f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( -
-
- - Opp lens logo - -
- - - -
-
- -
-
-
-

- AI-Powered -

- Opp lens platform interface on mobile device -
-

- Tender Management -

-

Win more tenders, automatically!

-

- 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. -

-
-
- Opp lens platform interface on desktop -
-
-

- Platform Statistics -

- {centerFrameItems.map((item, index) => ( - - ))} -
-
-
-
- Opp lens logo -
-
- {contactInfo.map((item) => ( - - ))} -
-
-

- Stockholm, Sweden © 2025 Opplens.com by PBL Partners AB. All Rights - Reserved. Democratizing Tender Access for SMEs. -

-
-
-
- ); -} -function HeaderButtons() { return ( -
- - Contact us - - - Login - -
+
+
+
+

+ AI-Powered +

+ Opp lens platform interface on mobile device +
+

+ Tender Management +

+

Win more tenders, automatically!

+

+ 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. +

+
+
+ Opp lens platform interface on desktop +
+
+

+ Platform Statistics +

+ {centerFrameItems.map((item, index) => ( + + ))} +
+
); } @@ -228,37 +127,3 @@ function CenterFrame({ ); } - -function ContactInfo({ - icon, - contact, - href, - ariaLabel, -}: { - icon: string; - contact: string; - href?: string; - ariaLabel: string; -}) { - return ( -
- - {href ? ( - - {contact} - - ) : ( -

{contact}

- )} -
- ); -} diff --git a/assets/css/style.css b/assets/css/style.css index bdc66ae..3fc04de 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -892,3 +892,42 @@ html body a:hover { } /*# sourceMappingURL=style.css.map */ + +.Toastify__toast { + padding: 10px !important; + border-radius: 24px !important; + font-family: inherit !important; + display: flex !important; + align-items: center !important; + + box-shadow: 0px 12px 34px 0px rgba(13, 10, 44, 0.05) !important; +} + +.dark .Toastify__toast { + box-shadow: + 0 20px 25px -5px rgb(255 255 255 / 0.07), + 0 8px 10px -6px rgb(255 255 255 / 0.05) !important; +} + +.Toastify__close-button { + display: none; +} +.Toastify__close-button { + display: none; +} + +.Toastify__toast--success { + background: linear-gradient(to bottom right, #82e6ac, #1a8245) !important; +} + +.Toastify__toast--error { + background: linear-gradient(to bottom right, #f89090, #e10e0e) !important; +} + +.Toastify__toast--warning { + background: linear-gradient(to bottom right, #f59e0b, #d97706) !important; +} + +.Toastify__toast--info { + background: linear-gradient(to bottom right, #8099ec, #1c3fb7) !important; +} diff --git a/package-lock.json b/package-lock.json index 054bb44..e700c70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "axios": "^1.12.2", "next": "15.5.5", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "react-hook-form": "^7.65.0", + "react-toastify": "^11.0.5" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -1051,6 +1053,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1774,6 +1785,35 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.65.0", + "resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.65.0.tgz", + "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmmirror.com/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.26.0.tgz", diff --git a/package.json b/package.json index ee80422..24a8ffa 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "axios": "^1.12.2", "next": "15.5.5", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "react-hook-form": "^7.65.0", + "react-toastify": "^11.0.5" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/public/side-shallow.svg b/public/side-shallow.svg new file mode 100644 index 0000000..26f5141 --- /dev/null +++ b/public/side-shallow.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/service/api.ts b/service/api.ts new file mode 100644 index 0000000..ea6c091 --- /dev/null +++ b/service/api.ts @@ -0,0 +1,10 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + timeout: 10000, + headers: { + "Content-Type": "application/json", + }, +}); +export default api;