12 Commits

Author SHA1 Message Date
ec8812c6e2 Strengthen mobile navbar blur 2026-05-16 23:12:43 +01:00
cdd6f1b103 Restore mobile glass effects 2026-05-16 23:09:32 +01:00
ec9bc7552f Stretch mobile WhatsApp CTA 2026-05-16 22:59:11 +01:00
c6b649ef33 Tighten mobile trust contact spacing 2026-05-16 22:56:13 +01:00
5fab9c95ac Balance mobile navbar controls 2026-05-16 22:55:24 +01:00
e168a646a1 Fit mobile nav on small screens 2026-05-16 22:49:16 +01:00
d93f862cba Tighten mobile CTA sizing 2026-05-16 22:45:57 +01:00
ebec21f1ac Merge 3D typography preview into main 2026-05-16 22:39:55 +01:00
539437b824 Use New York style display headings 2026-05-16 22:15:29 +01:00
6549b03d9a Use SF Pro style font stack 2026-05-16 21:52:38 +01:00
b77883a16c Revert "Merge pull request '3D' (#4) from 3D into main"
This reverts commit e69e20f753, reversing
changes made to 0ac37261a8.
2026-05-16 21:12:28 +01:00
e69e20f753 Merge pull request '3D' (#4) from 3D into main
Reviewed-on: #4
2026-05-16 20:49:27 +01:00
15 changed files with 55 additions and 383 deletions

View File

@@ -6,12 +6,19 @@ All notable changes to the New Optic website will be documented in this file.
### Changed
- Added a `3D` branch experiment with an interactive annotated glasses model section.
- Added a New York-style display font stack for large latin headings while keeping SF-style UI text.
- Switched the latin typography to an SF Pro-style system font stack for a more Apple-like feel.
- Removed the desktop liquid glass WebGL layer from navbar and CTA controls so text remains readable and glass surfaces do not show dark rendering artifacts while scrolling.
- Replaced the mobile hamburger dropdown with a fast, solid menu animation without bounce or stretch effects.
- Kept the language switcher visible in the mobile top navigation bar.
- Added cursor-reactive stretch motion to desktop nav links and matched the glossy dark WhatsApp CTA style on mobile.
- Corrected the French 2004 trust wording to use "depuis" without "autour de".
- Tightened mobile sizing for the WhatsApp nav pill and hero trust badge.
- Balanced mobile navbar controls so the language switcher, WhatsApp CTA, and menu do not bunch up on the right.
- Reduced the mobile gap between the reputation section and contact panel.
- Let the mobile WhatsApp nav CTA stretch into available navbar space.
- Restored mobile glass blur and premium highlight layers instead of flattening glass surfaces.
- Added a dedicated stronger blur treatment for the mobile top navigation bar.
## [1.0.0] - 2026-05-16

View File

@@ -6,6 +6,8 @@
color-scheme: light;
--bg: #f6f5f2;
--ink: #111317;
--font-sans: "SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-display: "New York Large", "New York", "NewYork", ui-serif, Georgia, Cambria, "Times New Roman", serif;
}
* {
@@ -24,7 +26,7 @@ body {
url('/assets/New-optic-BG-mobile.webp') center top / cover scroll,
linear-gradient(180deg, #fbfaf8 0%, #f6f5f2 42%, #ffffff 100%);
color: var(--ink);
font-family: var(--font-sans), system-ui, sans-serif;
font-family: var(--font-sans);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
@@ -67,6 +69,10 @@ body[dir="rtl"] {
font-family: var(--font-arabic), system-ui, sans-serif;
}
body:not([dir="rtl"]) :where(h1, h2) {
font-family: var(--font-display);
}
a {
color: inherit;
text-decoration: none;
@@ -80,7 +86,8 @@ a {
background: rgba(255, 255, 255, 0.68);
border: 1px solid rgba(255, 255, 255, 0.72);
box-shadow: 0 18px 60px rgba(17, 19, 23, 0.08);
backdrop-filter: blur(24px);
backdrop-filter: blur(24px) saturate(1.22);
-webkit-backdrop-filter: blur(24px) saturate(1.22);
}
.premium-glass::before {
@@ -109,6 +116,12 @@ a {
inset 18px 0 34px rgba(255, 255, 255, 0.12);
}
.nav-glass {
background: rgba(255, 255, 255, 0.62);
backdrop-filter: blur(30px) saturate(1.32);
-webkit-backdrop-filter: blur(30px) saturate(1.32);
}
.hairline {
background: linear-gradient(90deg, transparent, rgba(17, 19, 23, 0.15), transparent);
height: 1px;
@@ -166,25 +179,21 @@ a {
display: none;
}
.floating-sheen {
animation: none;
opacity: 0;
}
.glass,
[class*="backdrop-blur"] {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
.glass {
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 10px 30px rgba(17, 19, 23, 0.07);
background: rgba(255, 255, 255, 0.72);
box-shadow: 0 14px 34px rgba(17, 19, 23, 0.08);
backdrop-filter: blur(18px) saturate(1.24);
-webkit-backdrop-filter: blur(18px) saturate(1.24);
}
.premium-glass::before,
.premium-glass::after {
display: none;
.nav-glass {
background: rgba(255, 255, 255, 0.58);
border-color: rgba(255, 255, 255, 0.86);
box-shadow:
0 14px 36px rgba(17, 19, 23, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.78);
backdrop-filter: blur(26px) saturate(1.38);
-webkit-backdrop-filter: blur(26px) saturate(1.38);
}
main > section {

View File

@@ -1,10 +1,9 @@
import type { Metadata } from "next";
import { Inter, Noto_Kufi_Arabic } from "next/font/google";
import { Noto_Kufi_Arabic } from "next/font/google";
import "./globals.css";
import { business } from "@/config/business";
import { fr } from "@/messages/fr";
const inter = Inter({ subsets: ["latin"], variable: "--font-sans", display: "swap" });
const arabic = Noto_Kufi_Arabic({ subsets: ["arabic"], variable: "--font-arabic", display: "swap" });
export const metadata: Metadata = {
@@ -28,7 +27,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="fr" className={`${inter.variable} ${arabic.variable} scroll-smooth`}>
<html lang="fr" className={`${arabic.variable} scroll-smooth`}>
<body>
{children}
</body>

View File

@@ -7,7 +7,7 @@ import PhysicsButton from "./PhysicsButton";
export default function ContactSection({ t, whatsappUrl }: { t: Messages; whatsappUrl: string }) {
return (
<AnimatedSection id="contact" className="px-4 py-20 sm:px-6">
<AnimatedSection id="contact" className="px-4 pb-16 pt-6 sm:px-6 sm:py-20">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1.08fr_0.92fr]">
<div className="rounded-[3rem] bg-white p-8 shadow-glass sm:p-12">
<p className="mb-4 text-xs font-semibold uppercase tracking-[0.28em] text-optical/75">{t.contact.eyebrow}</p>

View File

@@ -1,243 +0,0 @@
"use client";
import { motion } from "framer-motion";
import { useEffect, useRef } from "react";
import * as THREE from "three";
import type { Messages } from "@/messages";
export default function GlassesModelSection({ t }: { t: Messages }) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const wrapRef = useRef<HTMLDivElement | null>(null);
const pointerRef = useRef({ x: 0, y: 0 });
useEffect(() => {
const canvas = canvasRef.current;
const wrap = wrapRef.current;
if (!canvas || !wrap) return;
let frame = 0;
let disposed = false;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 100);
camera.position.set(0, 0.15, 7.9);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance" });
renderer.setClearColor(0x000000, 0);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.08;
const group = createGlassesModel();
group.position.y = -0.34;
scene.add(group);
scene.add(new THREE.HemisphereLight(0xffffff, 0xaeb9c6, 1.8));
const key = new THREE.DirectionalLight(0xffffff, 3.8);
key.position.set(3.4, 4.6, 5.2);
scene.add(key);
const rim = new THREE.DirectionalLight(0x9ec7ff, 1.7);
rim.position.set(-4, 1.8, -2);
scene.add(rim);
const resize = () => {
const { width, height } = wrap.getBoundingClientRect();
const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.7);
const scale = width < 640 ? 0.5 : width < 1024 ? 0.68 : 0.9;
renderer.setPixelRatio(pixelRatio);
renderer.setSize(width, height, false);
group.scale.setScalar(scale);
group.position.y = width < 640 ? -0.44 : -0.34;
camera.aspect = width / Math.max(height, 1);
camera.updateProjectionMatrix();
};
const observer = new ResizeObserver(resize);
observer.observe(wrap);
resize();
const handlePointerMove = (event: PointerEvent) => {
const bounds = wrap.getBoundingClientRect();
pointerRef.current.x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 2;
pointerRef.current.y = ((event.clientY - bounds.top) / bounds.height - 0.5) * 2;
};
const handlePointerLeave = () => {
pointerRef.current.x = 0;
pointerRef.current.y = 0;
};
wrap.addEventListener("pointermove", handlePointerMove);
wrap.addEventListener("pointerleave", handlePointerLeave);
const animate = () => {
if (disposed) return;
const targetY = pointerRef.current.x * 0.28;
const targetX = pointerRef.current.y * 0.16;
group.rotation.y += (targetY + Math.sin(performance.now() * 0.00045) * 0.22 - group.rotation.y) * 0.055;
group.rotation.x += (-0.08 - targetX - group.rotation.x) * 0.055;
group.rotation.z = Math.sin(performance.now() * 0.00062) * 0.025;
renderer.render(scene, camera);
frame = window.requestAnimationFrame(animate);
};
animate();
return () => {
disposed = true;
window.cancelAnimationFrame(frame);
observer.disconnect();
wrap.removeEventListener("pointermove", handlePointerMove);
wrap.removeEventListener("pointerleave", handlePointerLeave);
scene.traverse((object) => {
if (!(object instanceof THREE.Mesh)) return;
object.geometry.dispose();
const material = object.material;
if (Array.isArray(material)) material.forEach((item) => item.dispose());
else material.dispose();
});
renderer.dispose();
};
}, []);
return (
<section id="model" className="relative overflow-hidden py-20 lg:py-28">
<div ref={wrapRef} className="relative left-1/2 min-h-[42rem] w-screen -translate-x-1/2 overflow-hidden lg:min-h-[46rem]" data-testid="glasses-3d-wrap">
<canvas ref={canvasRef} className="absolute inset-0 h-full w-full touch-pan-y" aria-label={t.model.canvasLabel} data-testid="glasses-3d-canvas" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_52%_46%,rgba(255,255,255,0.58),transparent_34%),radial-gradient(circle_at_52%_68%,rgba(49,95,143,0.15),transparent_30%)]" />
<div className="relative z-10 mx-auto max-w-7xl px-4 sm:px-6">
<p className="mb-4 text-xs font-semibold uppercase tracking-[0.28em] text-optical/75">{t.model.eyebrow}</p>
<h2 className="text-4xl font-semibold tracking-[-0.055em] text-ink sm:text-6xl">{t.model.title}</h2>
<p className="mt-5 max-w-2xl text-base leading-8 text-ink/62 sm:text-lg">{t.model.body}</p>
</div>
<Annotation className="left-4 top-[17rem] sm:left-10 lg:left-16 lg:top-[24rem]" align="left" label={t.model.annotations[0].label} body={t.model.annotations[0].body} />
<Annotation className="right-4 top-[33rem] sm:right-10 lg:right-20 lg:top-[24rem]" align="right" label={t.model.annotations[1].label} body={t.model.annotations[1].body} />
<Annotation className="bottom-8 left-6 sm:left-16 lg:bottom-16 lg:left-[42%]" align="left" label={t.model.annotations[2].label} body={t.model.annotations[2].body} />
</div>
</section>
);
}
function Annotation({ label, body, align, className }: { label: string; body: string; align: "left" | "right"; className: string }) {
return (
<motion.div
className={`pointer-events-none absolute z-10 max-w-[14rem] ${className} ${align === "right" ? "text-right" : "text-left"}`}
initial={{ opacity: 0, y: 14 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.6 }}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
>
<div className={`mb-3 flex items-center gap-3 ${align === "right" ? "justify-end" : ""}`}>
{align === "right" ? <span className="h-px w-16 bg-ink/18" /> : null}
<span className="grid size-3 place-items-center rounded-full bg-optical shadow-[0_0_0_7px_rgba(49,95,143,0.12)]" />
{align === "left" ? <span className="h-px w-16 bg-ink/18" /> : null}
</div>
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-optical/80">{label}</p>
<p className="mt-2 text-sm leading-6 text-ink/60">{body}</p>
</motion.div>
);
}
function createGlassesModel() {
const group = new THREE.Group();
group.scale.setScalar(0.9);
const frameMaterial = new THREE.MeshPhysicalMaterial({
color: 0x161a20,
roughness: 0.28,
metalness: 0.55,
clearcoat: 0.85,
clearcoatRoughness: 0.18
});
const lensMaterial = new THREE.MeshPhysicalMaterial({
color: 0xbfd7e3,
roughness: 0.12,
metalness: 0,
transmission: 0.48,
transparent: true,
opacity: 0.28,
thickness: 0.12,
side: THREE.DoubleSide
});
const padMaterial = new THREE.MeshPhysicalMaterial({
color: 0xf8fbff,
roughness: 0.18,
transparent: true,
opacity: 0.52
});
const accentMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.42 });
const ringGeometry = new THREE.TorusGeometry(1.04, 0.065, 18, 112);
const lensGeometry = new THREE.CircleGeometry(0.93, 72);
const hingeGeometry = new THREE.BoxGeometry(0.22, 0.22, 0.22);
const padGeometry = new THREE.SphereGeometry(0.16, 24, 16);
[-1.18, 1.18].forEach((xPosition) => {
const ring = new THREE.Mesh(ringGeometry, frameMaterial);
ring.position.x = xPosition;
ring.scale.y = 0.68;
group.add(ring);
const lens = new THREE.Mesh(lensGeometry, lensMaterial);
lens.position.set(xPosition, 0, -0.03);
lens.scale.set(1, 0.66, 1);
group.add(lens);
const highlight = new THREE.Mesh(new THREE.PlaneGeometry(0.58, 0.045), accentMaterial);
highlight.position.set(xPosition - 0.2, 0.34, 0.02);
highlight.rotation.z = -0.22;
group.add(highlight);
const hinge = new THREE.Mesh(hingeGeometry, frameMaterial);
hinge.position.set(xPosition > 0 ? 2.18 : -2.18, 0.03, -0.02);
hinge.scale.set(0.55, 0.8, 0.62);
group.add(hinge);
});
const bridgeCurve = new THREE.CatmullRomCurve3([
new THREE.Vector3(-0.3, 0.08, 0),
new THREE.Vector3(-0.12, 0.25, 0.04),
new THREE.Vector3(0, 0.3, 0.05),
new THREE.Vector3(0.12, 0.25, 0.04),
new THREE.Vector3(0.3, 0.08, 0)
]);
group.add(new THREE.Mesh(new THREE.TubeGeometry(bridgeCurve, 38, 0.052, 14, false), frameMaterial));
[-0.28, 0.28].forEach((xPosition) => {
const pad = new THREE.Mesh(padGeometry, padMaterial);
pad.position.set(xPosition, -0.22, 0.14);
pad.scale.set(0.55, 0.9, 0.28);
pad.rotation.z = xPosition > 0 ? -0.22 : 0.22;
group.add(pad);
});
const leftTemple = new THREE.CatmullRomCurve3([
new THREE.Vector3(-2.18, 0.03, -0.03),
new THREE.Vector3(-2.75, 0.03, -0.72),
new THREE.Vector3(-3.1, -0.04, -1.75),
new THREE.Vector3(-2.84, -0.28, -2.42)
]);
const rightTemple = new THREE.CatmullRomCurve3([
new THREE.Vector3(2.18, 0.03, -0.03),
new THREE.Vector3(2.75, 0.03, -0.72),
new THREE.Vector3(3.1, -0.04, -1.75),
new THREE.Vector3(2.84, -0.28, -2.42)
]);
group.add(new THREE.Mesh(new THREE.TubeGeometry(leftTemple, 54, 0.055, 14, false), frameMaterial));
group.add(new THREE.Mesh(new THREE.TubeGeometry(rightTemple, 54, 0.055, 14, false), frameMaterial));
const shadow = new THREE.Mesh(
new THREE.CircleGeometry(2.6, 96),
new THREE.MeshBasicMaterial({ color: 0x315f8f, transparent: true, opacity: 0.08, depthWrite: false })
);
shadow.position.set(0, -1.06, -0.92);
shadow.scale.y = 0.18;
group.add(shadow);
group.rotation.set(-0.08, -0.2, 0);
return group;
}

View File

@@ -29,9 +29,9 @@ export default function HeroSection({ t, whatsappUrl }: { t: Messages; whatsappU
<motion.div className="absolute left-1/2 top-24 hidden h-72 w-72 -translate-x-1/2 rounded-full bg-optical/10 blur-3xl lg:block" animate={canAnimateIntro ? { scale: [1, 1.16, 1], opacity: [0.55, 0.88, 0.55] } : undefined} transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }} />
<div className="mx-auto grid max-w-7xl items-center gap-12 lg:grid-cols-[1fr_0.92fr]">
<motion.div initial={canAnimateIntro ? { opacity: 0, y: 24 } : false} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }} className="relative z-10 text-center lg:text-start">
<div className="mx-auto mb-6 inline-flex items-center gap-2 rounded-full border border-ink/10 bg-white/65 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-ink/60 shadow-sm backdrop-blur lg:mx-0">
<ShieldCheck size={15} className="text-optical" />
{t.hero.eyebrow}
<div className="mx-auto mb-5 inline-flex max-w-full items-center gap-1.5 rounded-full border border-ink/10 bg-white/65 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.12em] text-ink/60 shadow-sm backdrop-blur max-[374px]:px-2.5 max-[374px]:text-[9px] max-[374px]:tracking-[0.08em] sm:mb-6 sm:gap-2 sm:px-4 sm:py-2 sm:text-xs sm:tracking-[0.18em] lg:mx-0">
<ShieldCheck className="size-3.5 shrink-0 text-optical sm:size-[15px]" />
<span className="whitespace-nowrap leading-4">{t.hero.eyebrow}</span>
</div>
<h1 className="mx-auto max-w-4xl text-5xl font-semibold tracking-[-0.075em] text-ink sm:text-7xl lg:mx-0 lg:text-8xl">{t.hero.title}</h1>
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-ink/64 sm:text-xl lg:mx-0">{t.hero.subtitle}</p>

View File

@@ -26,10 +26,10 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc
}
return (
<header className="fixed inset-x-0 top-0 z-50 px-4 pt-4 sm:px-6">
<nav className="glass premium-glass mx-auto flex max-w-7xl items-center justify-between gap-2 rounded-full px-3 py-3 sm:px-5" aria-label="Main navigation">
<a href="#home" onClick={(event) => scrollToSection(event, "#home")} className="relative z-[3] flex items-center gap-3" aria-label="New Optic home">
<span className="relative grid size-10 place-items-center overflow-hidden rounded-full bg-white shadow-sm">
<header className="fixed inset-x-0 top-0 z-50 px-4 pt-4 max-[374px]:px-2.5 sm:px-6">
<nav className="glass premium-glass nav-glass relative mx-auto flex max-w-7xl items-center justify-start gap-2 overflow-hidden rounded-full px-3 py-3 max-[374px]:px-2 max-[374px]:py-2.5 md:justify-between sm:px-5" aria-label="Main navigation">
<a href="#home" onClick={(event) => scrollToSection(event, "#home")} className="relative z-[3] flex shrink-0 items-center gap-3" aria-label="New Optic home">
<span className="relative grid size-10 place-items-center overflow-hidden rounded-full bg-white shadow-sm max-[374px]:size-9">
<Image src={business.assets.logo} alt="New Optic logo" fill sizes="40px" className="object-contain p-1" priority />
</span>
<span className="hidden text-sm font-semibold tracking-[-0.02em] xs:inline sm:inline">{business.name}</span>
@@ -48,9 +48,9 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc
</PhysicsButton>
</div>
<div className="relative z-[3] flex items-center gap-2 md:hidden">
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} className="bg-white/80 backdrop-blur-none" buttonClassName="px-2 sm:px-3" />
<PhysicsButton href={whatsappUrl} external className={`${navCtaClassName} px-3 py-2.5 text-xs max-[374px]:hidden sm:px-5 sm:text-sm`}>
<div className="relative z-[3] flex min-w-0 flex-1 items-center gap-1.5 max-[374px]:gap-0.5 sm:gap-2 md:hidden">
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} className="w-[132px] shrink-0 bg-white/72 backdrop-blur-xl max-[374px]:w-[106px] max-[374px]:p-0.5" buttonClassName="flex-1 px-0.5 max-[374px]:px-0 max-[374px]:py-1.5 sm:px-3" />
<PhysicsButton href={whatsappUrl} external className={`${navCtaClassName} min-w-0 flex-1 px-2.5 py-2 text-[11px] max-[374px]:px-1.5 max-[374px]:py-1.5 max-[374px]:text-[9px] sm:px-5 sm:py-2.5 sm:text-sm`}>
{t.nav.cta}
</PhysicsButton>
<motion.button
@@ -58,7 +58,7 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc
onClick={() => setOpen((value) => !value)}
whileTap={{ scale: 0.94 }}
transition={{ duration: 0.08 }}
className="grid size-11 place-items-center rounded-full bg-white/70 text-ink outline-none transition focus-visible:ring-2 focus-visible:ring-optical/45 focus-visible:ring-offset-2"
className="grid size-11 shrink-0 place-items-center rounded-full bg-white/70 text-ink outline-none transition max-[374px]:size-9 focus-visible:ring-2 focus-visible:ring-optical/45 focus-visible:ring-offset-2"
aria-label={t.nav.menu}
aria-expanded={open}
>

