first commit
This commit is contained in:
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal 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
50
README.md
Normal 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
5
next-env.d.ts
vendored
Normal 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
6
next.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
5884
package-lock.json
generated
Normal file
5884
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
package.json
Normal file
43
package.json
Normal 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
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
82
src/app/about/page.tsx
Normal file
82
src/app/about/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
76
src/app/businesses/page.tsx
Normal file
76
src/app/businesses/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
81
src/app/communities/page.tsx
Normal file
81
src/app/communities/page.tsx
Normal 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
128
src/app/contact/page.tsx
Normal 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
107
src/app/globals.css
Normal 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
80
src/app/layout.tsx
Normal 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
27
src/app/page.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
375
src/components/home/AppEcosystem.tsx
Normal file
375
src/components/home/AppEcosystem.tsx
Normal 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'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>
|
||||
)
|
||||
}
|
||||
175
src/components/home/Categories.tsx
Normal file
175
src/components/home/Categories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
102
src/components/home/ConnectLocally.tsx
Normal file
102
src/components/home/ConnectLocally.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
146
src/components/home/Contact.tsx
Normal file
146
src/components/home/Contact.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
136
src/components/home/Download.tsx
Normal file
136
src/components/home/Download.tsx
Normal 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
107
src/components/home/FAQ.tsx
Normal 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 India’s 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>
|
||||
)
|
||||
}
|
||||
108
src/components/home/Hero.tsx
Normal file
108
src/components/home/Hero.tsx
Normal 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'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'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>
|
||||
)
|
||||
}
|
||||
|
||||
246
src/components/home/NeighbourhoodMap.tsx
Normal file
246
src/components/home/NeighbourhoodMap.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
176
src/components/home/OperationalPlan.tsx
Normal file
176
src/components/home/OperationalPlan.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
358
src/components/home/OrderLifecycle.tsx
Normal file
358
src/components/home/OrderLifecycle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
94
src/components/home/Stats.tsx
Normal file
94
src/components/home/Stats.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
141
src/components/home/Testimonials.tsx
Normal file
141
src/components/home/Testimonials.tsx
Normal 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">
|
||||
“{TESTIMONIALS[active]!.quote}”
|
||||
</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>
|
||||
)
|
||||
}
|
||||
122
src/components/home/ValueEcosystem.tsx
Normal file
122
src/components/home/ValueEcosystem.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
93
src/components/layout/Footer.tsx
Normal file
93
src/components/layout/Footer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
166
src/components/layout/Navbar.tsx
Normal file
166
src/components/layout/Navbar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
1245
src/components/three/CityRider3D.tsx
Normal file
1245
src/components/three/CityRider3D.tsx
Normal file
File diff suppressed because it is too large
Load Diff
327
src/components/three/HeroPhones3D.tsx
Normal file
327
src/components/three/HeroPhones3D.tsx
Normal 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 & 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 & 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>
|
||||
)
|
||||
}
|
||||
246
src/components/three/IsoMap3D.tsx
Normal file
246
src/components/three/IsoMap3D.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
31
src/components/ui/AnimatedCounter.tsx
Normal file
31
src/components/ui/AnimatedCounter.tsx
Normal 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>
|
||||
}
|
||||
20
src/components/ui/Badge.tsx
Normal file
20
src/components/ui/Badge.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
57
src/components/ui/Button.tsx
Normal file
57
src/components/ui/Button.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
18
src/components/ui/GlassCard.tsx
Normal file
18
src/components/ui/GlassCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
51
src/components/ui/NearleLogo.tsx
Normal file
51
src/components/ui/NearleLogo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
33
src/components/ui/SectionHeader.tsx
Normal file
33
src/components/ui/SectionHeader.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
8
src/lib/LenisProvider.tsx
Normal file
8
src/lib/LenisProvider.tsx
Normal 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
51
src/lib/animations.ts
Normal 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
27
src/lib/useLenis.ts
Normal 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
30
src/lib/useScrollSpy.ts
Normal 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
75
tailwind.config.ts
Normal 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
24
tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user