update d2c brands
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||||
import { PremiumPageShell } from "@/components/layout/PremiumPageShell";
|
import { PremiumPageShell } from "@/components/layout/PremiumPageShell";
|
||||||
@@ -7,6 +8,8 @@ import { FAQAccordion } from "@/components/ui/FAQAccordion";
|
|||||||
import { AnimatedSection } from "@/components/motion/AnimatedSection";
|
import { AnimatedSection } from "@/components/motion/AnimatedSection";
|
||||||
import { AnimatedGrid, AnimatedCard } from "@/components/motion/AnimatedGrid";
|
import { AnimatedGrid, AnimatedCard } from "@/components/motion/AnimatedGrid";
|
||||||
import { CTABanner } from "@/components/ui/CTABanner";
|
import { CTABanner } from "@/components/ui/CTABanner";
|
||||||
|
import { NetworkBackground } from "@/components/brands/NetworkBackground";
|
||||||
|
import { PartnershipConnector } from "@/components/brands/PartnershipConnector";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -151,8 +154,9 @@ export default function BrandsPage() {
|
|||||||
<BrandsHero />
|
<BrandsHero />
|
||||||
|
|
||||||
{/* ─── What We Offer ─── */}
|
{/* ─── What We Offer ─── */}
|
||||||
<section className="py-10 md:py-14 xl:py-12 bg-white border-b border-[#6330D6]/10">
|
<section className="relative overflow-hidden py-10 md:py-14 xl:py-12 bg-white border-b border-[#6330D6]/10">
|
||||||
<div className="px-6 md:px-12 lg:px-16 xl:px-20 max-w-[1600px] mx-auto">
|
<NetworkBackground />
|
||||||
|
<div className="relative z-10 px-6 md:px-12 lg:px-16 xl:px-20 2xl:px-24">
|
||||||
<AnimatedSection>
|
<AnimatedSection>
|
||||||
<div className="text-center mb-10 md:mb-12">
|
<div className="text-center mb-10 md:mb-12">
|
||||||
<span className="text-xs text-[#6330D6] uppercase tracking-widest font-black bg-[#6330D6]/5 px-3.5 py-1 rounded-full">
|
<span className="text-xs text-[#6330D6] uppercase tracking-widest font-black bg-[#6330D6]/5 px-3.5 py-1 rounded-full">
|
||||||
@@ -183,7 +187,7 @@ export default function BrandsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-black text-[#16001D] leading-tight mb-2">{item.title}</h3>
|
<h3 className="text-base font-black text-[#16001D] leading-tight mb-2">{item.title}</h3>
|
||||||
<p className="text-sm text-[#5E5866] leading-relaxed">{item.desc}</p>
|
<p className="text-m text-[#5E5866] leading-relaxed">{item.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
))}
|
))}
|
||||||
@@ -193,40 +197,78 @@ export default function BrandsPage() {
|
|||||||
|
|
||||||
{/* ─── Industries ─── */}
|
{/* ─── Industries ─── */}
|
||||||
<section className="py-10 md:py-14 xl:py-12 bg-[#FAF7FD] border-b border-[#6330D6]/10">
|
<section className="py-10 md:py-14 xl:py-12 bg-[#FAF7FD] border-b border-[#6330D6]/10">
|
||||||
<div className="px-6 md:px-12 lg:px-16 xl:px-20 max-w-[1600px] mx-auto">
|
<div className="px-6 md:px-12 lg:px-16 xl:px-20 2xl:px-24">
|
||||||
<AnimatedSection>
|
<div className="grid grid-cols-1 lg:grid-cols-[30%_1fr] gap-10 xl:gap-16 items-center">
|
||||||
<div className="text-center mb-10">
|
{/* Real product collage */}
|
||||||
<span className="text-xs text-secondary uppercase tracking-widest font-black bg-secondary/5 px-3.5 py-1 rounded-full">
|
<AnimatedSection className="order-2 lg:order-1" variant="fadeUp">
|
||||||
Industries We Serve
|
<div className="relative mx-auto h-[280px] w-full max-w-[420px] sm:h-[360px] lg:mx-0 lg:h-[440px]">
|
||||||
</span>
|
<div className="absolute left-0 top-0 h-[62%] w-[72%] overflow-hidden rounded-[28px] border-4 border-white shadow-[0_20px_50px_rgba(22,0,29,0.16)]">
|
||||||
<h2 className="font-display text-3xl md:text-4xl font-black text-[#16001D] mt-4 mb-3 tracking-tight">
|
<Image
|
||||||
Your Category Has a Place on Nearle
|
src="/images/cat-daily-essentials.webp"
|
||||||
</h2>
|
alt="Daily essentials — milk, bread, and fresh produce stocked on Nearle stores"
|
||||||
<p className="text-sm md:text-base text-[#5E5866] max-w-2xl mx-auto leading-relaxed">
|
fill
|
||||||
Nearle's neighbourhood store network stocks a wide range of everyday product categories. If customers buy it daily, it belongs here.
|
sizes="(max-width: 1023px) 300px, 22vw"
|
||||||
</p>
|
className="object-cover"
|
||||||
</div>
|
/>
|
||||||
</AnimatedSection>
|
|
||||||
|
|
||||||
<AnimatedGrid className="grid grid-cols-3 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-9 gap-4">
|
|
||||||
{INDUSTRIES.map((industry) => (
|
|
||||||
<AnimatedCard
|
|
||||||
key={industry.label}
|
|
||||||
className="flex flex-col items-center gap-2.5 p-4 rounded-2xl bg-white border border-[#6330D6]/8 hover:border-[#6330D6]/20 hover:shadow-md transition-all duration-200 cursor-default"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 rounded-full bg-[#6330D6]/8 text-[#6330D6] flex items-center justify-center">
|
|
||||||
<MaterialIcon icon={industry.icon} size={20} />
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] font-black text-[#16001D] text-center leading-tight">{industry.label}</span>
|
<div className="absolute right-0 top-[6%] h-[42%] w-[52%] overflow-hidden rounded-[22px] border-4 border-white shadow-[0_16px_40px_rgba(22,0,29,0.14)]">
|
||||||
</AnimatedCard>
|
<Image
|
||||||
))}
|
src="/images/cat-health-wellness.webp"
|
||||||
</AnimatedGrid>
|
alt="Health and personal care products stocked on Nearle stores"
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 1023px) 220px, 16vw"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 left-[10%] h-[38%] w-[54%] overflow-hidden rounded-[22px] border-4 border-white shadow-[0_16px_40px_rgba(22,0,29,0.14)]">
|
||||||
|
<Image
|
||||||
|
src="/images/cat-pet-essentials.webp"
|
||||||
|
alt="Pet essentials brand stocked on Nearle stores"
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 1023px) 220px, 16vw"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="order-1 lg:order-2">
|
||||||
|
<AnimatedSection>
|
||||||
|
<div className="mb-10 text-left">
|
||||||
|
<span className="text-xs text-secondary uppercase tracking-widest font-black bg-secondary/5 px-3.5 py-1 rounded-full">
|
||||||
|
Industries We Serve
|
||||||
|
</span>
|
||||||
|
<h2 className="font-display text-3xl md:text-4xl font-black text-[#16001D] mt-4 mb-3 tracking-tight">
|
||||||
|
Your Category Has a Place on Nearle
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm md:text-base text-[#5E5866] max-w-2xl leading-relaxed">
|
||||||
|
Nearle's neighbourhood store network stocks a wide range of everyday product categories. If customers buy it daily, it belongs here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
|
||||||
|
<AnimatedGrid className="grid grid-cols-3 gap-4">
|
||||||
|
{INDUSTRIES.map((industry) => (
|
||||||
|
<AnimatedCard
|
||||||
|
key={industry.label}
|
||||||
|
className="flex flex-col items-center gap-2.5 p-4 rounded-2xl bg-white border border-[#6330D6]/8 hover:border-[#6330D6]/20 hover:shadow-md transition-all duration-200 cursor-default"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-full bg-[#6330D6]/8 text-[#6330D6] flex items-center justify-center">
|
||||||
|
<MaterialIcon icon={industry.icon} size={20} />
|
||||||
|
</div>
|
||||||
|
<span className="text-[16px] font-black text-[#16001D] text-center leading-tight">{industry.label}</span>
|
||||||
|
</AnimatedCard>
|
||||||
|
))}
|
||||||
|
</AnimatedGrid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ─── Partnership Process ─── */}
|
{/* ─── Partnership Process ─── */}
|
||||||
<section id="partnership" className="py-10 md:py-14 xl:py-12 bg-white border-b border-[#6330D6]/10">
|
<section id="partnership" className="py-10 md:py-14 xl:py-12 bg-white border-b border-[#6330D6]/10">
|
||||||
<div className="px-6 md:px-12 lg:px-16 xl:px-20 max-w-[1600px] mx-auto">
|
<div className="px-6 md:px-12 lg:px-16 xl:px-20 2xl:px-24">
|
||||||
<AnimatedSection>
|
<AnimatedSection>
|
||||||
<div className="text-center mb-10 md:mb-12">
|
<div className="text-center mb-10 md:mb-12">
|
||||||
<span className="text-xs text-[#6330D6] uppercase tracking-widest font-black bg-[#6330D6]/5 px-3.5 py-1 rounded-full">
|
<span className="text-xs text-[#6330D6] uppercase tracking-widest font-black bg-[#6330D6]/5 px-3.5 py-1 rounded-full">
|
||||||
@@ -241,6 +283,8 @@ export default function BrandsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</AnimatedSection>
|
</AnimatedSection>
|
||||||
|
|
||||||
|
<PartnershipConnector steps={PARTNERSHIP_STEPS.length} />
|
||||||
|
|
||||||
<AnimatedGrid className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
<AnimatedGrid className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||||
{PARTNERSHIP_STEPS.map((step) => (
|
{PARTNERSHIP_STEPS.map((step) => (
|
||||||
<AnimatedCard
|
<AnimatedCard
|
||||||
@@ -256,7 +300,7 @@ export default function BrandsPage() {
|
|||||||
<h3 className="font-display text-xl font-black text-[#16001D] leading-tight mb-2">
|
<h3 className="font-display text-xl font-black text-[#16001D] leading-tight mb-2">
|
||||||
{step.title}
|
{step.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-[#5E5866] leading-relaxed">{step.desc}</p>
|
<p className="text-md text-[#5E5866] leading-relaxed">{step.desc}</p>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
))}
|
))}
|
||||||
</AnimatedGrid>
|
</AnimatedGrid>
|
||||||
@@ -265,10 +309,24 @@ export default function BrandsPage() {
|
|||||||
|
|
||||||
{/* ─── FAQ ─── */}
|
{/* ─── FAQ ─── */}
|
||||||
<section className="py-10 md:py-14 xl:py-12 bg-[#FAF7FD] border-b border-[#6330D6]/10">
|
<section className="py-10 md:py-14 xl:py-12 bg-[#FAF7FD] border-b border-[#6330D6]/10">
|
||||||
<div className="px-6 md:px-12 lg:px-16 xl:px-20 max-w-[1600px] mx-auto">
|
<div className="px-6 md:px-12 lg:px-16 xl:px-20 2xl:px-24">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-[360px_1fr] gap-10 lg:gap-16 items-start">
|
<div className="grid grid-cols-1 lg:grid-cols-[360px_1fr] gap-10 lg:gap-16 items-start">
|
||||||
<AnimatedSection>
|
<AnimatedSection>
|
||||||
<div className="lg:sticky lg:top-28">
|
<div className="lg:sticky lg:top-28">
|
||||||
|
<div className="relative mb-6">
|
||||||
|
<div className="absolute -left-6 -top-6 h-28 w-28 rounded-full bg-[#6330D6]/25 blur-3xl -z-10" />
|
||||||
|
<div className="absolute -right-4 -bottom-4 h-24 w-24 rounded-full bg-secondary/20 blur-3xl -z-10" />
|
||||||
|
<div className="relative h-[220px] sm:h-[260px] rounded-[28px] overflow-hidden shadow-[0_20px_50px_rgba(22,0,29,0.16)]">
|
||||||
|
<Image
|
||||||
|
src="/images/nearle-merchant/success-owner.webp"
|
||||||
|
alt="Neighbourhood store owner reviewing brand orders on his phone"
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 1023px) 90vw, 360px"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-[#16001D]/70 via-[#6330D6]/20 to-transparent" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<span className="text-xs text-[#6330D6] uppercase tracking-widest font-black bg-[#6330D6]/5 px-3.5 py-1 rounded-full">
|
<span className="text-xs text-[#6330D6] uppercase tracking-widest font-black bg-[#6330D6]/5 px-3.5 py-1 rounded-full">
|
||||||
FAQs
|
FAQs
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
85
src/components/brands/NetworkBackground.tsx
Normal file
85
src/components/brands/NetworkBackground.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
|
||||||
|
const NODES = [
|
||||||
|
{ cx: 60, cy: 80 }, { cx: 220, cy: 40 }, { cx: 380, cy: 120 },
|
||||||
|
{ cx: 520, cy: 60 }, { cx: 680, cy: 140 }, { cx: 140, cy: 260 },
|
||||||
|
{ cx: 320, cy: 300 }, { cx: 480, cy: 260 }, { cx: 620, cy: 320 },
|
||||||
|
{ cx: 80, cy: 420 }, { cx: 260, cy: 460 }, { cx: 440, cy: 420 },
|
||||||
|
{ cx: 600, cy: 480 }, { cx: 740, cy: 400 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LINES: [number, number][] = [
|
||||||
|
[0, 1], [1, 2], [2, 3], [3, 4], [0, 5], [1, 6], [2, 7], [3, 8], [4, 8],
|
||||||
|
[5, 6], [6, 7], [7, 8], [5, 9], [6, 10], [7, 11], [8, 12], [9, 10], [10, 11], [11, 12], [12, 13], [8, 13],
|
||||||
|
];
|
||||||
|
|
||||||
|
const PARTICLES = [
|
||||||
|
{ cx: 100, cy: 500 }, { cx: 260, cy: 80 }, { cx: 420, cy: 520 }, { cx: 580, cy: 100 },
|
||||||
|
{ cx: 720, cy: 480 }, { cx: 40, cy: 250 }, { cx: 380, cy: 200 }, { cx: 660, cy: 260 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Ambient network backdrop for Section 1 — nodes, routes, and drifting particles at very low opacity. Purely decorative. */
|
||||||
|
export function NetworkBackground() {
|
||||||
|
const reduce = useReducedMotion();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none absolute inset-0 overflow-hidden opacity-[0.07]" aria-hidden="true">
|
||||||
|
<motion.div
|
||||||
|
className="absolute left-1/2 top-1/2 h-[520px] w-[520px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#6330D6] blur-[130px]"
|
||||||
|
animate={reduce ? undefined : { opacity: [0.45, 0.9, 0.45] }}
|
||||||
|
transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
className="absolute inset-0 h-full w-full"
|
||||||
|
viewBox="0 0 800 600"
|
||||||
|
fill="none"
|
||||||
|
preserveAspectRatio="xMidYMid slice"
|
||||||
|
>
|
||||||
|
{LINES.map(([a, b], i) => {
|
||||||
|
const n1 = NODES[a];
|
||||||
|
const n2 = NODES[b];
|
||||||
|
return (
|
||||||
|
<motion.line
|
||||||
|
key={`l-${a}-${b}`}
|
||||||
|
x1={n1.cx}
|
||||||
|
y1={n1.cy}
|
||||||
|
x2={n2.cx}
|
||||||
|
y2={n2.cy}
|
||||||
|
stroke="#6330D6"
|
||||||
|
strokeWidth={1}
|
||||||
|
strokeDasharray="4 7"
|
||||||
|
animate={reduce ? undefined : { strokeDashoffset: [0, -44] }}
|
||||||
|
transition={{ duration: 6 + (i % 5), repeat: Infinity, ease: "linear" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{NODES.map((n, i) => (
|
||||||
|
<motion.circle
|
||||||
|
key={`n-${i}`}
|
||||||
|
cx={n.cx}
|
||||||
|
cy={n.cy}
|
||||||
|
fill="#6330D6"
|
||||||
|
animate={reduce ? undefined : { r: [3, 5, 3], opacity: [0.4, 1, 0.4] }}
|
||||||
|
initial={{ r: 3, opacity: 0.6 }}
|
||||||
|
transition={{ duration: 3 + (i % 4), repeat: Infinity, ease: "easeInOut", delay: (i % 5) * 0.4 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{PARTICLES.map((p, i) => (
|
||||||
|
<motion.circle
|
||||||
|
key={`p-${i}`}
|
||||||
|
cx={p.cx}
|
||||||
|
r={2}
|
||||||
|
fill="#6330D6"
|
||||||
|
initial={{ cy: p.cy, opacity: 0 }}
|
||||||
|
animate={reduce ? undefined : { cy: [p.cy, p.cy - 46, p.cy], opacity: [0, 0.85, 0] }}
|
||||||
|
transition={{ duration: 7 + (i % 4), repeat: Infinity, ease: "easeInOut", delay: i * 0.6 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/components/brands/PartnershipConnector.tsx
Normal file
55
src/components/brands/PartnershipConnector.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
|
||||||
|
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||||
|
|
||||||
|
/** One-shot workflow connector for the Partnership Process — draws once on scroll into view, then a glow dot travels Step 1 → Step 4 and stops. No looping. Desktop-only (aligns to the single-row xl:grid-cols-4 layout). */
|
||||||
|
export function PartnershipConnector({ steps }: { steps: number }) {
|
||||||
|
const reduce = useReducedMotion();
|
||||||
|
const positions = Array.from({ length: steps }, (_, i) => ((i + 0.5) / steps) * 100);
|
||||||
|
const start = positions[0];
|
||||||
|
const end = positions[positions.length - 1];
|
||||||
|
const lineDuration = 0.9;
|
||||||
|
const dotDuration = 1.1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative mb-3 hidden h-6 xl:block" aria-hidden="true">
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-1/2 h-0 origin-left -translate-y-1/2 border-t-2 border-dashed border-[#6330D6]/20"
|
||||||
|
style={{ left: `${start}%`, right: `${100 - end}%` }}
|
||||||
|
initial={{ scaleX: 0 }}
|
||||||
|
whileInView={{ scaleX: 1 }}
|
||||||
|
viewport={{ once: true, amount: 0.4 }}
|
||||||
|
transition={reduce ? { duration: 0 } : { duration: lineDuration, ease: EASE }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{positions.map((left, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#6330D6]/40"
|
||||||
|
style={{ left: `${left}%` }}
|
||||||
|
initial={{ opacity: 0, scale: 0.4 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
|
viewport={{ once: true, amount: 0.4 }}
|
||||||
|
transition={reduce ? { duration: 0 } : { duration: 0.35, ease: EASE, delay: lineDuration * 0.6 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#6330D6] shadow-[0_0_14px_4px_rgba(99,48,214,0.55)]"
|
||||||
|
initial={{ left: `${start}%`, opacity: 0 }}
|
||||||
|
whileInView={{ left: `${end}%`, opacity: 1 }}
|
||||||
|
viewport={{ once: true, amount: 0.4 }}
|
||||||
|
transition={
|
||||||
|
reduce
|
||||||
|
? { duration: 0 }
|
||||||
|
: {
|
||||||
|
left: { duration: dotDuration, ease: EASE, delay: lineDuration },
|
||||||
|
opacity: { duration: 0.2, delay: lineDuration },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user