first commit

This commit is contained in:
2026-05-27 15:11:56 +05:30
commit d2e47805c8
44 changed files with 11390 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.next
out
tsconfig.tsbuildinfo
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

50
README.md Normal file
View File

@@ -0,0 +1,50 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

5
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

6
next.config.mjs Normal file
View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default nextConfig;

5884
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "nearle-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@react-three/drei": "^9.114.0",
"@react-three/fiber": "^8.17.10",
"framer-motion": "^12.40.0",
"gsap": "^3.15.0",
"lenis": "^1.3.23",
"lucide-react": "^0.460.0",
"next": "^14.2.35",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"three": "^0.170.0"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@tailwindcss/postcss": "^4.3.0",
"@types/node": "^25.9.1",
"@types/react": "^18.3.29",
"@types/react-dom": "^18.3.7",
"@types/three": "^0.184.1",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.5.0",
"eslint": "^9.13.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"postcss": "^8.5.15",
"tailwindcss": "^3.4.19",
"typescript": "~5.6.2",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

82
src/app/about/page.tsx Normal file
View File

@@ -0,0 +1,82 @@
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { AnimatedCounter } from '@/components/ui/AnimatedCounter'
export default function AboutPage() {
return (
<div className="pt-32 pb-24 sm:pt-40 sm:pb-32 bg-white relative overflow-hidden">
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-purple-soft/40 rounded-full blur-3xl -z-10 translate-x-1/2 -translate-y-1/2 pointer-events-none" />
<div className="max-w-7xl mx-auto px-5 sm:px-8">
<SectionHeader
label="About Us"
heading={
<>
We are Nearle
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
We build Neighbourhoods
</span>
</>
}
sub="Nearle is India's first hyper-local managed 'Neighbourhood living platform' which brings all the amenities and requirements of residents to their fingertips. We are redefining modern urban living by providing never before convenience."
align="center"
className="mb-16"
/>
<div className="grid md:grid-cols-2 gap-8 mb-20">
<GlassCard className="flex flex-col border-purple-lavender/50">
<h3 className="font-display font-extrabold text-xl text-nearle-dark mb-4">Our Mission</h3>
<p className="font-body text-nearle-mid leading-relaxed">
To make mediocre, or average stores, perform as well as the great ones. By going beyond selling a product, to establish a solid brand identity and build a relationship with their customers using Nearle.
</p>
</GlassCard>
<GlassCard className="flex flex-col border-purple-lavender/50">
<h3 className="font-display font-extrabold text-xl text-nearle-dark mb-4">Our Vision</h3>
<p className="font-body text-nearle-mid leading-relaxed">
Our vision is to achieve Global Digital Transformation services leadership in providing value-added high quality solution to our customers.
</p>
</GlassCard>
</div>
<div className="mb-24">
<SectionHeader
heading="Why Nearle"
sub="Nearly is essential for a business to grow by leveraging the full potential of its neighbourhood. It is an indispensable tool for Consumers to take advantage from its Neighbourhoods."
align="left"
className="mb-10"
/>
<div className="bg-nearle-bgsoft rounded-3xl p-8 sm:p-12 border border-nearle-border">
<h3 className="font-display font-bold text-2xl text-nearle-dark mb-4">Don't miss to build your brand with Nearle</h3>
<p className="font-body text-nearle-mid text-lg mb-10 max-w-2xl">
Delight your neighbourhoods with a personalized online experience. Unlock revenue opportunities and marketing strategies that will take your business further.
</p>
<div className="grid grid-cols-3 gap-6">
{[
{ label: 'Customers', val: 100 },
{ label: 'Business', val: 100 },
{ label: 'Neighbourhoods', val: 100 },
].map((stat) => (
<div key={stat.label} className="text-center sm:text-left">
<div className="font-display font-extrabold text-4xl text-purple-deep mb-2">
<AnimatedCounter value={stat.val} suffix="+" />
</div>
<div className="font-body text-sm font-bold text-nearle-mid uppercase tracking-wider">{stat.label}</div>
</div>
))}
</div>
</div>
</div>
<SectionHeader
label="Leadership"
heading="Team Nearle"
sub="Nearle is India's first hyper-local managed 'Neighbourhood living platform' which brings all the amenities and requirements of residents to their fingertips. With stores located within the neighbourhood. Successfully spreading your brand message to your Neighbourhood is the quickest way to generate new revenue stream. Nearle has blossomed into a proven business model Based on the core concept of 'Boost business by trust'."
align="center"
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,76 @@
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
export default function BusinessesPage() {
return (
<div className="pt-32 pb-24 sm:pt-40 sm:pb-32 bg-nearle-bgsoft relative overflow-hidden">
<div className="max-w-7xl mx-auto px-5 sm:px-8">
<SectionHeader
label="Nearle for Business"
heading={
<>
Reach the right people
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
India's #1 Neighbourhood Commerce app
</span>
</>
}
sub="Join the futuristic community lifestyle living. Nearle is built to help businesses like Yours. Get set in minutes and delight the Neighbourhood with a personalized online experience."
align="center"
className="mb-16"
/>
<div className="bg-white rounded-3xl p-8 sm:p-12 shadow-nearle-md border border-purple-lavender/30 mb-20 text-center">
<h3 className="font-display font-extrabold text-2xl text-purple-deep mb-4">Be a part of community</h3>
<p className="font-body text-nearle-dark text-lg max-w-2xl mx-auto mb-8">
Be a part of a community of like-minded people. Let's brand your businesses on Nearle and reach out to your neighbourhood with ease.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 font-mono uppercase tracking-widest text-sm font-bold text-purple-primary">
<span>Get Online</span>
<span className="hidden sm:block"></span>
<span>Get Noticed</span>
<span className="hidden sm:block"></span>
<span>Get Simplified</span>
</div>
</div>
<SectionHeader
heading="Build your brand with Nearle"
sub="Together, we'll move your business forward."
align="left"
className="mb-12"
/>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-20">
{[
{ t: 'Get Online', d: 'Establish a local online presence with Nearle and empower your customers to interact with you.' },
{ t: 'Become a Partner', d: 'Be part of a strong community of like-minded people. A Unified partner for all neighbourhood needs.' },
{ t: 'Build your brand', d: "Build your brand with Nearle. Let's brand your businesses on Nearle and reach-out to your community with ease." },
{ t: 'Huge Opportunity', d: 'Unlock revenue opportunities and marketing strategies that will take your business further.' },
{ t: 'Delight your customers', d: 'Delight your customers in the neighbourhood with a personalized online experience.' },
{ t: 'Unique model', d: "Business looks at Neighbourhood commerce as an Effective model to meet today's consumer needs." },
{ t: 'Reach right audience', d: 'We target your ideal customer — people in your neighborhood search for what you offer.' },
{ t: 'Increase your customers', d: 'Increase walk-in customers. We target your ideal customer — people in your neighborhood search for what you offer.' },
{ t: '...and more', d: 'Introduce your business to your Neighbourhood. Compete with online business through your offline store.' },
].map((feat) => (
<GlassCard key={feat.t} className="flex flex-col border-purple-lavender/40 hover:border-purple-primary">
<h4 className="font-display font-bold text-lg text-nearle-dark mb-3">{feat.t}</h4>
<p className="font-body text-nearle-mid text-sm leading-relaxed">{feat.d}</p>
</GlassCard>
))}
</div>
<div className="text-center bg-purple-deep text-white rounded-3xl p-12 shadow-cta">
<h3 className="font-display font-extrabold text-3xl mb-4">Wanted to know how?</h3>
<p className="font-body text-purple-soft text-lg mb-8">
Nearle is essential for a business to grow by leveraging the full potential of its neighbourhood.
</p>
<button className="bg-white text-purple-deep rounded-full px-8 py-3 font-bold hover:bg-nearle-bgsoft transition-colors shadow-nearle-md">
Talk to our Neighbourhood Expert
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,81 @@
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { AnimatedCounter } from '@/components/ui/AnimatedCounter'
export default function CommunitiesPage() {
return (
<div className="pt-32 pb-24 sm:pt-40 sm:pb-32 bg-white relative overflow-hidden">
<div className="absolute top-1/4 left-0 w-[500px] h-[500px] bg-blue-100 rounded-full blur-3xl -z-10 -translate-x-1/2 pointer-events-none" />
<div className="max-w-7xl mx-auto px-5 sm:px-8">
<SectionHeader
label="Nearle for People"
heading={
<>
Just a Click
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
India's #1 Neighbourhood Commerce app
</span>
</>
}
sub="Join the futuristic community lifestyle living. Be a part of a neighbourhood of like-minded people. Enjoy the comfort of elegant living with Nearle."
align="center"
className="mb-16"
/>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-24">
{[
{ t: 'Awareness at a Touch', d: 'Pick hot deals within your neighbourhood.', cat: 'Gadgets' },
{ t: 'Best of Both Worlds', d: 'Ease of World Wide Web, fulfilled by local business.', cat: 'Attractions' },
{ t: 'Lifestyle', d: 'Enjoy the privilege of upmarket shopping.', cat: 'Fashion & Shopping' },
{ t: 'Join the #1 Neighbourhood app', d: 'Its just a Click For All your needs in your neighbourhood.', cat: 'Fun Things' },
{ t: 'Amazingly Fresh', d: 'Get your fruits, vegetables and daily essentials instantly.', cat: 'Drinks & Summer' },
{ t: 'Restaurants, Take aways', d: 'You choose your best all within your reach.', cat: 'Nearle' },
{ t: 'Never Before Experience', d: 'Customized services for neighbourhood.', cat: 'Tips' },
{ t: 'Home Services @ your convenience', d: 'Home services at your convenience - Like never experienced before.', cat: 'Friends' },
].map((item) => (
<GlassCard key={item.t} className="border-purple-lavender/30">
<span className="text-[10px] font-mono font-bold uppercase tracking-widest text-purple-primary block mb-2">{item.cat}</span>
<h4 className="font-display font-bold text-lg text-nearle-dark mb-2">{item.t}</h4>
<p className="font-body text-nearle-mid text-sm leading-relaxed">{item.d}</p>
</GlassCard>
))}
</div>
<div className="bg-nearle-bgsoft rounded-3xl p-10 text-center border border-purple-lavender/30">
<SectionHeader
heading="Join the #1 Neighbourhood app"
sub="Get all your needs - delivered at your fingertips - from your local store to your home - Instantly!"
align="center"
className="mb-12"
/>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-8 mb-12">
{[
{ val: 100, lbl: 'Restaurants' },
{ val: 100, lbl: 'Green grocers' },
{ val: 100, lbl: 'Home etailers' },
{ val: 100, lbl: 'Service Providers' },
].map((stat) => (
<div key={stat.lbl}>
<div className="font-display font-extrabold text-3xl sm:text-4xl text-purple-deep mb-2">
<AnimatedCounter value={stat.val} suffix="+" />
</div>
<div className="font-body text-xs sm:text-sm font-bold text-nearle-mid uppercase tracking-wider">{stat.lbl}</div>
</div>
))}
</div>
<div className="inline-block p-6 bg-white rounded-2xl shadow-nearle-sm border border-purple-lavender/40">
<h4 className="font-display font-bold text-xl text-nearle-dark mb-2">GET READY NOW</h4>
<p className="font-body text-nearle-mid mb-6">Download Nearle. Enjoy the benefits of Neighbourhood Living.</p>
<button className="bg-purple-deep text-white rounded-full px-8 py-3 font-bold hover:bg-purple-primary transition-colors shadow-cta">
Proudly Support your neighbourhood business
</button>
</div>
</div>
</div>
</div>
)
}

128
src/app/contact/page.tsx Normal file
View File

@@ -0,0 +1,128 @@
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { Mail, Phone, MapPin, Clock } from 'lucide-react'
export default function ContactPage() {
return (
<div className="pt-32 pb-24 sm:pt-40 sm:pb-32 bg-nearle-bgsoft relative overflow-hidden">
<div className="max-w-7xl mx-auto px-5 sm:px-8">
<SectionHeader
label="Contact Us"
heading={
<>
Get in touch
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
Feel free to ask for details
</span>
</>
}
sub="Don't save any questions! We are here to help you build your neighbourhood."
align="center"
className="mb-16"
/>
<div className="grid lg:grid-cols-2 gap-10 items-start">
{/* Form Side */}
<GlassCard className="p-8 border-purple-lavender/50 shadow-nearle-md">
<form className="flex flex-col gap-5">
<div>
<label className="block text-sm font-bold text-nearle-dark mb-2">Full Name</label>
<input
type="text"
placeholder="John Doe"
className="w-full px-4 py-3 rounded-xl border border-nearle-border focus:outline-none focus:border-purple-primary transition-colors"
/>
</div>
<div>
<label className="block text-sm font-bold text-nearle-dark mb-2">Email Address</label>
<input
type="email"
placeholder="john@example.com"
className="w-full px-4 py-3 rounded-xl border border-nearle-border focus:outline-none focus:border-purple-primary transition-colors"
/>
</div>
<div>
<label className="block text-sm font-bold text-nearle-dark mb-2">Subject</label>
<input
type="text"
placeholder="How can we help?"
className="w-full px-4 py-3 rounded-xl border border-nearle-border focus:outline-none focus:border-purple-primary transition-colors"
/>
</div>
<div>
<label className="block text-sm font-bold text-nearle-dark mb-2">Message</label>
<textarea
rows={5}
placeholder="Type your message here..."
className="w-full px-4 py-3 rounded-xl border border-nearle-border focus:outline-none focus:border-purple-primary transition-colors resize-none"
/>
</div>
<button
type="button"
className="mt-2 bg-purple-deep text-white font-bold py-4 rounded-xl shadow-cta hover:bg-purple-primary transition-colors"
>
Send Message
</button>
</form>
</GlassCard>
{/* Info Side */}
<div className="flex flex-col gap-6">
<GlassCard className="flex items-start gap-4 border-purple-lavender/30">
<div className="w-12 h-12 rounded-full bg-purple-soft text-purple-primary flex items-center justify-center shrink-0">
<MapPin size={24} />
</div>
<div>
<h4 className="font-display font-bold text-lg text-nearle-dark mb-1">Office Address</h4>
<p className="font-body text-nearle-mid text-sm leading-relaxed">
Nearle Technology Private Limited,<br />
No.424, Red Rose Towers, DB Road,<br />
RS Puram Coimbatore- 641002
</p>
</div>
</GlassCard>
<GlassCard className="flex items-start gap-4 border-purple-lavender/30">
<div className="w-12 h-12 rounded-full bg-purple-soft text-purple-primary flex items-center justify-center shrink-0">
<Phone size={24} />
</div>
<div>
<h4 className="font-display font-bold text-lg text-nearle-dark mb-1">Phone</h4>
<p className="font-body text-nearle-mid text-sm leading-relaxed">
+91 96... (Contact Number)
</p>
</div>
</GlassCard>
<GlassCard className="flex items-start gap-4 border-purple-lavender/30">
<div className="w-12 h-12 rounded-full bg-purple-soft text-purple-primary flex items-center justify-center shrink-0">
<Mail size={24} />
</div>
<div>
<h4 className="font-display font-bold text-lg text-nearle-dark mb-1">Email</h4>
<p className="font-body text-nearle-mid text-sm leading-relaxed">
care@nearle.in
</p>
</div>
</GlassCard>
<GlassCard className="flex items-start gap-4 border-purple-lavender/30">
<div className="w-12 h-12 rounded-full bg-purple-soft text-purple-primary flex items-center justify-center shrink-0">
<Clock size={24} />
</div>
<div>
<h4 className="font-display font-bold text-lg text-nearle-dark mb-1">Business Hours</h4>
<div className="font-body text-nearle-mid text-sm leading-relaxed flex flex-col gap-1">
<span className="flex justify-between w-48"><span>Monday - Friday</span> <span className="font-bold">9am to 6pm</span></span>
<span className="flex justify-between w-48"><span>Saturday</span> <span className="font-bold">9am to 2pm</span></span>
<span className="flex justify-between w-48"><span>Sunday</span> <span className="font-bold text-red-500">Closed</span></span>
</div>
</div>
</GlassCard>
</div>
</div>
</div>
</div>
)
}

107
src/app/globals.css Normal file
View File

@@ -0,0 +1,107 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--purple-primary: #A467B7;
--purple-deep: #683285;
--purple-accent: #AE79BF;
--purple-lavender: #E7D3EF;
--purple-soft: #F5EEF8;
--text-dark: #111827;
--text-mid: #475569;
--text-light: #94A3B8;
--bg-white: #FFFFFF;
--bg-soft: #F8FAFC;
--border: #E2E8F0;
--shadow-sm: 0 4px 16px rgba(164, 103, 183, 0.08);
--shadow-md: 0 8px 32px rgba(164, 103, 183, 0.15);
--shadow-lg: 0 20px 60px rgba(164, 103, 183, 0.20);
--gradient-hero: radial-gradient(ellipse 80% 60% at 50% -10%, #E7D3EF 0%, #FFFFFF 70%);
--gradient-card: linear-gradient(135deg, #FFFFFF 0%, #F5EEF8 100%);
--gradient-purple: linear-gradient(135deg, #683285 0%, #A467B7 100%);
}
html,
body {
background: var(--bg-white);
color: var(--text-dark);
font-family: var(--font-body), 'DM Sans', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
::selection {
background: var(--purple-deep);
color: #fff;
}
/* Lenis recommended styles */
html.lenis,
html.lenis body {
height: auto;
}
.lenis.lenis-smooth {
scroll-behavior: auto !important;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-stopped {
overflow: hidden;
}
/* Display headings use DM Sans */
.font-display {
letter-spacing: -0.02em;
}
/* Headline scale */
.h1-display {
font-family: var(--font-display), 'DM Sans', sans-serif;
font-weight: 800;
font-size: clamp(40px, 7vw, 80px);
line-height: 1.05;
letter-spacing: -0.02em;
}
.h2-display {
font-family: var(--font-display), 'DM Sans', sans-serif;
font-weight: 700;
font-size: clamp(28px, 4vw, 52px);
line-height: 1.1;
letter-spacing: -0.02em;
}
.h3-display {
font-family: var(--font-display), 'DM Sans', sans-serif;
font-weight: 700;
font-size: clamp(20px, 2.5vw, 32px);
line-height: 1.2;
letter-spacing: -0.01em;
}
.label-mono {
font-family: var(--font-mono), 'JetBrains Mono', monospace;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
}
/* Soft grid background utility */
.bg-grid-soft {
background-image:
linear-gradient(to right, rgba(226, 232, 240, 0.6) 1px, transparent 1px),
linear-gradient(to bottom, rgba(226, 232, 240, 0.6) 1px, transparent 1px);
background-size: 56px 56px;
}
/* Hide scrollbar for horizontal scrollers */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}

80
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,80 @@
import type { Metadata, Viewport } from 'next'
import { DM_Sans, JetBrains_Mono } from 'next/font/google'
import './globals.css'
import { Navbar } from '@/components/layout/Navbar'
import { Footer } from '@/components/layout/Footer'
import { LenisProvider } from '@/lib/LenisProvider'
const dmSansDisplay = DM_Sans({
subsets: ['latin'],
weight: ['700', '800'],
variable: '--font-display',
display: 'swap',
})
const dmSansBody = DM_Sans({
subsets: ['latin'],
weight: ['400', '500', '700'],
variable: '--font-body',
display: 'swap',
})
const jetbrains = JetBrains_Mono({
subsets: ['latin'],
weight: ['400', '500'],
variable: '--font-mono',
display: 'swap',
})
export const metadata: Metadata = {
metadataBase: new URL('https://nearle.in'),
title: {
default: 'Nearle — Your Neighbourhood, Your World',
template: '%s | Nearle',
},
description:
"India's hyperlocal neighbourhood commerce platform. Groceries, restaurants, home services, and trusted local businesses — all delivered to your door.",
keywords: [
'hyperlocal',
'neighbourhood delivery',
'Coimbatore',
'local grocery',
'Nearle',
'home services',
'India delivery',
],
openGraph: {
title: 'Nearle — Your Neighbourhood, Your World',
description: "India's hyperlocal neighbourhood commerce platform",
url: 'https://nearle.in',
siteName: 'Nearle',
locale: 'en_IN',
type: 'website',
},
robots: { index: true, follow: true },
}
export const viewport: Viewport = {
themeColor: '#683285',
width: 'device-width',
initialScale: 1,
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html
lang="en"
className={`${dmSansDisplay.variable} ${dmSansBody.variable} ${jetbrains.variable}`}
>
<body className="bg-white text-nearle-dark font-body antialiased">
<LenisProvider>
<Navbar />
<main>{children}</main>
<Footer />
</LenisProvider>
</body>
</html>
)
}

27
src/app/page.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { Hero } from '@/components/home/Hero'
import { ValueEcosystem } from '@/components/home/ValueEcosystem'
import { NeighbourhoodMap } from '@/components/home/NeighbourhoodMap'
import { Testimonials } from '@/components/home/Testimonials'
import { Download } from '@/components/home/Download'
export default function HomePage() {
return (
<>
<section id="home">
<Hero />
</section>
<section id="features">
<ValueEcosystem />
</section>
<section id="map">
<NeighbourhoodMap />
</section>
<section id="testimonials">
<Testimonials />
</section>
<section id="download">
<Download />
</section>
</>
)
}

View File

@@ -0,0 +1,375 @@
'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { Badge } from '@/components/ui/Badge'
import { fadeUp, staggerContainer, viewportConfig } from '@/lib/animations'
type AppDetails = {
id: string
name: string
purpose: string
playUrl: string
features: string[]
themeColor: string
mockScreen: React.ReactNode
}
export function AppEcosystem() {
const [activeApp, setActiveApp] = useState<string>('deal')
const APPS: AppDetails[] = [
{
id: 'deal',
name: 'Nearle Deal',
purpose: 'Customer Shopping & Hyperlocal Commerce App',
playUrl: 'https://play.google.com/store/apps/details?id=com.nearle.deal',
themeColor: 'bg-purple-deep',
features: [
'Grocery & vegetable shopping',
'Restaurant food delivery',
'Exclusive hyperlocal local society discounts',
'Smart neighborhood deals',
],
mockScreen: (
<div className="w-full h-full bg-[#FAF8FB] p-5 flex flex-col justify-between font-body text-nearle-dark select-none">
{/* Header */}
<div className="flex items-center justify-between pb-3 border-b border-purple-lavender/30">
<span className="font-display font-extrabold text-sm text-purple-deep">Nearle Deal</span>
<span className="w-2.5 h-2.5 rounded-full bg-green-500 animate-pulse" />
</div>
{/* Body */}
<div className="flex-1 flex flex-col justify-center py-4">
<p className="text-[10px] uppercase font-mono tracking-wider text-purple-primary">Best Selling Near You</p>
<h4 className="font-display font-bold text-base mt-1">RS Puram Organic Mart</h4>
<div className="mt-3 bg-white p-3 rounded-2xl border border-nearle-border shadow-nearle-sm flex items-center gap-3">
<span className="text-2xl">🥬</span>
<div className="flex-1">
<p className="font-display font-bold text-xs">Fresh Farm Spinach</p>
<p className="text-[10px] text-nearle-mid mt-0.5">Qty: 500g · Fast Delivery</p>
</div>
<span className="font-mono text-xs font-bold text-purple-deep">28</span>
</div>
<div className="mt-3 bg-purple-soft/60 border border-purple-lavender/30 p-3 rounded-2xl flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xs">🛵</span>
<span className="text-[10px] font-bold text-purple-deep">Delivery in 14 Mins</span>
</div>
<span className="text-[9px] uppercase font-mono tracking-widest text-purple-primary font-bold">Track</span>
</div>
</div>
{/* Footer Checkout */}
<button className="w-full py-2.5 bg-purple-deep text-white rounded-full font-bold text-xs shadow-cta hover:bg-purple-primary transition">
Proceed to Checkout
</button>
</div>
),
},
{
id: 'partner',
name: 'Nearle Partner',
purpose: 'Merchant & Local Shop Management Platform',
playUrl: 'https://play.google.com/store/apps/details?id=com.nearle.partner',
themeColor: 'bg-purple-primary',
features: [
'Instant order notification alerts',
'Intuitive inventory catalog editor',
'Sales analysis and growth dashboards',
'Fast direct bank payout settlements',
],
mockScreen: (
<div className="w-full h-full bg-[#FCFAFD] p-5 flex flex-col justify-between font-body text-nearle-dark select-none">
{/* Header */}
<div className="flex items-center justify-between pb-3 border-b border-purple-lavender/30">
<span className="font-display font-extrabold text-sm text-purple-deep">Nearle Partner</span>
<span className="text-[10px] font-mono bg-purple-lavender px-2 py-0.5 rounded-full text-purple-deep font-bold">Active</span>
</div>
{/* Body */}
<div className="flex-1 flex flex-col justify-center py-4">
<p className="text-[10px] uppercase font-mono tracking-wider text-purple-primary">Business Analytics</p>
<h4 className="font-display font-bold text-base mt-1">Today&apos;s Revenue</h4>
<div className="mt-3 bg-white p-4 rounded-2xl border border-nearle-border shadow-nearle-sm">
<p className="text-2xl font-display font-extrabold text-nearle-dark">12,480</p>
<div className="flex items-center gap-1.5 text-green-600 text-xs mt-1 font-bold">
<span> 18.5%</span>
<span className="text-nearle-light font-normal text-[10px]">vs yesterday</span>
</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-2 text-center">
<div className="bg-purple-soft/40 p-2.5 rounded-xl border border-purple-lavender/10">
<p className="font-mono text-xs font-bold text-purple-deep">38</p>
<p className="text-[9px] text-nearle-light mt-0.5">Orders Accepted</p>
</div>
<div className="bg-purple-soft/40 p-2.5 rounded-xl border border-purple-lavender/10">
<p className="font-mono text-xs font-bold text-purple-deep">99.2%</p>
<p className="text-[9px] text-nearle-light mt-0.5">Accuracy Rate</p>
</div>
</div>
</div>
{/* Footer Active Alert */}
<div className="bg-emerald-500 text-white rounded-full p-2 text-center text-[10px] font-bold tracking-wider animate-pulse uppercase">
New Order Received
</div>
</div>
),
},
{
id: 'gear',
name: 'Nearle Gear',
purpose: 'Delivery Partner Fleet Optimization App',
playUrl: 'https://play.google.com/store/apps/details?id=com.nearle.gear',
themeColor: 'bg-purple-accent',
features: [
'Smart real-time route path optimization',
'Direct rider earnings and payout log',
'Instant pickup/delivery verification alerts',
'Flexible custom slots for riders',
],
mockScreen: (
<div className="w-full h-full bg-[#FAF8FB] p-5 flex flex-col justify-between font-body text-nearle-dark select-none">
{/* Header */}
<div className="flex items-center justify-between pb-3 border-b border-purple-lavender/30">
<span className="font-display font-extrabold text-sm text-purple-deep">Nearle Gear</span>
<span className="text-[10px] font-mono bg-purple-soft text-purple-deep px-2 py-0.5 rounded-full border border-purple-lavender/30 font-bold">On Duty</span>
</div>
{/* Body */}
<div className="flex-1 flex flex-col justify-center py-4">
<p className="text-[10px] uppercase font-mono tracking-wider text-purple-primary">Optimal Delivery Route</p>
<h4 className="font-display font-bold text-base mt-1">Route #4902</h4>
<div className="mt-3 bg-white p-3 rounded-2xl border border-nearle-border shadow-nearle-sm flex flex-col gap-3 font-body">
<div className="flex gap-2">
<span className="text-purple-primary font-mono text-xs">A</span>
<span className="text-xs text-nearle-dark font-bold">Kalyan Grocer (Pickup)</span>
</div>
<div className="flex gap-2 border-t border-nearle-border/40 pt-2">
<span className="text-purple-deep font-mono text-xs">B</span>
<span className="text-xs text-nearle-dark font-bold">Siddharth Apts (Drop)</span>
</div>
</div>
<div className="mt-3 bg-purple-soft/60 border border-purple-lavender/30 p-2.5 rounded-xl flex justify-between items-center">
<span className="text-[10px] text-nearle-mid font-semibold">Active Slot Earnings</span>
<span className="font-mono text-xs font-bold text-purple-deep">480</span>
</div>
</div>
{/* Footer Action */}
<button className="w-full py-2.5 bg-purple-deep text-white rounded-full font-bold text-xs shadow-cta hover:bg-purple-primary transition">
Swipe to Complete Drop
</button>
</div>
),
},
{
id: 'admin',
name: 'Nearle Admin',
purpose: 'Operations Control & Analytics Platform',
playUrl: 'https://play.google.com/store/apps/details?id=com.nearle.admin',
themeColor: 'bg-purple-deep',
features: [
'Real-time society network dashboard',
'Automatic rider assigning algorithms',
'Comprehensive user feedback tracker',
'Escrow and merchant settlement monitoring',
],
mockScreen: (
<div className="w-full h-full bg-[#FAF8FB] p-5 flex flex-col justify-between font-body text-nearle-dark select-none">
{/* Header */}
<div className="flex items-center justify-between pb-3 border-b border-purple-lavender/30">
<span className="font-display font-extrabold text-sm text-purple-deep">Nearle Admin</span>
<span className="text-[10px] font-mono bg-purple-deep text-white px-2 py-0.5 rounded-full font-bold">HQ</span>
</div>
{/* Body */}
<div className="flex-1 flex flex-col justify-center py-4">
<p className="text-[10px] uppercase font-mono tracking-wider text-purple-primary">Operations Center</p>
<h4 className="font-display font-bold text-base mt-1">Platform Diagnostics</h4>
<div className="mt-3 bg-white p-3 rounded-2xl border border-nearle-border shadow-nearle-sm flex flex-col gap-2">
<div className="flex justify-between items-center">
<span className="text-xs text-nearle-mid">Active Society Hubs</span>
<span className="font-mono text-xs font-bold text-purple-deep">24 / 25</span>
</div>
<div className="flex justify-between items-center border-t border-nearle-border/40 pt-2">
<span className="text-xs text-nearle-mid">Fleet Operational</span>
<span className="font-mono text-xs font-bold text-purple-deep">98.4%</span>
</div>
</div>
<div className="mt-3 bg-green-50 border border-green-200 p-2.5 rounded-xl text-center">
<span className="text-[10px] text-green-700 font-bold"> All services operational</span>
</div>
</div>
{/* Footer Option */}
<button className="w-full py-2.5 bg-nearle-dark text-white rounded-full font-bold text-xs hover:bg-nearle-mid transition">
Launch Admin Center
</button>
</div>
),
},
]
const activeAppDetails = APPS.find((app) => app.id === activeApp) || APPS[0]!
return (
<div className="py-24 sm:py-32 bg-nearle-bgsoft px-5 sm:px-8 relative overflow-hidden">
{/* Decorative Blur Backgrounds */}
<div className="absolute top-1/4 left-1/4 w-[500px] h-[500px] rounded-full bg-purple-soft/50 blur-3xl -z-10 pointer-events-none" />
<div className="absolute bottom-1/4 right-1/4 w-[500px] h-[500px] rounded-full bg-purple-lavender/30 blur-3xl -z-10 pointer-events-none" />
<div className="max-w-7xl mx-auto">
<SectionHeader
label="App Ecosystem"
heading={
<>
Unified Hyperlocal
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
Mobile Suite
</span>
</>
}
sub="Nearle powers four custom-tailored mobile applications designed for the distinct members of our active community."
align="center"
className="mb-16 sm:mb-20"
/>
<div className="grid lg:grid-cols-12 gap-12 items-center">
{/* Mockup Showcase Column (5 cols) */}
<div className="lg:col-span-5 flex justify-center">
<div className="relative">
{/* Floating notification decor */}
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ duration: 5, repeat: Infinity, ease: 'easeInOut' }}
className="absolute top-12 -left-10 z-20 w-[160px] bg-white border border-nearle-border shadow-nearle-md p-3 rounded-2xl flex items-center gap-2"
>
<div className="w-6 h-6 rounded-full bg-purple-lavender flex items-center justify-center text-xs">💜</div>
<div className="flex-1">
<p className="font-display font-bold text-[10px] text-nearle-dark">Delivery Alert</p>
<p className="font-body text-[8px] text-nearle-mid">Rider dispatched</p>
</div>
</motion.div>
<motion.div
animate={{ y: [0, 8, 0] }}
transition={{ duration: 6, repeat: Infinity, ease: 'easeInOut', delay: 1 }}
className="absolute bottom-16 -right-12 z-20 w-[140px] bg-white border border-nearle-border shadow-nearle-md p-3 rounded-2xl flex items-center gap-2"
>
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center text-xs"></div>
<div className="flex-1">
<p className="font-display font-bold text-[10px] text-nearle-dark">Payout Sent</p>
<p className="font-body text-[8px] text-green-600 font-bold">+1,250</p>
</div>
</motion.div>
{/* Smartphone Outer Shell */}
<div className="relative w-[280px] h-[570px] rounded-[48px] border-[12px] border-nearle-dark bg-nearle-dark shadow-nearle-lg overflow-hidden flex flex-col">
{/* Dynamic Screen Container */}
<div className="flex-1 rounded-[36px] overflow-hidden relative bg-white">
{/* Phone Notch */}
<div className="absolute top-0 inset-x-0 h-5 bg-nearle-dark flex justify-center items-center z-30">
<div className="w-16 h-4 bg-nearle-dark rounded-b-xl" />
</div>
{/* Screen Content */}
<div className="w-full h-full pt-5">
<AnimatePresence mode="wait">
<motion.div
key={activeAppDetails.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="w-full h-full"
>
{activeAppDetails.mockScreen}
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</div>
</div>
{/* Description & Selection Tabs Column (7 cols) */}
<div className="lg:col-span-7 flex flex-col gap-8">
<div className="grid grid-cols-2 gap-4">
{APPS.map((app) => {
const isActive = app.id === activeApp
return (
<button
key={app.id}
onClick={() => setActiveApp(app.id)}
className={`text-left p-5 rounded-3xl border transition-all duration-300 focus:outline-none ${
isActive
? 'bg-white border-purple-deep shadow-nearle-md'
: 'bg-white/40 border-nearle-border hover:border-purple-primary'
}`}
>
<span className="font-mono text-[9px] uppercase tracking-widest text-purple-primary">
Official Application
</span>
<h3 className="font-display font-bold text-lg text-nearle-dark mt-1">
{app.name}
</h3>
</button>
)
})}
</div>
{/* Selected App Details Panel */}
<GlassCard className="border-purple-lavender/50 hover:border-purple-primary shadow-nearle-md p-8">
<span className={`inline-block px-3 py-1 rounded-full text-white text-xs font-mono font-bold uppercase tracking-wider ${activeAppDetails.themeColor} mb-4`}>
{activeAppDetails.name}
</span>
<h3 className="h3-display text-nearle-dark">
{activeAppDetails.purpose}
</h3>
<p className="font-body text-nearle-mid mt-4 leading-relaxed">
Specifically built with GPU-optimised rendering, secure escrowed payments, and smart push alert integrations to empower this segment.
</p>
<ul className="mt-6 grid sm:grid-cols-2 gap-4">
{activeAppDetails.features.map((feat) => (
<li key={feat} className="flex items-start gap-3">
<span className="w-5 h-5 rounded-full bg-purple-soft flex items-center justify-center text-xs text-purple-deep font-bold mt-0.5">
</span>
<span className="font-body text-sm text-nearle-dark">
{feat}
</span>
</li>
))}
</ul>
<div className="mt-8 pt-6 border-t border-nearle-border flex flex-wrap gap-4 items-center justify-between">
<a
href={activeAppDetails.playUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2.5 px-6 py-2.5 bg-[#111827] text-white rounded-full font-bold text-sm shadow-cta hover:bg-purple-deep transition active:scale-95"
>
<span className="text-lg">🤖</span>
<span>Get on Google Play</span>
</a>
</div>
</GlassCard>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,175 @@
'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { staggerContainer, fadeUp, scaleUp, viewportConfig } from '@/lib/animations'
type CategoryItem = {
id: string
name: string
icon: string
subItems: string[]
accentColor: string
}
const CATEGORIES: CategoryItem[] = [
{
id: 'groceries',
name: 'Fresh Groceries',
icon: '🥬',
subItems: ['Fruits & Greens', 'Local Dairy & Farm Milk', 'Snacks & Beverages', 'Spices & Staples'],
accentColor: 'border-green-300 bg-green-500/5 text-green-600',
},
{
id: 'food',
name: 'Food & Dining',
icon: '🍜',
subItems: ['South Indian Special', 'Fine Dine Curries', 'Organic Salads', 'Street Side Sweets'],
accentColor: 'border-orange-300 bg-orange-500/5 text-orange-600',
},
{
id: 'services',
name: 'Home Services',
icon: '🧹',
subItems: ['Appliance Repair', 'Premium Deep Cleaning', 'Plumbing & Wiring', 'Home Painting'],
accentColor: 'border-blue-300 bg-blue-500/5 text-blue-600',
},
]
export function Categories() {
const [active, setActive] = useState<string>('groceries')
const activeCategory = CATEGORIES.find((cat) => cat.id === active) || CATEGORIES[0]!
return (
<div className="py-24 sm:py-32 bg-white px-5 sm:px-8 relative overflow-hidden">
<div className="absolute top-1/2 left-0 w-[400px] h-[400px] rounded-full bg-purple-soft/30 blur-3xl -z-10 pointer-events-none" />
<div className="max-w-7xl mx-auto">
<SectionHeader
label="Smarter Taxonomy"
heading={
<>
Explore our diverse
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
Platform Categories
</span>
</>
}
sub="Everything categorised precisely. Whether you need grocery supplies, a hot meal, or a trusted electrician, find it instantly."
align="center"
className="mb-16 sm:mb-20"
/>
<div className="grid lg:grid-cols-12 gap-8 items-start">
{/* Category Tabs (5 cols) */}
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewportConfig}
className="lg:col-span-5 flex flex-col gap-4"
>
{CATEGORIES.map((cat) => {
const isActive = cat.id === active
return (
<motion.button
key={cat.id}
variants={fadeUp}
onClick={() => setActive(cat.id)}
className={`w-full text-left p-5 rounded-2xl border transition-all duration-300 focus:outline-none ${
isActive
? 'bg-purple-deep text-white border-purple-deep shadow-nearle-md'
: 'bg-white border-nearle-border hover:border-purple-primary text-nearle-dark'
}`}
>
<div className="flex items-center gap-4">
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center text-2xl transition-colors duration-300 ${
isActive ? 'bg-white/10 text-white' : 'bg-purple-soft text-purple-deep'
}`}
>
{cat.icon}
</div>
<div className="flex-1">
<h3 className="font-display font-bold text-base leading-tight">
{cat.name}
</h3>
<p
className={`text-xs mt-1 font-body transition-colors duration-300 ${
isActive ? 'text-purple-lavender' : 'text-nearle-mid'
}`}
>
{cat.subItems.length} core segments available
</p>
</div>
</div>
</motion.button>
)
})}
</motion.div>
{/* Sub-Category Preview Panel (7 cols) */}
<div className="lg:col-span-7 h-full">
<AnimatePresence mode="wait">
<motion.div
key={activeCategory.id}
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
transition={{ duration: 0.35 }}
>
<GlassCard className="border-purple-lavender/50 hover:border-purple-primary p-8 min-h-[380px] flex flex-col justify-between shadow-nearle-md">
<div>
<div className="flex items-center gap-3 mb-6">
<span className="text-4xl">{activeCategory.icon}</span>
<div>
<span className={`inline-block px-2.5 py-0.5 rounded-full border text-[10px] font-mono font-bold uppercase tracking-wider ${activeCategory.accentColor}`}>
Ecosystem Partner Group
</span>
<h3 className="font-display font-extrabold text-2xl text-nearle-dark mt-1">
{activeCategory.name}
</h3>
</div>
</div>
<p className="font-body text-nearle-mid text-base leading-relaxed mb-8">
We curate verified local operators under this segment, providing them with robust tools, escrowed payouts, and real-time live navigation.
</p>
<div className="grid sm:grid-cols-2 gap-4">
{activeCategory.subItems.map((sub, idx) => (
<motion.div
key={sub}
variants={scaleUp}
initial="hidden"
animate="visible"
transition={{ delay: idx * 0.08 }}
className="flex items-center gap-3 p-4 bg-purple-soft/40 border border-purple-lavender/20 rounded-xl"
>
<span className="w-1.5 h-1.5 rounded-full bg-purple-primary" />
<span className="font-display font-bold text-sm text-nearle-dark">
{sub}
</span>
</motion.div>
))}
</div>
</div>
<div className="mt-8 pt-6 border-t border-nearle-border flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="font-body text-xs text-nearle-light text-center sm:text-left">
Interested in joining as a partner? Click become a partner in the hero section or visit our businesses page.
</p>
</div>
</GlassCard>
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,102 @@
'use client'
import dynamic from 'next/dynamic'
import { motion } from 'framer-motion'
import { fadeUp, viewportConfig } from '@/lib/animations'
const CityRider3D = dynamic(() => import('@/components/three/CityRider3D'), {
ssr: false,
loading: () => (
<div className="w-full h-full flex items-center justify-center bg-purple-soft/60 rounded-3xl">
<span className="font-mono text-xs text-purple-deep tracking-widest uppercase">
Loading scene
</span>
</div>
),
})
export function ConnectLocally() {
return (
<div className="relative py-20 sm:py-28 px-5 sm:px-8 bg-nearle-bgsoft overflow-hidden">
<div className="max-w-7xl mx-auto">
<div className="relative rounded-3xl overflow-hidden border border-nearle-border shadow-nearle-md bg-white">
{/* 3D scene */}
<div className="relative h-[480px] sm:h-[560px] lg:h-[620px]">
<CityRider3D />
{/* Top-left info card */}
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewportConfig}
className="absolute top-5 left-5 sm:top-7 sm:left-7 max-w-[300px] bg-white border border-nearle-border rounded-2xl px-4 py-3 shadow-nearle-md flex items-start gap-3"
>
<div className="w-9 h-9 rounded-xl bg-purple-soft flex items-center justify-center text-lg shrink-0">
🚚
</div>
<div>
<div className="label-mono text-purple-primary">Nearle Commerce</div>
<div className="font-display font-extrabold text-nearle-dark text-base leading-tight mt-0.5">
Connect Locally
</div>
<p className="font-body text-xs text-nearle-mid mt-1.5 leading-snug">
Households nearby discover your store and order in a few taps.
</p>
</div>
</motion.div>
{/* Top-right metrics card */}
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewportConfig}
className="absolute top-5 right-5 sm:top-7 sm:right-7 w-[260px] bg-white border border-nearle-border rounded-2xl p-4 shadow-nearle-md"
>
<div className="label-mono text-nearle-light">Neighbourhood Metrics</div>
<div className="mt-3 flex items-center justify-between">
<span className="font-body text-sm text-nearle-mid">Community Progress</span>
<span className="font-mono text-sm font-bold text-purple-primary">35%</span>
</div>
<div className="mt-1.5 h-1.5 rounded-full bg-purple-soft overflow-hidden">
<motion.div
className="h-full bg-gradient-purple"
initial={{ width: 0 }}
whileInView={{ width: '35%' }}
viewport={{ once: true }}
transition={{ duration: 1.2, ease: 'easeOut' }}
/>
</div>
<div className="mt-4 space-y-1.5 text-sm">
<div className="flex justify-between">
<span className="text-nearle-mid">Status</span>
<span className="font-bold text-nearle-dark">Building</span>
</div>
<div className="flex justify-between">
<span className="text-nearle-mid">Reach</span>
<span className="font-bold text-nearle-dark">Neighbourhood</span>
</div>
<div className="flex justify-between">
<span className="text-nearle-mid">Model</span>
<span className="font-bold text-nearle-dark">Hyper-local</span>
</div>
</div>
</motion.div>
{/* Bottom hint */}
<motion.div
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
className="absolute bottom-5 right-5 sm:bottom-7 sm:right-7 bg-white border border-nearle-border rounded-full px-4 py-2 shadow-nearle-sm font-body text-xs text-nearle-mid"
>
Scroll down to explore Nearle commerce
</motion.div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,146 @@
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { Button } from '@/components/ui/Button'
export function Contact() {
const [email, setEmail] = useState('')
const [society, setSociety] = useState('')
const [submitted, setSubmitted] = useState(false)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (email) {
setSubmitted(true)
setTimeout(() => {
setSubmitted(false)
setEmail('')
setSociety('')
}, 4000)
}
}
return (
<div className="py-24 sm:py-32 bg-nearle-bgsoft px-5 sm:px-8 relative overflow-hidden">
{/* Decorative Blur Backgrounds */}
<div className="absolute top-1/4 left-1/4 w-[400px] h-[400px] rounded-full bg-purple-soft/30 blur-3xl -z-10 pointer-events-none" />
<div className="max-w-6xl mx-auto">
<div className="grid lg:grid-cols-12 gap-12 items-center">
{/* Left Column Text (5 cols) */}
<div className="lg:col-span-5 flex flex-col justify-center">
<SectionHeader
label="Join the Network"
heading={
<>
Request Nearle in
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
Your Neighbourhood
</span>
</>
}
sub="Are you a society coordinator or a local merchant who wants to activate cooperative living? Reach out and we will geofence your area."
align="left"
className="mb-8"
/>
<div className="flex flex-col gap-5 font-body text-nearle-mid text-sm">
<div className="flex items-center gap-3">
<span className="w-10 h-10 rounded-xl bg-purple-soft text-purple-deep flex items-center justify-center text-lg">
📍
</span>
<div>
<p className="font-bold text-nearle-dark">HQ Location</p>
<p>RS Puram, Coimbatore, Tamil Nadu, India</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className="w-10 h-10 rounded-xl bg-purple-soft text-purple-deep flex items-center justify-center text-lg">
</span>
<div>
<p className="font-bold text-nearle-dark">Official Email</p>
<p>partners@nearle.in · operations@nearle.in</p>
</div>
</div>
</div>
</div>
{/* Right Column Form (7 cols) */}
<div className="lg:col-span-7">
<GlassCard className="border-purple-lavender/50 hover:border-purple-primary shadow-nearle-md p-8 sm:p-10 relative overflow-hidden">
{submitted ? (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center py-12 flex flex-col items-center"
>
<span className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center text-3xl mb-6 shadow-sm">
</span>
<h3 className="font-display font-extrabold text-2xl text-nearle-dark">
Request Received!
</h3>
<p className="font-body text-nearle-mid text-sm leading-relaxed mt-4 max-w-sm">
Thank you! Our geofencing calibration team will contact you or your society management shortly to initiate the verification process.
</p>
</motion.div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
<div>
<label
htmlFor="email-address"
className="block font-display font-bold text-xs uppercase tracking-wider text-purple-deep mb-2"
>
Email Address
</label>
<input
id="email-address"
type="email"
required
placeholder="e.g. coordinator@society.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-white border border-nearle-border rounded-xl px-4 py-3.5 text-sm focus:outline-none focus:border-purple-primary focus:ring-1 focus:ring-purple-primary transition-all duration-300"
/>
</div>
<div>
<label
htmlFor="society-name"
className="block font-display font-bold text-xs uppercase tracking-wider text-purple-deep mb-2"
>
Society or Complex Name
</label>
<input
id="society-name"
type="text"
placeholder="e.g. Sree Vatsa Residency, RS Puram"
value={society}
onChange={(e) => setSociety(e.target.value)}
className="w-full bg-white border border-nearle-border rounded-xl px-4 py-3.5 text-sm focus:outline-none focus:border-purple-primary focus:ring-1 focus:ring-purple-primary transition-all duration-300"
/>
</div>
<Button
type="submit"
variant="primary"
size="md"
className="w-full justify-center mt-2 shadow-cta hover:shadow-cta-hover transition-all"
>
Send Society Request
</Button>
</form>
)}
</GlassCard>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,136 @@
'use client'
import { motion } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { fadeUp, staggerContainer, viewportConfig } from '@/lib/animations'
type DownloadCard = {
name: string
role: string
desc: string
playUrl: string
qrSymbol: string
color: string
tag: string
}
const CARDS: DownloadCard[] = [
{
name: 'Nearle Deal',
role: 'For Residents',
desc: 'Order groceries, buy daily essentials, secure community deals, and manage local bookings in one dashboard.',
playUrl: 'https://play.google.com/store/apps/details?id=com.nearle.deal',
qrSymbol: '🥦',
color: 'border-purple-lavender/50 hover:border-purple-primary bg-gradient-to-br from-white to-purple-soft/20',
tag: 'Shop Now',
},
{
name: 'Nearle Partner',
role: 'For Businesses',
desc: 'List catalog products, manage orders instantly, inspect revenue graphs, and execute instant escrow cash outs.',
playUrl: 'https://play.google.com/store/apps/details?id=com.nearle.partner',
qrSymbol: '🏪',
color: 'border-purple-lavender/50 hover:border-purple-primary bg-gradient-to-br from-white to-purple-soft/20',
tag: 'Grow Sales',
},
{
name: 'Nearle Gear',
role: 'For Riders',
desc: 'Receive optimized route paths, complete geolocated drop confirmations, and view comprehensive fleet earnings.',
playUrl: 'https://play.google.com/store/apps/details?id=com.nearle.gear',
qrSymbol: '🛵',
color: 'border-purple-lavender/50 hover:border-purple-primary bg-gradient-to-br from-white to-purple-soft/20',
tag: 'Earn Daily',
},
{
name: 'Nearle Admin',
role: 'For Coordinators',
desc: 'Oversee local hubs, monitor rider activity levels, approve new merchant stores, and check operational health.',
playUrl: 'https://play.google.com/store/apps/details?id=com.nearle.admin',
qrSymbol: '🔮',
color: 'border-purple-lavender/50 hover:border-purple-primary bg-gradient-to-br from-white to-purple-soft/20',
tag: 'Manage Hub',
},
]
export function Download() {
return (
<div className="py-24 sm:py-32 bg-nearle-bgsoft px-5 sm:px-8 relative overflow-hidden">
{/* Decorative Grids */}
<div className="absolute inset-0 bg-grid-soft opacity-30 pointer-events-none" />
<div className="max-w-7xl mx-auto">
<SectionHeader
label="Mobile Suite"
heading={
<>
Deploy the Nearle
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
Platform Today
</span>
</>
}
sub="Scan the QR codes or download directly from Google Play Store to activate your smart neighbourhood ecosystem."
align="center"
className="mb-16 sm:mb-20"
/>
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewportConfig}
className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-8"
>
{CARDS.map((card) => (
<motion.div key={card.name} variants={fadeUp} className="group">
<GlassCard className={`h-full flex flex-col justify-between p-6 ${card.color} shadow-nearle-sm hover:shadow-nearle-lg group-hover:scale-[1.02] transition-all duration-300`}>
<div>
<div className="flex items-center justify-between gap-3 mb-6">
<span className="text-[10px] font-mono font-bold tracking-widest text-purple-primary uppercase">
{card.role}
</span>
<span className="bg-purple-lavender text-purple-deep rounded-full px-2.5 py-0.5 font-mono text-[9px] font-bold tracking-widest uppercase">
{card.tag}
</span>
</div>
<h3 className="font-display font-extrabold text-xl text-nearle-dark group-hover:text-purple-deep transition-colors duration-200">
{card.name}
</h3>
<p className="font-body text-nearle-mid text-xs leading-relaxed mt-3 mb-8">
{card.desc}
</p>
</div>
<div className="flex items-center justify-between gap-4 pt-6 border-t border-nearle-border/40">
{/* Mock QR Code container */}
<div className="w-16 h-16 rounded-xl bg-white border border-nearle-border flex flex-col items-center justify-center p-1 shadow-sm relative group-hover:border-purple-primary transition-all duration-300 select-none">
<div className="w-full h-full border-2 border-dashed border-purple-lavender rounded-lg flex items-center justify-center text-xl">
{card.qrSymbol}
</div>
</div>
<div className="flex-1 flex flex-col gap-1.5">
<a
href={card.playUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 bg-nearle-dark hover:bg-purple-deep text-white rounded-full font-bold text-[10px] tracking-wider uppercase transition active:scale-95 shadow-sm"
>
<span>🤖</span>
<span>Play Store</span>
</a>
</div>
</div>
</GlassCard>
</motion.div>
))}
</motion.div>
</div>
</div>
)
}

107
src/components/home/FAQ.tsx Normal file
View File

@@ -0,0 +1,107 @@
'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
type FAQItem = {
question: string
answer: string
}
const FAQS: FAQItem[] = [
{
question: 'What is Nearle?',
answer: 'Nearle is Indias first premium managed smart hyperlocal neighbourhood platform. We map out geofenced societies and connect residents directly to verified local merchants, service technicians, and private community deals in a unified digital ecosystem.',
},
{
question: 'How does hyperlocal delivery work?',
answer: 'Once an order is placed on the Nearle Deal app, nearby partner stores receive instant geofenced alerts. Dedicated Nearle Gear riders use optimised routing navigation to collect the packed package and complete the delivery in under 15-20 minutes.',
},
{
question: 'How can local businesses and shops join?',
answer: 'Local shops, supermarkets, restaurants, and freelance service providers can apply via the Nearle Partner app. After quick boundary verification and catalog sync, they go live in the local geofenced mesh.',
},
{
question: 'Which cooperative societies and communities are supported?',
answer: 'Currently we are actively activating major societies and gated complexes in Coimbatore. If your society is not geofenced yet, you can send an onboarding request via the coordinator hub inside the Admin application.',
},
{
question: 'Is delivery available 24/7?',
answer: 'Our smart network works matching store opening timings. Most essential grocery and food categories operate from 7:00 AM to 11:00 PM, while instant peer-to-peer courier options remain active in key zones 24/7.',
},
]
export function FAQ() {
const [openIdx, setOpenIdx] = useState<number | null>(0) // Default expand first question
const toggle = (idx: number) => {
setOpenIdx(openIdx === idx ? null : idx)
}
return (
<div className="py-24 sm:py-32 bg-white px-5 sm:px-8 relative overflow-hidden">
{/* Decorative Blur BG */}
<div className="absolute bottom-1/4 left-1/4 w-[500px] h-[500px] rounded-full bg-purple-soft/30 blur-3xl -z-10 pointer-events-none" />
<div className="max-w-4xl mx-auto">
<SectionHeader
label="Help Center"
heading={
<>
Frequently Asked
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
Questions
</span>
</>
}
sub="Everything you need to know about the Nearle hyperlocal platform, society geofencing, and merchant tools."
align="center"
className="mb-16 sm:mb-20"
/>
<div className="flex flex-col gap-4">
{FAQS.map((faq, idx) => {
const isOpen = openIdx === idx
return (
<GlassCard
key={idx}
hover={false}
className="border-purple-lavender/50 hover:border-purple-primary transition-all duration-300 p-0 overflow-hidden shadow-nearle-sm"
>
<button
onClick={() => toggle(idx)}
className="w-full text-left p-6 sm:p-8 flex justify-between items-center gap-4 focus:outline-none select-none group"
>
<span className="font-display font-bold text-base sm:text-lg text-nearle-dark group-hover:text-purple-deep transition-colors duration-200">
{faq.question}
</span>
<span className={`w-8 h-8 rounded-full bg-purple-soft flex items-center justify-center text-purple-deep transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`}>
</span>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
<div className="px-6 pb-6 sm:px-8 sm:pb-8 font-body text-nearle-mid text-sm sm:text-base leading-relaxed border-t border-nearle-border/40 pt-4 bg-[#FCFAFD]/50">
{faq.answer}
</div>
</motion.div>
)}
</AnimatePresence>
</GlassCard>
)
})}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,108 @@
'use client'
import Link from 'next/link'
import dynamic from 'next/dynamic'
import { motion } from 'framer-motion'
import { fadeUp, staggerContainer } from '@/lib/animations'
const HeroPhones3D = dynamic(() => import('@/components/three/HeroPhones3D'), {
ssr: false,
loading: () => (
<div className="w-full h-full flex items-center justify-center">
<span className="font-mono text-xs text-purple-deep tracking-widest uppercase">
Loading
</span>
</div>
),
})
export function Hero() {
return (
<div className="relative isolate overflow-hidden bg-gradient-hero min-h-[100svh] flex items-center">
<div className="absolute inset-0 bg-grid-soft opacity-50 pointer-events-none" />
<FloatingBlobs />
<div className="relative z-10 max-w-7xl mx-auto px-5 sm:px-8 w-full grid lg:grid-cols-12 gap-10 items-center pt-28 pb-20">
<motion.div
className="lg:col-span-7 text-center lg:text-left"
variants={staggerContainer}
initial="hidden"
animate="visible"
>
<motion.span
variants={fadeUp}
className="inline-block label-mono text-purple-primary"
>
India&apos;s Hyperlocal Platform
</motion.span>
<motion.h1
variants={fadeUp}
className="h1-display text-nearle-dark mt-5"
>
Your Neighbourhood,
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
Your World
</span>
</motion.h1>
<motion.p
variants={fadeUp}
className="font-body text-nearle-mid text-lg leading-relaxed mt-6 max-w-xl mx-auto lg:mx-0"
>
India&apos;s futuristic hyperlocal ecosystem connecting residents
with groceries, restaurants, local stores, home services,
and trusted neighbourhood businesses.
</motion.p>
<motion.div
variants={fadeUp}
className="mt-9 flex flex-col sm:flex-row gap-3 justify-center lg:justify-start"
>
<Link
href="/#download"
className="inline-flex items-center justify-center gap-2 bg-purple-deep text-white rounded-full px-7 py-3 font-semibold shadow-cta hover:bg-purple-primary hover:shadow-cta-hover transition-all active:scale-95"
>
Download App
</Link>
<Link
href="/businesses"
className="inline-flex items-center justify-center gap-2 border-2 border-purple-deep text-purple-deep rounded-full px-7 py-3 font-semibold hover:bg-purple-deep hover:text-white transition-all active:scale-95"
>
Become a Partner
</Link>
</motion.div>
</motion.div>
<div className="lg:col-span-5 relative h-[440px] sm:h-[520px] lg:h-[560px]">
<HeroPhones3D />
</div>
</div>
</div>
)
}
function FloatingBlobs() {
return (
<div className="absolute inset-0 -z-0 pointer-events-none">
<motion.div
className="absolute top-[-10%] left-[-10%] w-[420px] h-[420px] rounded-full bg-purple-lavender opacity-60 blur-3xl"
animate={{ x: [0, 40, 0], y: [0, 25, 0] }}
transition={{ duration: 12, repeat: Infinity, ease: 'easeInOut' }}
/>
<motion.div
className="absolute bottom-[-15%] right-[-10%] w-[480px] h-[480px] rounded-full bg-purple-soft opacity-70 blur-3xl"
animate={{ x: [0, -30, 0], y: [0, -20, 0] }}
transition={{ duration: 14, repeat: Infinity, ease: 'easeInOut' }}
/>
<motion.div
className="absolute top-[30%] right-[10%] w-[260px] h-[260px] rounded-full bg-purple-primary/20 blur-3xl"
animate={{ x: [0, 20, 0], y: [0, 15, 0] }}
transition={{ duration: 10, repeat: Infinity, ease: 'easeInOut' }}
/>
</div>
)
}

View File

@@ -0,0 +1,246 @@
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
import { AnimatePresence, motion } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
const IsoMap3D = dynamic(() => import('@/components/three/IsoMap3D'), {
ssr: false,
loading: () => (
<div className="w-full h-full flex items-center justify-center bg-purple-soft/40 rounded-3xl">
<span className="font-mono text-xs text-purple-deep tracking-widest uppercase">
Loading 3D map
</span>
</div>
),
})
type DetailNode = {
id: string
label: string
emoji: string
category: 'Registered Business' | 'Active Restaurant' | 'Community Home' | 'Network Hub'
desc: string
metric: string
bullets: { icon: string; tone: string; text: string }[]
}
const DETAIL_BY_ID: Record<string, DetailNode> = {
'food-essentials': {
id: 'food-essentials',
label: 'Food Essentials Store',
emoji: '🛒',
category: 'Registered Business',
desc: 'Daily essentials, fruits, vegetables, and friendly service from local business.',
metric: 'Neighbourhood offers · Local assurance',
bullets: [
{ icon: '🛡️', tone: 'bg-[#ECFDF5] text-[#10B981]', text: 'Hyper-local catalog, updated by every shop daily' },
{ icon: '⚡', tone: 'bg-[#FFF7ED] text-[#F97316]', text: 'Live inventory from shops within walking distance' },
],
},
'shoppers': {
id: 'shoppers',
label: 'Local Shoppers',
emoji: '🛍️',
category: 'Community Home',
desc: 'Households nearby who order daily essentials and discover neighbourhood deals.',
metric: 'Active residents · Loyal community',
bullets: [
{ icon: '🛡️', tone: 'bg-[#ECFDF5] text-[#10B981]', text: 'Trusted shopper accounts verified by community' },
{ icon: '⚡', tone: 'bg-[#FFF7ED] text-[#F97316]', text: 'Personalised recommendations from local stores' },
],
},
'hot-food': {
id: 'hot-food',
label: 'Hot Food Partner',
emoji: '🍔',
category: 'Active Restaurant',
desc: 'Restaurants serving fresh meals to homes inside the neighbourhood radius.',
metric: 'Live kitchen · Fast last-mile',
bullets: [
{ icon: '🛡️', tone: 'bg-[#ECFDF5] text-[#10B981]', text: 'FSSAI compliant kitchens, live order pipeline' },
{ icon: '⚡', tone: 'bg-[#FFF7ED] text-[#F97316]', text: 'Average meal handover under 12 minutes' },
],
},
'community': {
id: 'community',
label: 'Community Homes',
emoji: '🏠',
category: 'Community Home',
desc: 'Residential pockets connected through one Nearle Deal household account.',
metric: 'Shared trust · Neighbourhood feed',
bullets: [
{ icon: '🛡️', tone: 'bg-[#ECFDF5] text-[#10B981]', text: 'Verified resident network with private community feed' },
{ icon: '⚡', tone: 'bg-[#FFF7ED] text-[#F97316]', text: 'Group orders unlock neighbourhood bulk pricing' },
],
},
'nearle-hood': {
id: 'nearle-hood',
label: 'Nearle Neighbourhood',
emoji: '🏘️',
category: 'Network Hub',
desc: 'The Nearle hyper-hub orchestrating routing, payments, and trust for the area.',
metric: 'Routing live · 100% uptime',
bullets: [
{ icon: '🛡️', tone: 'bg-[#ECFDF5] text-[#10B981]', text: 'Real-time delivery orchestration with safe payouts' },
{ icon: '⚡', tone: 'bg-[#FFF7ED] text-[#F97316]', text: 'Smart matching between residents, shops, and riders' },
],
},
}
export function NeighbourhoodMap() {
const [selectedId, setSelectedId] = useState<string>('food-essentials')
const selected = DETAIL_BY_ID[selectedId]!
return (
<div className="py-24 sm:py-32 bg-nearle-bgsoft px-5 sm:px-8 relative overflow-hidden">
<div className="absolute inset-0 bg-grid-soft opacity-30 pointer-events-none" />
<div className="max-w-7xl mx-auto">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6 mb-12">
<SectionHeader
label="Neighbourhood Commerce Map"
heading={
<>
Your neighbourhood,
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
fully connected
</span>
</>
}
sub="Explore how local businesses, homes, restaurants, and service providers connect inside one neighbourhood commerce platform."
align="left"
className="max-w-2xl"
/>
<div className="flex items-center gap-2 self-start lg:self-end bg-white border border-nearle-border rounded-full pl-2 pr-4 py-2 shadow-nearle-sm">
<span className="w-6 h-6 rounded-full bg-purple-primary" />
<span className="font-display font-extrabold text-nearle-dark text-[11px] sm:text-xs uppercase tracking-wider">
Connecting local business with the neighbourhood
</span>
</div>
</div>
<div className="grid lg:grid-cols-12 gap-8 items-stretch">
{/* DETAIL CARD (4 cols) */}
<div className="lg:col-span-4">
<GlassCard className="h-full flex flex-col border-purple-lavender/50 shadow-nearle-md">
<AnimatePresence mode="wait">
<motion.div
key={selected.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.25 }}
className="flex-1 flex flex-col"
>
<div className="flex items-start gap-3 pb-5 border-b border-nearle-border">
<div className="w-12 h-12 rounded-2xl bg-purple-soft border border-purple-lavender/50 flex items-center justify-center text-2xl">
{selected.emoji}
</div>
<div>
<div className="label-mono text-purple-primary">
{selected.category}
</div>
<h3 className="font-display font-extrabold text-nearle-dark text-xl leading-tight mt-1">
{selected.label}
</h3>
</div>
</div>
<div className="pt-5">
<div className="flex items-center gap-2 mb-3">
<span className="w-4 h-4 rounded-full bg-purple-soft flex items-center justify-center text-[9px] text-purple-deep font-bold">
i
</span>
<span className="label-mono text-nearle-dark">
Neighbourhood Details
</span>
</div>
<p className="font-body text-nearle-mid text-sm leading-relaxed">
{selected.desc}
</p>
</div>
<div className="mt-5 rounded-2xl bg-purple-soft/60 border border-purple-lavender/40 p-4">
<div className="label-mono text-purple-primary mb-1">
Node Metrics
</div>
<div className="flex items-baseline gap-2">
<span className="font-body text-xs text-nearle-mid leading-snug w-20 shrink-0">
Nearle<br />Activity
</span>
<span className="font-display font-bold text-nearle-dark text-sm leading-snug">
{selected.metric}
</span>
</div>
</div>
<div className="mt-5 space-y-3">
{selected.bullets.map((b, i) => (
<div key={i} className="flex items-start gap-3">
<div className={`w-8 h-8 rounded-xl flex items-center justify-center text-sm shrink-0 ${b.tone}`}>
{b.icon}
</div>
<p className="font-body text-sm text-nearle-dark leading-snug pt-1">
{b.text}
</p>
</div>
))}
</div>
</motion.div>
</AnimatePresence>
</GlassCard>
</div>
{/* 3D MAP CANVAS (8 cols) */}
<div className="lg:col-span-8 relative aspect-[4/3] sm:aspect-[16/11] bg-white rounded-3xl border border-nearle-border shadow-nearle-md overflow-hidden">
{/* Legend chips */}
<div className="absolute top-4 left-4 z-10 flex flex-col gap-2">
<LegendChip color="#683285" label="Food Essentials / Shops" />
<LegendChip color="#AE79BF" label="Active Restaurants" />
<LegendChip color="#3B82F6" label="People / Neighbourhoods" emoji="🏠" />
</div>
<IsoMap3D
selectedId={selectedId}
onSelect={(id) => setSelectedId(id)}
/>
</div>
</div>
</div>
</div>
)
}
function LegendChip({
color,
label,
emoji,
suffix,
}: {
color: string
label: string
emoji?: string
suffix?: string
}) {
return (
<div className="inline-flex items-center gap-2 bg-white border border-nearle-border rounded-full px-3 py-1.5 shadow-nearle-sm">
<span
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: color }}
/>
<span className="font-body text-[11px] font-semibold text-nearle-dark">
{label}
</span>
{suffix && (
<span className="font-body text-[11px] text-nearle-mid">
{emoji} {suffix}
</span>
)}
</div>
)
}

View File

@@ -0,0 +1,176 @@
'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { Badge } from '@/components/ui/Badge'
import { fadeUp, staggerContainer, viewportConfig } from '@/lib/animations'
type RoadmapStep = {
phase: string
title: string
desc: string
metrics: string[]
icon: string
status: 'completed' | 'current' | 'upcoming'
}
const ROADMAP: RoadmapStep[] = [
{
phase: 'Phase 01',
title: 'Society Geofencing',
desc: 'Cooperative societies sign partnership agreements. Geofences are digitally matching with strict boundary lines.',
metrics: ['99.8% Geofence Accuracy', 'Escrow Account Mappings', 'Resident Onboarding'],
icon: '🗺️',
status: 'completed',
},
{
phase: 'Phase 02',
title: 'Merchant Integration',
desc: 'Onboarding nearby grocery shops, premium restaurants, pharmacies, and trusted home service professionals.',
metrics: ['Catalog Editor Sync', 'Fast Merchant Approvals', 'API Device Activation'],
icon: '🏪',
status: 'current',
},
{
phase: 'Phase 03',
title: 'Fleet Calibration',
desc: 'Activating Nearle Gear delivery riders. Dispatch rules are optimized with real-time navigation tools.',
metrics: ['Optimal Path Training', 'Equipment Issuance', 'Payout System Verified'],
icon: '🛵',
status: 'upcoming',
},
{
phase: 'Phase 04',
title: 'Go-Live society Launch',
desc: 'Platform soft launch within the geofenced society limits accompanied by exclusive society discounts.',
metrics: ['Society Launch Event', 'Support Centers Active', '12-Minute Deliveries'],
icon: '🔮',
status: 'upcoming',
},
]
export function OperationalPlan() {
const [activeStep, setActiveStep] = useState<number>(1) // Default to active step (Phase 02)
return (
<div className="py-24 sm:py-32 bg-nearle-bgsoft px-5 sm:px-8 relative overflow-hidden">
{/* Decorative vectors */}
<div className="absolute inset-0 bg-grid-soft opacity-30 pointer-events-none" />
<div className="max-w-7xl mx-auto">
<SectionHeader
label="Execution Blueprint"
heading={
<>
Our Operational
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
Launch Roadmap
</span>
</>
}
sub="Watch how Nearle systematically activates cooperative societies and curates merchant networks."
align="center"
className="mb-16 sm:mb-20"
/>
<div className="grid lg:grid-cols-12 gap-8 items-stretch">
{/* Timeline side controls (5 cols) */}
<div className="lg:col-span-5 flex flex-col gap-4">
{ROADMAP.map((step, idx) => {
const isActive = activeStep === idx
return (
<button
key={step.phase}
onClick={() => setActiveStep(idx)}
className={`text-left p-5 rounded-2xl border transition-all duration-300 focus:outline-none flex items-center justify-between ${
isActive
? 'bg-purple-deep text-white border-purple-deep shadow-nearle-md'
: 'bg-white border-nearle-border hover:border-purple-primary text-nearle-dark'
}`}
>
<div className="flex items-center gap-4">
<span className="text-2xl">{step.icon}</span>
<div>
<span className={`text-[10px] font-mono font-bold tracking-widest ${isActive ? 'text-purple-lavender' : 'text-purple-primary'}`}>
{step.phase}
</span>
<h4 className="font-display font-bold text-base mt-0.5">
{step.title}
</h4>
</div>
</div>
{step.status === 'completed' && (
<span className={`text-[10px] font-mono uppercase font-bold tracking-wider px-2 py-0.5 rounded-full ${isActive ? 'bg-white/20 text-white' : 'bg-green-100 text-green-700'}`}>
Completed
</span>
)}
{step.status === 'current' && (
<span className={`text-[10px] font-mono uppercase font-bold tracking-wider px-2 py-0.5 rounded-full animate-pulse ${isActive ? 'bg-white/20 text-white' : 'bg-purple-lavender text-purple-deep'}`}>
Active
</span>
)}
</button>
)
})}
</div>
{/* Timeline detail container (7 cols) */}
<div className="lg:col-span-7">
<AnimatePresence mode="wait">
<motion.div
key={activeStep}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="h-full"
>
<GlassCard className="h-full p-8 flex flex-col justify-between border-purple-lavender/50 hover:border-purple-primary shadow-nearle-md">
<div>
<span className="text-xs font-mono font-bold text-purple-primary uppercase tracking-widest block mb-2">
Active Operational phase
</span>
<h3 className="h3-display text-nearle-dark mb-4">
{ROADMAP[activeStep]!.title}
</h3>
<p className="font-body text-nearle-mid text-sm leading-relaxed mb-8">
{ROADMAP[activeStep]!.desc}
</p>
<h4 className="font-display font-bold text-xs uppercase text-purple-deep mb-4 tracking-wider">
Key Deliverables & Metrics
</h4>
<div className="grid sm:grid-cols-2 gap-4">
{ROADMAP[activeStep]!.metrics.map((metric) => (
<div
key={metric}
className="flex items-center gap-3 p-4 bg-purple-soft/40 border border-purple-lavender/20 rounded-xl"
>
<span className="w-1.5 h-1.5 rounded-full bg-purple-primary" />
<span className="font-display font-bold text-xs text-nearle-dark">
{metric}
</span>
</div>
))}
</div>
</div>
<div className="mt-8 pt-6 border-t border-nearle-border flex justify-between items-center flex-wrap gap-4">
<p className="font-body text-xs text-nearle-light">
* Launch timings subject to merchant onboarding rate and society density geofencing tests.
</p>
</div>
</GlassCard>
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,358 @@
'use client'
import { useEffect, useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
type Node = {
icon: string
title: string
desc: string
metric: string
log: string
}
const NODES: Node[] = [
{
icon: '📱',
title: 'Customer Books',
desc: 'Resident places order via Nearle Deal app',
metric: 'Active Status',
log: '// Geofence matched. Nearest 3 stores pinged...',
},
{
icon: '🏪',
title: 'Store Receives',
desc: 'Nearest verified store gets instant notification',
metric: 'Verified Store',
log: '// Push notification delivered. ETA: 45s...',
},
{
icon: '✔️',
title: 'Store Confirms',
desc: 'Store accepts and begins packing the order',
metric: 'Live Packing',
log: '// Inventory verified. Packing initiated...',
},
{
icon: '💳',
title: 'Payment Processed',
desc: 'Secure digital settlement: 95% merchant, 5% fleet',
metric: 'Direct Settlement',
log: '// Escrow verified. Payout queued...',
},
{
icon: '🏍️',
title: 'Rider Picks Up',
desc: 'Gear rider collects and begins last-mile delivery',
metric: 'Route Optimized',
log: '// Route optimized. Rider en route...',
},
{
icon: '🏠',
title: 'Delivered',
desc: "Order reaches resident's door. Trust confirmed.",
metric: '< 30 min',
log: '// Delivery confirmed. Rating prompt sent...',
},
]
function TypewriterLog({ text }: { text: string }) {
const [displayed, setDisplayed] = useState('')
useEffect(() => {
setDisplayed('')
let idx = 0
const interval = setInterval(() => {
setDisplayed((d) => d + text.charAt(idx))
idx++
if (idx >= text.length) {
clearInterval(interval)
}
}, 20)
return () => clearInterval(interval)
}, [text])
return (
<span className="font-mono text-[11px] sm:text-xs flex items-center gap-1.5 text-emerald-400">
<span>{displayed}</span>
<span className="w-1.5 h-3.5 bg-emerald-400 inline-block shrink-0 animate-[blink_1s_infinite]" />
<style dangerouslySetInnerHTML={{ __html: `
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
`}} />
</span>
)
}
function DetailsCard({ activeNode, activeIndex }: { activeNode: Node; activeIndex: number }) {
const [rotate, setRotate] = useState({ x: 0, y: 0 })
const [shine, setShine] = useState({ x: 0, y: 0, opacity: 0 })
function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
const el = e.currentTarget
const r = el.getBoundingClientRect()
const x = e.clientX - r.left
const y = e.clientY - r.top
// Smooth angle calculations (tilt range -6 to 6 deg)
const rx = -((y / r.height) - 0.5) * 12
const ry = ((x / r.width) - 0.5) * 12
setRotate({ x: rx, y: ry })
setShine({ x, y, opacity: 0.15 })
}
function handleMouseLeave() {
setRotate({ x: 0, y: 0 })
setShine({ x: 0, y: 0, opacity: 0 })
}
return (
<motion.div
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{
transformStyle: 'preserve-3d',
transform: `perspective(1000px) rotateX(${rotate.x}deg) rotateY(${rotate.y}deg)`,
transition: 'transform 0.1s ease-out',
}}
className="relative bg-white rounded-3xl border border-nearle-border shadow-nearle-sm p-8 overflow-hidden select-none"
>
{/* Light shine overlay */}
<div
className="absolute pointer-events-none rounded-full"
style={{
width: '320px',
height: '320px',
background: 'radial-gradient(circle, rgba(164,103,183,0.15) 0%, rgba(255,255,255,0) 70%)',
left: `${shine.x - 160}px`,
top: `${shine.y - 160}px`,
opacity: shine.opacity,
transition: 'opacity 0.2s ease',
}}
/>
<div className="flex items-center justify-between flex-wrap gap-3" style={{ transform: 'translateZ(10px)' }}>
<span className="font-mono text-xs tracking-widest uppercase text-purple-primary">
Step 0{activeIndex + 1} of 06
</span>
<span className="bg-purple-soft text-purple-deep border border-purple-lavender/30 rounded-full px-3 py-1 font-mono text-[11px] tracking-widest uppercase shadow-nearle-xs">
{activeNode.metric}
</span>
</div>
<h3 className="h3-display text-nearle-dark mt-4" style={{ transform: 'translateZ(20px)' }}>
{activeNode.title}
</h3>
<p className="font-body text-nearle-mid mt-3 leading-relaxed" style={{ transform: 'translateZ(15px)' }}>
{activeNode.desc}
</p>
<div
className="mt-6 inline-flex items-center bg-[#0C0614] text-emerald-300 rounded-full px-5 py-2.5 font-mono text-xs shadow-nearle-sm border border-purple-deep/10"
style={{ transform: 'translateZ(25px)' }}
>
<TypewriterLog text={activeNode.log} />
</div>
</motion.div>
)
}
export function OrderLifecycle() {
const [active, setActive] = useState(0)
const [prevActive, setPrevActive] = useState(0)
const [tilt, setTilt] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setActive((a) => (a + 1) % NODES.length)
}, 4500)
return () => clearInterval(id)
}, [])
useEffect(() => {
if (active > prevActive) {
setTilt(12) // Lean forward
const t = setTimeout(() => setTilt(0), 650)
return () => clearTimeout(t)
} else if (active < prevActive) {
setTilt(-12) // Lean backward
const t = setTimeout(() => setTilt(0), 650)
return () => clearTimeout(t)
}
}, [active, prevActive])
// Track previous step to identify travel direction
useEffect(() => {
setPrevActive(active)
}, [active])
return (
<div className="bg-nearle-bgsoft py-24 sm:py-32 px-5 sm:px-8 relative overflow-hidden">
<div className="absolute inset-0 bg-grid-soft opacity-30 pointer-events-none" />
<div className="max-w-7xl mx-auto relative z-10">
<SectionHeader
label="Order Lifecycle"
heading="How your order travels"
sub="Watch the real order journey — the Nearle delivery chain"
align="center"
/>
<div className="mt-20 relative">
{/* Animated 3D/gliding road track for desktop views */}
<div className="hidden md:block absolute top-[36px] left-[8.33%] right-[8.33%] h-2 -translate-y-1/2">
<svg width="100%" height="8" className="overflow-visible">
<defs>
<linearGradient id="road-progress-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#683285" />
<stop offset="100%" stopColor="#A467B7" />
</linearGradient>
</defs>
{/* Ground Road Track */}
<line
x1="0"
y1="4"
x2="100%"
y2="4"
stroke="#F1EEF4"
strokeWidth="6"
strokeLinecap="round"
/>
{/* Dashlane inside road */}
<line
x1="0"
y1="4"
x2="100%"
y2="4"
stroke="#E2D6EC"
strokeWidth="2"
strokeDasharray="4 6"
/>
{/* Dynamic Fills Glowing Progress */}
<motion.line
x1="0"
y1="4"
x2={`${(active / 5) * 100}%`}
y2="4"
stroke="url(#road-progress-grad)"
strokeWidth="6"
strokeLinecap="round"
animate={{ x2: `${(active / 5) * 100}%` }}
transition={{ type: 'spring', stiffness: 90, damping: 16 }}
/>
</svg>
{/* Gliding scooter rider on road */}
<motion.div
className="absolute top-1/2 w-10 h-10 rounded-full bg-white border-2 border-purple-primary shadow-[0_4px_16px_rgba(104,50,133,0.22)] flex items-center justify-center text-xl z-20 overflow-hidden"
style={{
y: '-50%',
x: '-50%',
}}
animate={{
left: `${(active / 5) * 100}%`,
rotate: tilt,
}}
transition={{
left: { type: 'spring', stiffness: 90, damping: 16 },
rotate: { type: 'spring', stiffness: 100, damping: 10 },
}}
>
<AnimatePresence mode="wait">
<motion.span
key={active}
initial={{ scale: 0, rotate: -45, opacity: 0 }}
animate={{ scale: 1, rotate: 0, opacity: 1 }}
exit={{ scale: 0, rotate: 45, opacity: 0 }}
transition={{ duration: 0.22, ease: 'easeOut' }}
className="inline-block"
>
{active === 0 && '📱'}
{active === 1 && '🏪'}
{active === 2 && '📦'}
{active === 3 && '💳'}
{active === 4 && '🛵'}
{active === 5 && '🛵'}
</motion.span>
</AnimatePresence>
</motion.div>
</div>
<ol className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-y-10 gap-x-4 relative">
{NODES.map((node, i) => (
<li
key={node.title}
className="flex flex-col items-center text-center cursor-pointer group"
onClick={() => {
setPrevActive(active)
setActive(i)
}}
>
<div className="relative">
<motion.div
animate={{
scale: i === active ? 1.08 : 1,
y: i === active ? [0, -3, 0] : 0,
}}
transition={{
scale: { duration: 0.35 },
y: i === active ? { duration: 3, repeat: Infinity, ease: 'easeInOut' } : { duration: 0.3 },
}}
className={`relative z-10 w-18 h-18 rounded-full flex items-center justify-center text-2xl border-2 transition-all ${
i === active
? 'bg-purple-deep text-white border-purple-deep shadow-[0_8px_20px_rgba(104,50,133,0.25)]'
: i < active
? 'bg-purple-soft/90 text-purple-deep border-purple-primary/40 shadow-nearle-xs'
: 'bg-white/70 backdrop-blur-sm text-nearle-mid border-nearle-border hover:border-purple-primary hover:bg-white'
}`}
>
<span>{node.icon}</span>
</motion.div>
{i === active && (
<>
<motion.span
className="absolute inset-0 rounded-full border-2 border-purple-primary"
animate={{ scale: [1, 1.35], opacity: [0.6, 0] }}
transition={{ duration: 1.8, repeat: Infinity, ease: 'easeOut' }}
/>
<motion.span
className="absolute inset-0 rounded-full border-2 border-purple-primary"
animate={{ scale: [1, 1.6], opacity: [0.35, 0] }}
transition={{ duration: 1.8, repeat: Infinity, ease: 'easeOut', delay: 0.5 }}
/>
</>
)}
</div>
<span className="font-mono text-[9px] text-purple-primary tracking-widest uppercase mt-4 font-bold">
Step 0{i + 1}
</span>
<span className="font-display font-extrabold text-sm text-nearle-dark mt-1 group-hover:text-purple-primary transition-colors">
{node.title}
</span>
</li>
))}
</ol>
</div>
<div className="mt-16 max-w-3xl mx-auto">
<AnimatePresence mode="wait">
<motion.div
key={active}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.3 }}
>
<DetailsCard activeNode={NODES[active]!} activeIndex={active} />
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,94 @@
'use client'
import { motion } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { fadeUp, staggerContainer, viewportConfig } from '@/lib/animations'
type StatItem = {
value: string
label: string
desc: string
icon: string
}
const STATS: StatItem[] = [
{
value: '10,000+',
label: 'Active Residents',
desc: 'Empowering local communities with seamless smart living amenities.',
icon: '👥',
},
{
value: '5,000+',
label: 'Completed Deliveries',
desc: 'Completed in record time across Coimbatore societies.',
icon: '🛵',
},
{
value: '1,000+',
label: 'Verified Businesses',
desc: 'Directly supporting neighborhood shops and service providers.',
icon: '🏪',
},
{
value: '25+',
label: 'Connected Communities',
desc: 'Integrating diverse cooperative societies in Coimbatore.',
icon: '🏡',
},
]
export function Stats() {
return (
<div className="py-24 sm:py-32 bg-white px-5 sm:px-8 relative overflow-hidden">
{/* Decorative Blob */}
<div className="absolute top-1/4 right-0 w-[500px] h-[500px] rounded-full bg-purple-soft/40 blur-3xl -z-10 pointer-events-none" />
<div className="max-w-7xl mx-auto">
<SectionHeader
label="Proven Scale"
heading={
<>
Powering Cooperative
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
Living at Scale
</span>
</>
}
sub="Our growing network makes sure that residents get access to everything they need quickly and reliably."
align="center"
className="mb-16 sm:mb-20"
/>
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewportConfig}
className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-8"
>
{STATS.map((stat) => (
<motion.div key={stat.label} variants={fadeUp} className="group">
<GlassCard className="h-full flex flex-col justify-between border-purple-lavender/50 hover:border-purple-primary shadow-nearle-sm group-hover:shadow-nearle-md group-hover:scale-[1.02] p-8">
<div>
<span className="text-3xl block mb-4">{stat.icon}</span>
<p className="font-display font-extrabold text-4xl sm:text-5xl text-purple-deep tracking-tight bg-gradient-purple bg-clip-text text-transparent">
{stat.value}
</p>
<h4 className="font-display font-bold text-base text-nearle-dark mt-3">
{stat.label}
</h4>
</div>
<p className="font-body text-nearle-mid text-xs leading-relaxed mt-4 pt-4 border-t border-nearle-border/40">
{stat.desc}
</p>
</GlassCard>
</motion.div>
))}
</motion.div>
</div>
</div>
)
}

View File

@@ -0,0 +1,141 @@
'use client'
import { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { fadeUp, scaleUp, viewportConfig } from '@/lib/animations'
type Testimonial = {
quote: string
name: string
role: string
society: string
avatarText: string
rating: number
}
const TESTIMONIALS: Testimonial[] = [
{
quote: 'Nearle transformed our neighborhood shopping experience with incredibly fast local delivery. We get groceries and hot food from stores we trust in under 15 minutes!',
name: 'Aravind Swamy',
role: 'Resident Owner',
society: 'Sree Vatsa Residency',
avatarText: 'AS',
rating: 5,
},
{
quote: 'As a local store owner, Nearle has doubled our sales by connecting us directly to nearby societies. The payouts are instant, and the custom catalog builder is so easy to use.',
name: 'Kalyan Kumar',
role: 'Merchant Partner',
society: 'Kalyan Grocers · RS Puram',
avatarText: 'KK',
rating: 5,
},
{
quote: 'The route optimization in the Nearle Gear app is incredible. I complete drops quickly, and the escrow payout structure ensures I get paid fairly for every single delivery.',
name: 'Karthik Raja',
role: 'Gear Delivery Rider',
society: 'RS Puram Hub',
avatarText: 'KR',
rating: 5,
},
]
export function Testimonials() {
const [active, setActive] = useState<number>(0)
useEffect(() => {
const id = setInterval(() => {
setActive((prev) => (prev + 1) % TESTIMONIALS.length)
}, 6000)
return () => clearInterval(id)
}, [])
return (
<div className="py-24 sm:py-32 bg-white px-5 sm:px-8 relative overflow-hidden">
{/* Decorative Blob */}
<div className="absolute top-1/3 left-1/3 w-[500px] h-[500px] rounded-full bg-purple-soft/40 blur-3xl -z-10 pointer-events-none" />
<div className="max-w-7xl mx-auto">
<SectionHeader
label="Community Voices"
heading={
<>
Trusted by Your
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
Active Neighbourhood
</span>
</>
}
sub="Hear what residents, local merchant partners, and our delivery riders say about the Nearle platform."
align="center"
className="mb-16 sm:mb-20"
/>
<div className="max-w-3xl mx-auto relative min-h-[320px]">
<AnimatePresence mode="wait">
<motion.div
key={active}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.4 }}
>
<GlassCard className="border-purple-lavender/50 hover:border-purple-primary shadow-nearle-md p-8 sm:p-12 relative overflow-hidden">
{/* Visual Quote Icon Decor */}
<span className="absolute -top-6 -right-6 text-9xl text-purple-soft/40 font-serif select-none pointer-events-none">
</span>
<div className="flex gap-1.5 mb-6 text-purple-primary text-xl">
{Array.from({ length: TESTIMONIALS[active]!.rating }).map((_, i) => (
<span key={i}></span>
))}
</div>
<p className="font-body text-nearle-dark text-lg sm:text-xl leading-relaxed italic relative z-10">
&ldquo;{TESTIMONIALS[active]!.quote}&rdquo;
</p>
<div className="mt-8 pt-6 border-t border-nearle-border/40 flex items-center gap-4">
{/* Custom Avatar Icon */}
<div className="w-12 h-12 rounded-full bg-purple-deep flex items-center justify-center font-display font-extrabold text-white text-sm shadow-nearle-sm">
{TESTIMONIALS[active]!.avatarText}
</div>
<div className="flex-1">
<h4 className="font-display font-bold text-sm text-nearle-dark flex items-center gap-2">
<span>{TESTIMONIALS[active]!.name}</span>
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full bg-green-500/15 text-green-700 font-mono text-[8px] font-bold uppercase tracking-wider">
Verified
</span>
</h4>
<p className="font-body text-xs text-nearle-mid mt-0.5">
{TESTIMONIALS[active]!.role} · {TESTIMONIALS[active]!.society}
</p>
</div>
</div>
</GlassCard>
</motion.div>
</AnimatePresence>
{/* Carousel dots indicators */}
<div className="flex justify-center gap-2 mt-8">
{TESTIMONIALS.map((_, idx) => (
<button
key={idx}
onClick={() => setActive(idx)}
className={`w-2.5 h-2.5 rounded-full transition-all duration-300 ${
active === idx ? 'bg-purple-deep w-6' : 'bg-purple-lavender hover:bg-purple-primary'
}`}
aria-label={`Go to slide ${idx + 1}`}
/>
))}
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,122 @@
'use client'
import { motion } from 'framer-motion'
import { SectionHeader } from '@/components/ui/SectionHeader'
import { GlassCard } from '@/components/ui/GlassCard'
import { fadeUp, staggerContainer, viewportConfig } from '@/lib/animations'
type FeatureItem = {
title: string
desc: string
icon: string
color: string
}
const FEATURES: FeatureItem[] = [
{
title: 'Grocery Delivery',
desc: 'Daily essentials, fresh fruits, vegetables, and staples from your preferred local marts.',
icon: '🥬',
color: 'from-green-500/10 to-emerald-500/5',
},
{
title: 'Restaurants & Takeaways',
desc: 'Order hot, freshly prepared dishes from iconic neighbourhood diners and restaurants.',
icon: '🍜',
color: 'from-orange-500/10 to-red-500/5',
},
{
title: 'Home Services',
desc: 'Verified local technicians, electricians, plumbers, and home cleaners at your command.',
icon: '🧹',
color: 'from-blue-500/10 to-indigo-500/5',
},
{
title: 'Community Deals',
desc: 'Unlock special bulk pricing and exclusive discounts negotiated collectively by your local society.',
icon: '🤝',
color: 'from-purple-500/10 to-pink-500/5',
},
{
title: 'Local Marketplace',
desc: 'Discover unique handmade items, homegrown brands, and direct artisan offerings in your area.',
icon: '🏪',
color: 'from-amber-500/10 to-yellow-500/5',
},
{
title: 'Instant Delivery',
desc: 'Need something instantly? Local courier service to send or receive packages in under 20 minutes.',
icon: '⚡',
color: 'from-rose-500/10 to-orange-500/5',
},
{
title: 'Trusted Neighborhood Businesses',
desc: 'Strengthening local economy. Directly support the small businesses you know and trust.',
icon: '💜',
color: 'from-purple-primary/10 to-purple-accent/5',
},
]
export function ValueEcosystem() {
return (
<div className="py-24 sm:py-32 bg-white px-5 sm:px-8 relative overflow-hidden">
{/* Background Decorative Mesh */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-[800px] h-[800px] rounded-full bg-purple-soft/40 blur-3xl -z-10 pointer-events-none" />
<div className="max-w-7xl mx-auto">
<SectionHeader
label="Value Ecosystem"
heading={
<>
Everything you need,
<br />
<span className="bg-gradient-purple bg-clip-text text-transparent">
right next door
</span>
</>
}
sub="Nearle integrates essential daily activities into one powerful, cohesive neighbourhood dashboard."
align="center"
className="mb-16 sm:mb-20"
/>
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewportConfig}
className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-8"
>
{FEATURES.map((feat) => (
<motion.div key={feat.title} variants={fadeUp} className="group">
<GlassCard className="h-full flex flex-col items-start relative overflow-hidden transition-all duration-300 group-hover:scale-[1.02] group-hover:shadow-nearle-lg border-purple-lavender/50 hover:border-purple-primary">
{/* Accent Background Glow on Hover */}
<div
className={`absolute inset-0 bg-gradient-to-br ${feat.color} opacity-0 group-hover:opacity-100 transition-opacity duration-500 -z-10`}
/>
<div className="w-12 h-12 rounded-2xl bg-white border border-nearle-border flex items-center justify-center text-2xl shadow-sm transition-all duration-300 group-hover:border-purple-primary group-hover:shadow-nearle-sm group-hover:scale-110">
{feat.icon}
</div>
<h3 className="font-display font-bold text-nearle-dark text-lg mt-6 group-hover:text-purple-deep transition-colors duration-200">
{feat.title}
</h3>
<p className="font-body text-nearle-mid text-sm mt-3 leading-relaxed">
{feat.desc}
</p>
{/* Subtle Indicator */}
<div className="mt-auto pt-6 flex items-center gap-2 text-xs font-mono font-bold uppercase tracking-wider text-purple-primary opacity-0 translate-x-[-10px] group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300">
<span>Explore</span>
<span></span>
</div>
</GlassCard>
</motion.div>
))}
</motion.div>
</div>
</div>
)
}

View File

@@ -0,0 +1,93 @@
import Link from 'next/link'
import { Instagram, Twitter, Linkedin, Youtube } from 'lucide-react'
import { NearleLogo } from '@/components/ui/NearleLogo'
const company = [
{ label: 'About Us', href: '/about' },
{ label: 'Communities', href: '/communities' },
{ label: 'Blog', href: '#' },
{ label: 'Careers', href: '#' },
]
const support = [
{ label: 'FAQ', href: '/#faq' },
{ label: 'Contact Us', href: '/contact' },
{ label: 'Privacy Policy', href: '#' },
{ label: 'Terms & Conditions', href: '#' },
]
export function Footer() {
return (
<footer className="bg-[#111827] text-white">
<div className="max-w-7xl mx-auto px-5 sm:px-8 pt-16 pb-10 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10">
<div>
<div className="flex items-center gap-2">
<NearleLogo size={36} fill="#1f2937" stroke="#E7D3EF" />
<span className="font-display font-extrabold text-white text-2xl tracking-tight">
Nearle
</span>
</div>
<p className="font-body text-nearle-light mt-4 max-w-xs">
Your Neighbourhood, Your World.
</p>
<div className="flex items-center gap-4 mt-6 text-nearle-light">
<a aria-label="Instagram" href="#" className="hover:text-purple-primary transition">
<Instagram size={20} />
</a>
<a aria-label="Twitter / X" href="#" className="hover:text-purple-primary transition">
<Twitter size={20} />
</a>
<a aria-label="LinkedIn" href="#" className="hover:text-purple-primary transition">
<Linkedin size={20} />
</a>
<a aria-label="YouTube" href="#" className="hover:text-purple-primary transition">
<Youtube size={20} />
</a>
</div>
</div>
<FooterCol title="Company" links={company} />
<FooterCol title="Support" links={support} />
</div>
<div className="bg-[#0D1117]">
<div className="max-w-7xl mx-auto px-5 sm:px-8 py-5 flex flex-col sm:flex-row items-center justify-between gap-2">
<p className="text-sm text-[#475569] font-body">
© 2024 Nearle Technology Private Limited. All rights reserved.
</p>
<p className="text-sm text-[#475569] font-body">
Made with <span className="text-purple-primary"></span> in Coimbatore, India
</p>
</div>
</div>
</footer>
)
}
function FooterCol({
title,
links,
}: {
title: string
links: { label: string; href: string }[]
}) {
return (
<div>
<h4 className="font-mono uppercase text-[#94A3B8] text-xs tracking-widest">
{title}
</h4>
<ul className="mt-4 flex flex-col gap-3">
{links.map((l) => (
<li key={l.label}>
<Link
href={l.href}
className="text-nearle-light hover:text-white transition text-[15px]"
>
{l.label}
</Link>
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,166 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useEffect, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { Menu, X } from 'lucide-react'
import { NearleLogo } from '@/components/ui/NearleLogo'
import { useScrollSpy } from '@/lib/useScrollSpy'
type NavItem = {
label: string
href: string
sectionId?: string
}
const HOME_SECTIONS = [
'home',
'features',
'map',
'download',
'contact',
]
const NAV: NavItem[] = [
{ label: 'Home', href: '/', sectionId: 'home' },
{ label: 'About', href: '/about' },
{ label: 'Communities', href: '/communities', sectionId: 'map' },
{ label: 'Businesses', href: '/businesses' },
{ label: 'Download', href: '/#download', sectionId: 'download' },
{ label: 'Contact', href: '/contact', sectionId: 'contact' },
]
export function Navbar() {
const pathname = usePathname()
const [scrolled, setScrolled] = useState(false)
const [open, setOpen] = useState(false)
const activeSection = useScrollSpy(HOME_SECTIONS)
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 60)
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
useEffect(() => {
setOpen(false)
}, [pathname])
const isActive = (item: NavItem) => {
if (item.href.startsWith('/#')) {
if (pathname !== '/') return false
return activeSection === item.sectionId
}
if (item.href === '/') {
return (
pathname === '/' &&
(activeSection === 'home' || activeSection === null)
)
}
return pathname === item.href || pathname.startsWith(item.href + '/')
}
return (
<header
className={`fixed top-0 inset-x-0 z-50 transition-all duration-400 ${
scrolled
? 'bg-white/85 backdrop-blur-xl border-b border-purple-lavender shadow-nearle-sm'
: 'bg-transparent border-b border-transparent'
}`}
>
<nav className="max-w-7xl mx-auto px-5 sm:px-8 h-14 md:h-16 flex items-center justify-between">
<Link href="/" className="flex items-center gap-2 group">
<NearleLogo size={32} />
<span className="font-display font-extrabold text-purple-deep text-xl tracking-tight">
Nearle
</span>
</Link>
<ul className="hidden lg:flex items-center gap-1">
{NAV.map((item) => {
const active = isActive(item)
return (
<li key={item.label}>
<Link
href={item.href}
className={`px-4 py-1.5 rounded-full text-sm transition-all duration-200 ${
active
? 'bg-purple-lavender text-purple-deep font-semibold'
: 'text-nearle-mid hover:text-purple-deep'
}`}
>
{item.label}
</Link>
</li>
)
})}
</ul>
<div className="hidden lg:flex items-center gap-3">
<Link
href="/#download"
className="bg-purple-deep text-white rounded-full px-5 py-2 font-semibold text-sm hover:bg-purple-primary shadow-cta hover:shadow-cta-hover transition-all"
>
Get Ready Now
</Link>
</div>
<button
aria-label="Toggle menu"
onClick={() => setOpen((v) => !v)}
className="lg:hidden inline-flex items-center justify-center w-10 h-10 rounded-full text-purple-deep hover:bg-purple-soft transition"
>
{open ? <X size={22} /> : <Menu size={22} />}
</button>
</nav>
<AnimatePresence>
{open && (
<motion.div
key="mobile-drawer"
initial={{ opacity: 0, y: -12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.25 }}
className="lg:hidden bg-white border-b border-purple-lavender shadow-nearle-sm"
>
<ul className="max-w-7xl mx-auto px-5 py-4 flex flex-col gap-1">
{NAV.map((item, i) => {
const active = isActive(item)
return (
<motion.li
key={item.label}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.04 }}
>
<Link
href={item.href}
className={`block px-4 py-3 rounded-xl text-base transition-all ${
active
? 'bg-purple-lavender text-purple-deep font-semibold'
: 'text-nearle-mid hover:bg-purple-soft hover:text-purple-deep'
}`}
>
{item.label}
</Link>
</motion.li>
)
})}
<li className="pt-3">
<Link
href="/#download"
className="block text-center bg-purple-deep text-white rounded-full px-5 py-3 font-semibold hover:bg-purple-primary shadow-cta transition-all"
>
Get Ready Now
</Link>
</li>
</ul>
</motion.div>
)}
</AnimatePresence>
</header>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,327 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { motion } from 'framer-motion'
const DEEP = '#683285'
const PRIMARY = '#A467B7'
type ScreenProps = { variant: 'map' | 'home' | 'chat' }
function PhoneScreen({ variant }: ScreenProps) {
if (variant === 'map') {
return (
<div className="w-full h-full rounded-[28px] bg-white overflow-hidden flex flex-col p-3 font-body select-none">
<style dangerouslySetInnerHTML={{ __html: `
@keyframes ride-along-path {
0% {
left: 36.3%;
top: 18.4%;
}
14.29% {
left: 38.0%;
top: 27.8%;
}
28.57% {
left: 40.5%;
top: 37.2%;
}
42.86% {
left: 43.0%;
top: 46.6%;
}
57.14% {
left: 46.0%;
top: 56.0%;
}
71.43% {
left: 49.0%;
top: 65.4%;
}
85.71% {
left: 52.0%;
top: 74.8%;
}
100% {
left: 54.5%;
top: 84.2%;
}
}
.scooter-rider {
transform: translate(-50%, -50%);
animation: ride-along-path 9s linear infinite;
}
`}} />
<div className="flex items-start justify-between rounded-2xl border border-[#E7D3EF] px-3 py-2 mb-3">
<div>
<div className="text-[9px] tracking-[0.18em] font-mono font-bold text-[#683285]">NEARLE</div>
<div className="text-[10px] text-[#94A3B8]">Neighbourhood app</div>
</div>
<span className="text-[8px] font-bold bg-[#F5EEF8] text-[#683285] rounded-full px-2 py-0.5">
FASTEST
</span>
</div>
<div className="flex-1 relative rounded-2xl bg-[#FAFAFB] border border-[#EEF1F5] overflow-hidden">
<svg className="absolute inset-0 w-full h-full" viewBox="0 0 220 380" preserveAspectRatio="none">
{[40, 80, 120, 160, 200, 240, 280, 320].map((y) => (
<line key={`h${y}`} x1="0" y1={y} x2="220" y2={y} stroke="#E5E7EB" strokeWidth="0.5" />
))}
{[40, 80, 120, 160].map((x) => (
<line key={`v${x}`} x1={x} y1="0" x2={x} y2="380" stroke="#E5E7EB" strokeWidth="0.5" />
))}
<rect x="30" y="60" width="40" height="30" rx="4" fill="#F1F0F4" />
<rect x="90" y="90" width="50" height="40" rx="4" fill="#F1F0F4" />
<rect x="160" y="60" width="40" height="30" rx="4" fill="#F1F0F4" />
<rect x="30" y="160" width="50" height="40" rx="4" fill="#F1F0F4" />
<rect x="160" y="200" width="40" height="40" rx="4" fill="#F1F0F4" />
<rect x="30" y="260" width="40" height="40" rx="4" fill="#F1F0F4" />
<path d="M 80 70 C 90 110, 95 160, 110 220 S 115 290, 120 320" stroke={PRIMARY} strokeWidth="1.5" strokeDasharray="3 3" fill="none" />
</svg>
<div className="absolute top-[14%] left-[14%] bg-white text-[9px] font-bold text-[#111827] rounded-md px-1.5 py-0.5 shadow-sm border border-[#EEE]">
Food Essentials
</div>
<div className="absolute w-6 h-6 rounded-full bg-[#683285] border-2 border-white flex items-center justify-center text-[10px] shadow scooter-rider">🛵</div>
<div className="absolute bottom-[12%] left-[44%] bg-white text-[9px] font-bold text-[#111827] rounded-md px-1.5 py-0.5 shadow-sm border border-[#EEE]">
Your Home
</div>
</div>
<div className="mt-3 flex gap-2">
<div className="flex-1 rounded-xl border border-[#EEF1F5] px-2 py-1.5">
<div className="text-[8px] uppercase tracking-wider text-[#94A3B8] font-mono">Local Shopping</div>
<div className="text-[12px] font-extrabold text-[#683285]">Safe &amp; Easy</div>
</div>
<div className="flex-1 rounded-xl border border-[#EEF1F5] px-2 py-1.5">
<div className="text-[8px] uppercase tracking-wider text-[#94A3B8] font-mono">Fulfilled by</div>
<div className="text-[12px] font-extrabold text-[#111827]">Local Business</div>
</div>
</div>
</div>
)
}
if (variant === 'home') {
return (
<div className="w-full h-full rounded-[28px] bg-white overflow-hidden flex flex-col p-3 font-body select-none">
<div className="flex items-center justify-between mb-2">
<div className="font-display font-extrabold text-[#111827] text-[15px]">Nearle.</div>
<div className="flex gap-1.5">
<div className="w-5 h-5 rounded-full bg-[#F5EEF8] flex items-center justify-center text-[9px]">🛒</div>
<div className="w-5 h-5 rounded-full bg-[#E7D3EF] text-[#683285] flex items-center justify-center text-[8px] font-bold">A</div>
</div>
</div>
<div className="rounded-2xl p-3" style={{ background: `linear-gradient(135deg, ${DEEP} 0%, ${PRIMARY} 100%)` }}>
<div className="flex items-start justify-between">
<div>
<div className="text-[8px] tracking-[0.16em] font-mono text-white/80 font-bold">NEARLE APP</div>
<div className="text-white font-display font-extrabold text-[15px] mt-0.5 leading-tight">All needs nearby</div>
</div>
<div className="w-6 h-4 rounded bg-white/15 flex items-center justify-center text-[10px]">💳</div>
</div>
<div className="flex items-end justify-between mt-3 pt-2 border-t border-white/15">
<div className="text-[7px] tracking-[0.14em] font-mono text-white/85 font-bold">NEIGHBOURHOOD LIVING</div>
<div className="text-[7px] tracking-[0.14em] font-mono text-white/55 font-bold">LOCAL TRUST</div>
</div>
</div>
<div className="text-[8px] tracking-[0.16em] font-mono font-bold text-[#94A3B8] mt-3 mb-2">DISCOVER NEARLE</div>
{[
{ icon: '🥐', t: 'Food Hot Food', s: 'Restaurants, take aways', cta: 'ORDER', tone: 'purple' },
{ icon: '🥑', t: 'Food Essentials', s: 'Fruits and essentials instantly', cta: 'ORDER', tone: 'purple' },
].map((row) => (
<div key={row.t} className="flex items-center gap-2 rounded-xl border border-[#EEF1F5] px-2 py-1.5 mb-1.5">
<div className="w-6 h-6 rounded-md bg-[#F8FAFC] flex items-center justify-center text-[12px]">{row.icon}</div>
<div className="flex-1 min-w-0">
<div className="text-[10px] font-extrabold text-[#111827] truncate">{row.t}</div>
<div className="text-[8px] text-[#94A3B8] truncate">{row.s}</div>
</div>
<div
className={`text-[8px] font-extrabold rounded-md px-1.5 py-0.5 ${
row.tone === 'blue' ? 'bg-[#E0F2FE] text-[#0284C7]' : 'bg-[#F5EEF8] text-[#683285]'
}`}
>
{row.cta}
</div>
</div>
))}
<div className="mt-auto flex items-center justify-around pt-2 rounded-xl bg-[#F5EEF8]/70">
<div className="flex flex-col items-center text-[#683285]">
<span className="text-[12px]">🏠</span>
<span className="text-[7px] font-bold mt-0.5">Home</span>
</div>
<div className="flex flex-col items-center text-[#94A3B8]">
<span className="text-[12px]">🛵</span>
<span className="text-[7px] font-bold mt-0.5">Tracking</span>
</div>
<div className="flex flex-col items-center text-[#94A3B8]">
<span className="text-[12px]">🛍</span>
<span className="text-[7px] font-bold mt-0.5">Local</span>
</div>
</div>
</div>
)
}
// chat
return (
<div className="w-full h-full rounded-[28px] bg-white overflow-hidden flex flex-col p-3 font-body select-none">
<div className="flex items-center gap-2 rounded-xl border border-[#EEF1F5] p-2 mb-2">
<div className="w-6 h-6 rounded-full bg-[#FFEEC2] flex items-center justify-center text-[10px]">💎</div>
<div className="flex-1 min-w-0">
<div className="text-[10px] font-extrabold text-[#111827]">Nearle Community</div>
<div className="text-[8px] text-[#94A3B8]">Neighbourhood active</div>
</div>
</div>
<div className="flex-1 flex flex-col gap-2 overflow-hidden">
<div className="self-start max-w-[80%] rounded-xl rounded-tl-sm bg-[#F1F5F9] px-2 py-1.5">
<div className="text-[8px] font-bold text-[#475569]">Neighbourhood</div>
<div className="text-[9px] text-[#475569]">Pick hot deals from neighbourhood</div>
</div>
<div className="self-end max-w-[85%] rounded-xl rounded-tr-sm bg-[#F5EEF8] px-2 py-1.5">
<div className="text-[8px] font-bold text-[#683285] text-right">You</div>
<div className="text-[9px] text-[#683285]">Get all needs from local store to home instantly.</div>
</div>
<div className="self-start max-w-[80%] rounded-xl rounded-tl-sm bg-[#F1F5F9] px-2 py-1.5">
<div className="text-[8px] font-bold text-[#0284C7]">Local Business</div>
<div className="text-[9px] text-[#475569]">Order received packing your items now.</div>
</div>
</div>
<div className="mt-2 rounded-2xl border border-[#D1FAE5] bg-[#ECFDF5] py-2 px-3 text-center">
<div className="text-[7px] tracking-[0.16em] font-mono font-bold text-[#10B981]">GET READY NOW</div>
<div className="text-[12px] font-extrabold text-[#111827] mt-0.5">Download Nearle</div>
</div>
</div>
)
}
function Phone({
variant,
rotateY,
translateX,
translateY,
translateZ,
delay,
}: {
variant: ScreenProps['variant']
rotateY: number
translateX: number
translateY: number
translateZ: number
delay: number
}) {
return (
<div
style={{
transformStyle: 'preserve-3d',
transform: `translate3d(${translateX}px, ${translateY}px, ${translateZ}px) rotateY(${rotateY}deg) translate(-50%, -50%)`,
}}
className="absolute top-1/2 left-1/2"
>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay }}
style={{ transformStyle: 'preserve-3d' }}
>
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{
duration: 5 + Math.abs(rotateY) * 0.05,
repeat: Infinity,
ease: 'easeInOut',
delay,
}}
className="relative"
style={{
width: 198,
height: 396,
filter: 'drop-shadow(0 30px 50px rgba(104,50,133,0.18))',
}}
>
{/* Phone frame */}
<div
className="absolute inset-0 rounded-[34px] bg-white"
style={{
border: '1.5px solid #DDD8E5',
boxShadow: 'inset 0 0 0 6px #F5EEF8, 0 16px 40px -10px rgba(104,50,133,0.20)',
}}
/>
{/* Notch */}
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-14 h-2 rounded-full bg-[#1F1B2E]/85 z-10" />
{/* Screen */}
<div className="absolute inset-[10px] rounded-[26px] overflow-hidden bg-white">
<PhoneScreen variant={variant} />
</div>
</motion.div>
</motion.div>
</div>
)
}
export default function HeroPhones3D() {
const ref = useRef<HTMLDivElement>(null)
const [tilt, setTilt] = useState({ x: 0, y: 0 })
useEffect(() => {
const el = ref.current
if (!el) return
function onMove(e: MouseEvent) {
if (!el) return
const r = el.getBoundingClientRect()
const nx = (e.clientX - r.left) / r.width - 0.5
const ny = (e.clientY - r.top) / r.height - 0.5
setTilt({ x: nx, y: ny })
}
function onLeave() {
setTilt({ x: 0, y: 0 })
}
el.addEventListener('mousemove', onMove)
el.addEventListener('mouseleave', onLeave)
return () => {
el.removeEventListener('mousemove', onMove)
el.removeEventListener('mouseleave', onLeave)
}
}, [])
return (
<div
ref={ref}
className="relative w-full h-full"
style={{ perspective: '1400px' }}
>
{/* Top-left tag */}
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="absolute top-2 left-2 z-20 flex items-center gap-2 bg-white border border-[#E7D3EF] rounded-full pl-2 pr-3 py-1.5 shadow-[0_4px_16px_rgba(164,103,183,0.10)]"
>
<span className="w-5 h-5 rounded-full bg-[#F5EEF8] flex items-center justify-center text-[10px]">🏅</span>
<span className="font-display font-extrabold text-[#111827] text-[11px] leading-none">
Convenient &amp; safe shopping locally
</span>
</motion.div>
{/* Phones stage */}
<div
className="absolute inset-0"
style={{
transformStyle: 'preserve-3d',
transform: `rotateY(${tilt.x * 6}deg) rotateX(${-tilt.y * 4}deg)`,
transition: 'transform 0.25s ease-out',
}}
>
<Phone variant="map" rotateY={16} translateX={-220} translateY={10} translateZ={-50} delay={0.05} />
<Phone variant="home" rotateY={0} translateX={0} translateY={-10} translateZ={40} delay={0.18} />
<Phone variant="chat" rotateY={-16} translateX={220} translateY={10} translateZ={-50} delay={0.3} />
</div>
</div>
)
}

View File

@@ -0,0 +1,246 @@
'use client'
import { motion, AnimatePresence } from 'framer-motion'
type IsoNode = {
id: string
label: string
emoji: string
kind: 'shop' | 'home' | 'restaurant' | 'hub'
x: number // X coordinate as percentage (0-100)
y: number // Y coordinate as percentage (0-100)
}
const NODES: IsoNode[] = [
{ id: 'food-essentials', label: 'Food Essentials Store', emoji: '🛒', kind: 'shop', x: 80, y: 22 },
{ id: 'shoppers', label: 'Local Shoppers', emoji: '🛍️', kind: 'home', x: 84, y: 54 },
{ id: 'hot-food', label: 'Hot Food Partner', emoji: '🍔', kind: 'restaurant', x: 74, y: 80 },
{ id: 'community', label: 'Community Homes', emoji: '🏠', kind: 'home', x: 28, y: 78 },
{ id: 'nearle-hood', label: 'Nearle Neighbourhood', emoji: '🏘️', kind: 'hub', x: 50, y: 50 },
]
function getCurvePath(id: string): string {
switch (id) {
case 'food-essentials':
return 'M 50 50 C 60 40, 70 30, 80 22'
case 'shoppers':
return 'M 50 50 C 62 50, 72 52, 84 54'
case 'hot-food':
return 'M 50 50 C 58 62, 66 70, 74 80'
case 'community':
return 'M 50 50 C 42 62, 36 70, 28 78'
default:
return ''
}
}
export default function IsoMap3D({
onSelect,
selectedId,
}: {
onSelect: (id: string, label: string) => void
selectedId: string
}) {
const scooterTarget = NODES.find((n) => n.id === selectedId)
return (
<div className="relative w-full h-full bg-[#FAF9FC] overflow-hidden select-none select-none">
<style dangerouslySetInnerHTML={{ __html: `
@keyframes flow {
to {
stroke-dashoffset: -16;
}
}
`}} />
{/* Mesh dot pattern background */}
<div className="absolute inset-0 bg-[radial-gradient(#E8E5EE_1.5px,transparent_1.5px)] [background-size:24px_24px] opacity-75 pointer-events-none" />
{/* Connection street layer */}
<div className="absolute inset-0 pointer-events-none z-0">
<svg width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none" className="overflow-visible">
<defs>
<linearGradient id="map-progress-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#683285" />
<stop offset="100%" stopColor="#A467B7" />
</linearGradient>
</defs>
{/* Background ground road tracks — always visible for ALL nodes */}
{NODES.filter((n) => n.id !== 'nearle-hood').map((node) => {
const path = getCurvePath(node.id)
return (
<g key={`road-${node.id}`}>
{/* Thick soft background road */}
<path
d={path}
fill="none"
stroke="#EDE5F2"
strokeWidth="2.0"
strokeLinecap="round"
/>
{/* Dashed lane marker */}
<path
d={path}
fill="none"
stroke="#D9CCE3"
strokeWidth="0.6"
strokeLinecap="round"
strokeDasharray="2 2.5"
/>
</g>
)
})}
{/* Glowing active order flow tracks */}
{NODES.filter((n) => n.id !== 'nearle-hood').map((node) => {
const isActive = selectedId === node.id
const path = getCurvePath(node.id)
if (!isActive) return null
return (
<g key={`active-road-${node.id}`}>
{/* Glow back-halo */}
<path
d={path}
fill="none"
stroke="#E7D3EF"
strokeWidth="2.4"
strokeLinecap="round"
className="opacity-45"
/>
{/* Animated dash line */}
<motion.path
d={path}
fill="none"
stroke="url(#map-progress-grad)"
strokeWidth="1.2"
strokeLinecap="round"
strokeDasharray="1.5 1.5"
className="animate-[flow_1.8s_linear_infinite]"
/>
</g>
)
})}
</svg>
</div>
{/* Gliding Dispatch Scooter — slowly travels FROM store TO hub, repeating */}
{scooterTarget && selectedId !== 'nearle-hood' && (
<motion.div
key={selectedId}
className="absolute w-9 h-9 rounded-full bg-white border-2 border-purple-primary shadow-[0_4px_16px_rgba(104,50,133,0.25)] flex items-center justify-center text-lg z-30 pointer-events-none"
style={{
x: '-50%',
y: '-50%',
}}
animate={{
left: [`${scooterTarget.x}%`, '50%'],
top: [`${scooterTarget.y}%`, '50%'],
}}
transition={{
duration: 6,
ease: 'linear',
repeat: Infinity,
repeatDelay: 0.8,
}}
>
🛵
</motion.div>
)}
{/* Node Cards Layer */}
<div className="absolute inset-0 z-10">
{NODES.map((node) => {
const isActive = selectedId === node.id
return (
<div
key={node.id}
className="absolute"
style={{
left: `${node.x}%`,
top: `${node.y}%`,
transform: 'translate(-50%, -50%)',
zIndex: isActive ? 40 : 20,
}}
>
{/* Floating Interactive Node Bubble/Card */}
<motion.div
onClick={() => onSelect(node.id, node.label)}
whileHover={{ scale: 1.04 }}
className="relative cursor-pointer flex flex-col items-center group"
>
{/* Icon container — relative wrapper for centering effects */}
<div className="relative flex items-center justify-center">
{/* Smooth sonar ripple rings on active — no heartbeat jerk */}
{isActive && (
<>
<style dangerouslySetInnerHTML={{ __html: `
@keyframes sonar-ripple {
0% {
transform: scale(0.85);
opacity: 0.5;
}
100% {
transform: scale(1.8);
opacity: 0;
}
}
`}} />
<span
className="absolute w-12 h-12 rounded-full border border-purple-primary/60 pointer-events-none"
style={{ animation: 'sonar-ripple 3s ease-out infinite' }}
/>
<span
className="absolute w-12 h-12 rounded-full border border-purple-primary/40 pointer-events-none"
style={{ animation: 'sonar-ripple 3s ease-out 1s infinite' }}
/>
<span
className="absolute w-12 h-12 rounded-full border border-purple-primary/25 pointer-events-none"
style={{ animation: 'sonar-ripple 3s ease-out 2s infinite' }}
/>
</>
)}
{/* Steady centered glow behind active icon */}
{isActive && (
<div className="absolute w-16 h-16 rounded-full bg-[#E7D3EF]/35 blur-[10px] pointer-events-none" />
)}
{/* Central Icon Circle */}
<div
className={`relative w-12 h-12 rounded-2xl flex items-center justify-center text-2xl border transition-all duration-300 ${
isActive
? 'bg-purple-deep border-purple-deep text-white shadow-[0_6px_20px_rgba(104,50,133,0.25)]'
: 'bg-white border-[#E7D3EF]/85 text-nearle-dark group-hover:border-purple-primary/60 group-hover:shadow-nearle-sm'
}`}
>
{node.emoji}
</div>
</div>
{/* Label Badge underneath node */}
<div
className={`mt-2.5 whitespace-nowrap border rounded-full px-3 py-1 font-body text-[10px] font-extrabold shadow-nearle-xs transition-all duration-300 ${
isActive
? 'bg-purple-deep border-purple-deep text-white shadow-[0_4px_12px_rgba(104,50,133,0.18)]'
: 'bg-white border-[#E7D3EF] text-nearle-dark group-hover:border-purple-primary group-hover:text-purple-primary font-bold'
}`}
>
{node.label}
</div>
</motion.div>
</div>
)
})}
</div>
{/* Helper hint badge */}
<div className="absolute bottom-4 right-4 flex items-center gap-2 bg-white border border-[#E7D3EF] rounded-full px-3.5 py-1.5 shadow-[0_4px_16px_rgba(164,103,183,0.08)] select-none z-30">
<span className="text-[12px] animate-bounce">📍</span>
<span className="font-body text-[11px] font-bold text-nearle-dark tracking-wide uppercase">
Neighbourhood Plan
</span>
</div>
</div>
)
}

View File

@@ -0,0 +1,31 @@
'use client'
import { useEffect, useRef } from 'react'
import { useInView, animate } from 'framer-motion'
type Props = {
value: number
duration?: number
suffix?: string
}
export function AnimatedCounter({ value, duration = 2, suffix = '' }: Props) {
const ref = useRef<HTMLSpanElement>(null)
const inView = useInView(ref, { once: true, margin: '-50px' })
useEffect(() => {
if (inView && ref.current) {
animate(0, value, {
duration,
ease: 'easeOut',
onUpdate: (latest) => {
if (ref.current) {
ref.current.textContent = Math.floor(latest).toString() + suffix
}
},
})
}
}, [inView, value, duration, suffix])
return <span ref={ref}>0{suffix}</span>
}

View File

@@ -0,0 +1,20 @@
type Props = {
children: React.ReactNode
className?: string
tone?: 'lavender' | 'deep' | 'outline'
}
export function Badge({ children, className = '', tone = 'lavender' }: Props) {
const tones = {
lavender: 'bg-purple-lavender text-purple-deep',
deep: 'bg-purple-deep text-white',
outline: 'border border-purple-deep text-purple-deep',
}
return (
<span
className={`inline-flex items-center font-mono text-[11px] tracking-widest uppercase rounded-full px-3 py-1 ${tones[tone]} ${className}`}
>
{children}
</span>
)
}

View File

@@ -0,0 +1,57 @@
import { forwardRef } from 'react'
import type { ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react'
type Variant = 'primary' | 'secondary' | 'ghost'
const base =
'inline-flex items-center justify-center gap-2 font-body font-semibold rounded-full transition-all duration-300 active:scale-95 disabled:opacity-60 disabled:cursor-not-allowed'
const sizes = {
sm: 'px-4 py-2 text-sm',
md: 'px-7 py-3 text-base',
lg: 'px-8 py-3.5 text-base',
}
const variants: Record<Variant, string> = {
primary:
'bg-purple-deep text-white shadow-cta hover:bg-purple-primary hover:shadow-cta-hover',
secondary:
'border-2 border-purple-deep text-purple-deep bg-transparent hover:bg-purple-deep hover:text-white',
ghost:
'text-purple-deep hover:bg-purple-soft',
}
type CommonProps = {
variant?: Variant
size?: keyof typeof sizes
}
export type ButtonProps = CommonProps &
ButtonHTMLAttributes<HTMLButtonElement> & { as?: 'button' }
export type AnchorProps = CommonProps &
AnchorHTMLAttributes<HTMLAnchorElement> & { as: 'a'; href: string }
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', className = '', ...rest }, ref) => (
<button
ref={ref}
className={`${base} ${sizes[size]} ${variants[variant]} ${className}`}
{...rest}
/>
)
)
Button.displayName = 'Button'
export function ButtonLink({
variant = 'primary',
size = 'md',
className = '',
...rest
}: AnchorProps) {
return (
<a
className={`${base} ${sizes[size]} ${variants[variant]} ${className}`}
{...rest}
/>
)
}

View File

@@ -0,0 +1,18 @@
import type { HTMLAttributes } from 'react'
type Props = HTMLAttributes<HTMLDivElement> & {
hover?: boolean
}
export function GlassCard({ hover = true, className = '', children, ...rest }: Props) {
return (
<div
className={`bg-white/80 backdrop-blur-md border border-nearle-border rounded-2xl p-6 shadow-nearle-sm transition-all duration-300 ${
hover ? 'hover:border-purple-primary hover:shadow-nearle-md hover:-translate-y-1' : ''
} ${className}`}
{...rest}
>
{children}
</div>
)
}

View File

@@ -0,0 +1,51 @@
type Props = {
size?: number
className?: string
stroke?: string
fill?: string
}
export function NearleLogo({
size = 40,
className = '',
stroke = '#683285',
fill = '#F5EEF8',
}: Props) {
return (
<svg
width={size}
height={size * 1.15}
viewBox="0 0 60 70"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-label="Nearle logo"
>
<rect
x="4"
y="24"
width="52"
height="42"
rx="7"
fill={fill}
stroke={stroke}
strokeWidth="2.5"
/>
<path
d="M16 24 C16 10 26 5 30 8 C34 5 44 10 44 24"
stroke={stroke}
strokeWidth="3"
fill="none"
strokeLinecap="round"
/>
<path
d="M14 60 L14 32 L30 54 L46 32 L46 60"
stroke={stroke}
strokeWidth="4"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}

View File

@@ -0,0 +1,33 @@
type Props = {
label?: string
heading: React.ReactNode
sub?: React.ReactNode
align?: 'left' | 'center'
className?: string
}
export function SectionHeader({
label,
heading,
sub,
align = 'left',
className = '',
}: Props) {
const alignment =
align === 'center' ? 'items-center text-center mx-auto' : 'items-start text-left'
return (
<div className={`flex flex-col ${alignment} max-w-3xl ${className}`}>
{label ? (
<span className="font-mono text-[11px] tracking-[0.18em] uppercase text-purple-primary">
{label}
</span>
) : null}
<h2 className="h2-display text-nearle-dark mt-3">{heading}</h2>
{sub ? (
<p className="font-body text-nearle-mid text-lg leading-relaxed mt-4">
{sub}
</p>
) : null}
</div>
)
}

View File

@@ -0,0 +1,8 @@
'use client'
import { useLenis } from './useLenis'
export function LenisProvider({ children }: { children: React.ReactNode }) {
useLenis()
return <>{children}</>
}

51
src/lib/animations.ts Normal file
View File

@@ -0,0 +1,51 @@
import type { Variants } from 'framer-motion'
export const fadeUp: Variants = {
hidden: { opacity: 0, y: 28 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: [0.25, 0.46, 0.45, 0.94] },
},
}
export const fadeIn: Variants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.5 } },
}
export const staggerContainer: Variants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
},
}
export const scaleUp: Variants = {
hidden: { opacity: 0, scale: 0.92 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.5, ease: [0.25, 0.46, 0.45, 0.94] },
},
}
export const slideLeft: Variants = {
hidden: { opacity: 0, x: 40 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.6, ease: [0.25, 0.46, 0.45, 0.94] },
},
}
export const slideRight: Variants = {
hidden: { opacity: 0, x: -40 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.6, ease: [0.25, 0.46, 0.45, 0.94] },
},
}
export const viewportConfig = { once: true, margin: '-80px' } as const

27
src/lib/useLenis.ts Normal file
View File

@@ -0,0 +1,27 @@
'use client'
import { useEffect } from 'react'
import Lenis from 'lenis'
export function useLenis() {
useEffect(() => {
const lenis = new Lenis({
duration: 1.2,
easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
orientation: 'vertical',
smoothWheel: true,
})
let rafId = 0
function raf(time: number) {
lenis.raf(time)
rafId = requestAnimationFrame(raf)
}
rafId = requestAnimationFrame(raf)
return () => {
cancelAnimationFrame(rafId)
lenis.destroy()
}
}, [])
}

30
src/lib/useScrollSpy.ts Normal file
View File

@@ -0,0 +1,30 @@
'use client'
import { useEffect, useState } from 'react'
export function useScrollSpy(sectionIds: string[], rootMargin = '-40% 0px -55% 0px') {
const [activeId, setActiveId] = useState<string | null>(null)
useEffect(() => {
if (typeof window === 'undefined') return
const elements = sectionIds
.map((id) => document.getElementById(id))
.filter((el): el is HTMLElement => !!el)
if (elements.length === 0) return
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) setActiveId(entry.target.id)
})
},
{ rootMargin, threshold: 0 }
)
elements.forEach((el) => observer.observe(el))
return () => observer.disconnect()
}, [sectionIds, rootMargin])
return activeId
}

75
tailwind.config.ts Normal file
View File

@@ -0,0 +1,75 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {
colors: {
purple: {
primary: '#A467B7',
deep: '#683285',
accent: '#AE79BF',
lavender: '#E7D3EF',
soft: '#F5EEF8',
},
nearle: {
dark: '#111827',
mid: '#475569',
light: '#94A3B8',
border: '#E2E8F0',
bgsoft: '#F8FAFC',
},
},
fontFamily: {
display: ['var(--font-display)', 'Syne', 'sans-serif'],
body: ['var(--font-body)', 'DM Sans', 'sans-serif'],
mono: ['var(--font-mono)', 'JetBrains Mono', 'monospace'],
},
fontWeight: {
'800': '800',
},
boxShadow: {
'nearle-sm': '0 4px 16px rgba(164, 103, 183, 0.08)',
'nearle-md': '0 8px 32px rgba(164, 103, 183, 0.15)',
'nearle-lg': '0 20px 60px rgba(164, 103, 183, 0.20)',
'cta': '0 4px 20px rgba(104, 50, 133, 0.30)',
'cta-hover': '0 8px 32px rgba(104, 50, 133, 0.40)',
},
backgroundImage: {
'gradient-hero':
'radial-gradient(ellipse 80% 60% at 50% -10%, #E7D3EF 0%, #FFFFFF 70%)',
'gradient-card':
'linear-gradient(135deg, #FFFFFF 0%, #F5EEF8 100%)',
'gradient-purple':
'linear-gradient(135deg, #683285 0%, #A467B7 100%)',
},
animation: {
float: 'float 6s ease-in-out infinite',
'pulse-slow': 'pulse 4s ease-in-out infinite',
'slide-in': 'slideIn 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
'ride-loop': 'rideLoop 12s linear infinite',
'dash-loop': 'dashLoop 1.5s linear infinite',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-12px)' },
},
slideIn: {
from: { opacity: '0', transform: 'translateY(24px)' },
to: { opacity: '1', transform: 'translateY(0)' },
},
rideLoop: {
'0%': { offsetDistance: '0%' },
'100%': { offsetDistance: '100%' },
},
dashLoop: {
to: { strokeDashoffset: '-24' },
},
},
},
},
plugins: [],
}
export default config

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "nearle-next", "dist"]
}