View File

@@ -7,7 +7,6 @@ import AboutSection from "./AboutSection";
import ContactSection from "./ContactSection";
import CollectionsSection from "./CollectionsSection";
import Footer from "./Footer";
import GlassesModelSection from "./GlassesModelSection";
import HeroSection from "./HeroSection";
import LiquidGlass from "./LiquidGlass";
import Navbar from "./Navbar";
@@ -39,7 +38,6 @@ export default function SiteShell() {
<Navbar locale={locale} onLocaleChange={changeLocale} t={t} whatsappUrl={whatsappUrl} />
<main>
<HeroSection t={t} whatsappUrl={whatsappUrl} />
<GlassesModelSection t={t} />
<AboutSection t={t} />
<ServicesSection t={t} />
<CollectionsSection t={t} />

View File

@@ -5,7 +5,7 @@ import SectionHeader from "./SectionHeader";
export default function TrustSection({ t }: { t: Messages }) {
return (
<AnimatedSection id="trust" className="px-4 py-20 sm:px-6">
<AnimatedSection id="trust" className="px-4 pb-8 pt-16 sm:px-6 sm:py-20">
<div className="mx-auto max-w-7xl overflow-hidden rounded-[3rem] bg-ink p-8 text-white shadow-glass sm:p-12 lg:p-16">
<SectionHeader eyebrow={t.trust.eyebrow} title={t.trust.title} body={t.trust.body} tone="dark" />
<motion.div className="mt-12 grid gap-4 md:grid-cols-3" initial={false} whileInView="show" viewport={{ once: true, amount: 0.08 }} variants={{ show: { transition: { staggerChildren: 0.12 } } }}>

View File

@@ -29,17 +29,6 @@ export const ar = {
body: "منذ حوالي 2004، يرافق نيو أوبتيك زبناء تمارة بطريقة بسيطة: نصيحة واضحة، نظارات أصلية، أثمنة منصفة وخدمة متابعة بعد الشراء.",
cards: ["سمعة محلية قوية", "نظارات أصلية فقط", "مواكبة من اختيار الإطار حتى الضمان"]
},
model: {
eyebrow: "معاينة ثلاثية الأبعاد",
title: "شوف التفاصيل قبل الاختيار.",
body: "معاينة تفاعلية كتوضح المهم: شكل الإطار، الراحة، العدسات والتعديل النهائي.",
canvasLabel: "نموذج ثلاثي الأبعاد لنظارات مع ملاحظات",
annotations: [
{ label: "العدسات", body: "نصيحة في العدسات المناسبة للاستعمال اليومي." },
{ label: "الإطار", body: "إطارات أصلية بجودة وتشطيب واضح." },
{ label: "التعديل", body: "تعديل في المحل لراحة أفضل كل يوم." }
]
},
services: {
eyebrow: "الخدمات",
title: "كل ما تحتاجه لنظاراتك في مكان واحد.",

View File

@@ -29,17 +29,6 @@ export const en = {
body: "Since around 2004, New Optic has supported customers in Temara with a simple promise: honest guidance, authentic frames, fair prices and reliable after-sales support.",
cards: ["Strong local reputation", "Original frames only", "Guidance from frame to warranty"]
},
model: {
eyebrow: "3D inspection",
title: "See the details before choosing.",
body: "An interactive preview for the essentials: shape, comfort, lenses and the final fit.",
canvasLabel: "3D glasses model with annotations",
annotations: [
{ label: "Lenses", body: "Guidance on lenses suited to everyday use." },
{ label: "Frame", body: "Original frames with a clean, durable finish." },
{ label: "Fit", body: "In-store adjustment for daily comfort." }
]
},
services: {
eyebrow: "Services",
title: "Everything essential for your eyewear, in one place.",

View File

@@ -29,17 +29,6 @@ export const fr = {
body: "Depuis 2004, New Optic accompagne les clients de Temara avec une approche claire: bien conseiller, proposer des montures authentiques, garder des prix justes et assurer le suivi apres l'achat.",
cards: ["Reputation locale solide", "Montures originales uniquement", "Conseil de la monture a la garantie"]
},
model: {
eyebrow: "Inspection 3D",
title: "Regardez les details avant de choisir.",
body: "Un apercu interactif pour montrer ce qui compte vraiment: la forme, le confort, les verres et l'ajustement final.",
canvasLabel: "Modele 3D de lunettes avec annotations",
annotations: [
{ label: "Verres", body: "Conseil sur les verres adaptes a votre usage." },
{ label: "Monture", body: "Selection de montures originales et bien finies." },
{ label: "Ajustement", body: "Reglage en magasin pour le confort au quotidien." }
]
},
services: {
eyebrow: "Services",
title: "Tout l'essentiel pour vos lunettes, au meme endroit.",

68
package-lock.json generated
View File

@@ -12,14 +12,12 @@
"lucide-react": "^0.468.0",
"next": "^16.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"three": "^0.184.0"
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@types/three": "^0.184.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-config-next": "^16.0.0",
@@ -281,13 +279,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@dimforge/rapier3d-compat": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -1268,13 +1259,6 @@
"tslib": "^2.8.0"
}
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true,
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -1337,35 +1321,6 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/three": {
"version": "0.184.1",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz",
"integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": ">=0.5.17",
"fflate": "~0.8.2",
"meshoptimizer": "~1.1.1"
}
},
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
@@ -3456,13 +3411,6 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"dev": true,
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4641,13 +4589,6 @@
"node": ">= 8"
}
},
"node_modules/meshoptimizer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
"integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -5143,6 +5084,7 @@
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -6178,12 +6120,6 @@
"node": ">=0.8"
}
},
"node_modules/three": {
"version": "0.184.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
"integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",

View File

@@ -13,14 +13,12 @@
"lucide-react": "^0.468.0",
"next": "^16.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"three": "^0.184.0"
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@types/three": "^0.184.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-config-next": "^16.0.0",

View File

@@ -11,7 +11,8 @@ const config: Config = {
optical: "#315f8f"
},
fontFamily: {
sans: ["var(--font-sans)", "Inter", "system-ui", "sans-serif"],
sans: ["var(--font-sans)"],
display: ["var(--font-display)"],
arabic: ["var(--font-arabic)", "system-ui", "sans-serif"]
},
boxShadow: {