feat: add custom cursor, navbar responsiveness, and active section indicator
This commit is contained in:
@@ -41,6 +41,53 @@ Custom utility class `text-huge` uses `clamp(2rem, 8vw, 8rem)` for responsive he
|
||||
|
||||
Scroll snap: `.snap-container` (y proximity) + `.snap-section` (align start) on homepage sections.
|
||||
|
||||
## Navbar
|
||||
|
||||
**Responsive breakpoints**: The navbar uses `lg` (1024px) as the mobile/desktop toggle. Between `lg` and `xl` (1280px), text sizes, gaps, and padding scale down to prevent overflow. Mobile (<1024px) has compact padding and logo.
|
||||
|
||||
**Active section indicator**: Scroll-based detection (not IntersectionObserver — sections are dynamically imported with `ssr: false` so they don't exist at mount time). A sliding `h-1 bg-primary` bar under the desktop nav links animates between sections using CSS transitions. Only visible on homepage (`pathname === "/"`). Recalculates position on scroll state change and window resize.
|
||||
|
||||
## Custom Cursor (`CustomCursor.tsx`)
|
||||
|
||||
A GSAP-powered custom cursor rendered in `layout.tsx` (direct import, not `next/dynamic` — `ssr: false` is not allowed in Server Component layouts in Next.js 16). Desktop-only (`pointer: fine`), disabled for `prefers-reduced-motion`.
|
||||
|
||||
**Default shape**: Terminal-style block (22×22px, 5px radius), white fill + 2px outline ring with 1px gap. Uses `mix-blend-mode: difference` for automatic color inversion against any background.
|
||||
|
||||
**Mouse tracking**: Uses `gsap.to` with `overwrite: "auto"` for all positioning. Does NOT use `gsap.quickTo` — it conflicts with `gsap.to` on the same x/y properties during morph transitions (causes "not eligible for reset" errors).
|
||||
|
||||
**Three interaction modes** (opt-in via `data-cursor` attribute):
|
||||
|
||||
### `data-cursor="hug"` — Hug mode
|
||||
- Cursor morphs into a transparent outline frame around the element with 6px padding
|
||||
- Element scales to 1.05 with glow (`box-shadow` via `.cursor-hugged` class in `globals.css`)
|
||||
- Click bounce: mousedown → scale(0.95), mouseup → scale(1.08) → scale(1.05) spring
|
||||
- Border-radius resolved from element, then first child (for wrapper divs like the About photo), fallback to pill (`100px`)
|
||||
- Used on: hero CTAs, navbar desktop links + Contattaci button, CtaSection button, contatti page phone/email links, footer social icons, About section photo
|
||||
|
||||
### `data-cursor="underline"` — Underline mode
|
||||
- Cursor morphs into a 3px-tall bar under the `<h3>` question text
|
||||
- Used on: FAQ section question items
|
||||
|
||||
### FAQ split/merge (underline + block cursor)
|
||||
- When a FAQ accordion opens (`data-faq-open` attribute detected via MutationObserver):
|
||||
1. A phantom div takes over the underline position (stays as visual anchor)
|
||||
2. Main cursor pulses (thickens to 6px), then peels off as a block cursor
|
||||
3. Block cursor is free to follow the mouse through the answer text
|
||||
- When the FAQ closes: block cursor flies back up to the underline, phantom fades out, cursor becomes the underline again
|
||||
- Switching FAQs: block cursor flies directly to the new question's underline
|
||||
|
||||
### Shared behaviors
|
||||
- **Rubber-band pull**: While morphed (hug or underline), cursor applies 15% of mouse-to-element-center distance as pull offset. Gives an elastic "trying to follow" feel.
|
||||
- **Scroll tracking**: `scroll` event recalculates morph position from `getBoundingClientRect()`. Both the main cursor and phantom underline update on scroll.
|
||||
- **Z-index**: Walks the full ancestor chain and uses the highest explicit z-index + 1. Navbar elements get z-51 (above backdrop blur). Body content gets z-1 (below navbar). Default circle mode stays at z-9999.
|
||||
- **Edge cases**: Cursor hidden until first mousemove. Fades out when mouse leaves window. Event delegation via `closest()` — no cached DOM refs (SPA-safe).
|
||||
|
||||
### Files involved
|
||||
- `src/components/CustomCursor.tsx` — All cursor logic (self-contained client component)
|
||||
- `src/app/globals.css` — `cursor: none` media query, `.cursor-hugged` glow class
|
||||
- `src/app/layout.tsx` — Renders `<CustomCursor />` as last child in `<body>`
|
||||
- `src/components/sections/FaqSection.tsx` — `data-cursor="underline"`, `data-faq-open` attribute
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Path alias: `@/*` maps to `./src/*`
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- [Custom Cursor Feature](feature_custom_cursor.md) — Custom cursor with hug behavior, tracking, and click bounce animations
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Custom Cursor Feature Implementation
|
||||
description: Custom cursor with hug behavior and click bounce animations using GSAP
|
||||
type: project
|
||||
---
|
||||
|
||||
**Feature**: Custom cursor with smooth tracking, element hug behavior, and click animations
|
||||
|
||||
**Implementation details**:
|
||||
- Component: `/frontend/src/components/CustomCursor.tsx` — "use client" component
|
||||
- Styling: CSS rules added to `/frontend/src/app/globals.css`
|
||||
- Layout integration: Loaded via `next/dynamic` with `ssr: false` in `/frontend/src/app/layout.tsx`
|
||||
|
||||
**How it works**:
|
||||
- 35px white donut ring cursor (white fill + 2px outline with 1px gap)
|
||||
- Smooth tracking with `gsap.quickTo` (0.15s duration, power2.out easing)
|
||||
- Device detection: requires `(pointer: fine)` and `prefers-reduced-motion: no-preference`
|
||||
- Event delegation (no DOM caching) — uses `closest('[data-cursor="hug"]')` for element detection
|
||||
|
||||
**Hug behavior** (on elements with `data-cursor="hug"`):
|
||||
- Element with transparent background: cursor fills element (opacity 0.15 as fill), applies blue glow to element
|
||||
- Element with visible background: cursor becomes oversized glow border around element
|
||||
- Both: scale element to 1.05 during hover, restore on leave
|
||||
- All animations use GSAP timelines (0.3s duration, power3.out easing)
|
||||
|
||||
**Click bounce**:
|
||||
- `mousedown`: scale 0.95 (0.1s, power2.in)
|
||||
- `mouseup`: scale 1.08 (0.15s, back.out(2)), then scale 1.05 (0.3s, power2.out)
|
||||
|
||||
**Elements with hug behavior** (data-cursor="hug" added to):
|
||||
- HeroSection: both CTA links
|
||||
- Navbar: desktop nav links + desktop "Contattaci" button (NOT mobile menu)
|
||||
- CtaSection: "Contattaci" button
|
||||
- contatti/page.tsx: phone number and email links (prominent ones only)
|
||||
|
||||
**Styling rules**:
|
||||
- Native cursor hidden on desktop via media query (pointer: fine)
|
||||
- `.cursor-hugged` class applies `box-shadow: 0 0 25px rgba(0, 1, 187, 0.25)` during hug state
|
||||
@@ -1,6 +1,23 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* Enable SVG imports as React components via @svgr/webpack */
|
||||
turbopack: {
|
||||
rules: {
|
||||
'*.svg': {
|
||||
loaders: ['@svgr/webpack'],
|
||||
as: '*.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ['@svgr/webpack'],
|
||||
});
|
||||
return config;
|
||||
},
|
||||
|
||||
/* Image optimization for better performance */
|
||||
images: {
|
||||
formats: ["image/avif", "image/webp"],
|
||||
|
||||
Generated
+2297
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
@@ -11,7 +11,7 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function Contatti() {
|
||||
return (
|
||||
<div className="pt-24 sm:pt-32 pb-16 sm:pb-24 px-6 lg:px-8 max-w-7xl mx-auto overflow-x-hidden">
|
||||
<div className="pt-36 sm:pt-44 pb-16 sm:pb-24 px-6 lg:px-8 max-w-7xl mx-auto overflow-x-hidden">
|
||||
<MaterialSymbolsFont />
|
||||
{/* Hero Section - Priority render */}
|
||||
<section className="grid grid-cols-1 lg:grid-cols-12 mb-16 lg:mb-24">
|
||||
@@ -46,6 +46,7 @@ export default function Contatti() {
|
||||
<a
|
||||
className="text-xl sm:text-3xl lg:text-4xl font-black tracking-tighter text-on-surface hover:text-primary transition-colors duration-300"
|
||||
href="tel:+393382451171"
|
||||
data-cursor="hug"
|
||||
>
|
||||
+39 338 245 1171
|
||||
</a>
|
||||
@@ -57,6 +58,7 @@ export default function Contatti() {
|
||||
<a
|
||||
className="text-xl sm:text-3xl lg:text-4xl font-black tracking-tighter text-on-surface hover:text-primary transition-colors duration-300 underline decoration-2 underline-offset-8"
|
||||
href="mailto:info@cimaprogetti.it"
|
||||
data-cursor="hug"
|
||||
>
|
||||
info@cimaprogetti.it
|
||||
</a>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
@theme inline {
|
||||
--font-sans: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--font-heading: var(--font-heading), "IBM Plex Mono", monospace;
|
||||
|
||||
--color-background: #fcf9f8;
|
||||
--color-on-background: #1b1c1c;
|
||||
@@ -56,7 +57,9 @@ html {
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-background);
|
||||
background: linear-gradient(90deg, rgba(238,238,238,0.2) 0%, rgba(238,238,238,0.2) 100%), url('/images/body-texture.webp'), var(--color-background);
|
||||
background-attachment: fixed;
|
||||
background-size: cover;
|
||||
color: var(--color-on-background);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -116,6 +119,22 @@ img {
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
|
||||
/* Scroll-down cue animation */
|
||||
@keyframes scroll-down {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(8px);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-scroll-down {
|
||||
animation: scroll-down 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Skeleton wave animation */
|
||||
@keyframes wave {
|
||||
0% {
|
||||
@@ -169,3 +188,23 @@ img {
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Custom cursor - hide native cursor on desktop */
|
||||
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
|
||||
html,
|
||||
body {
|
||||
cursor: none;
|
||||
}
|
||||
a,
|
||||
button,
|
||||
[role="button"],
|
||||
[data-cursor] {
|
||||
cursor: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hug state glow */
|
||||
.cursor-hugged {
|
||||
box-shadow: 0 0 25px rgba(0, 1, 187, 0.25);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { IBM_Plex_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Footer from "@/components/Footer";
|
||||
import CustomCursor from "@/components/CustomCursor";
|
||||
|
||||
const ibmPlexMono = IBM_Plex_Mono({
|
||||
subsets: ["latin"],
|
||||
weight: ["700"],
|
||||
variable: "--font-heading",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
@@ -35,7 +44,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="it" className="scroll-smooth mb:overflow-x-hidden">
|
||||
<html lang="it" className={`scroll-smooth mb:overflow-x-hidden ${ibmPlexMono.variable}`}>
|
||||
<head>
|
||||
{/* Preconnect to Google Fonts — used by Material Symbols loaded lazily */}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
@@ -45,6 +54,7 @@ export default function RootLayout({
|
||||
<Navbar />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
<CustomCursor />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import gsap from "gsap";
|
||||
|
||||
const CURSOR_W = 22;
|
||||
const CURSOR_H = 22;
|
||||
const HALF_W = CURSOR_W / 2;
|
||||
const HALF_H = CURSOR_H / 2;
|
||||
const CURSOR_RADIUS = "5px";
|
||||
const HUG_PADDING = 6;
|
||||
const MIN_BORDER_RADIUS = "100px";
|
||||
const PULL_STRENGTH = 0.15;
|
||||
const Z_NORMAL = 9999;
|
||||
|
||||
export default function CustomCursor() {
|
||||
const cursorRef = useRef<HTMLDivElement>(null);
|
||||
const phantomRef = useRef<HTMLDivElement>(null);
|
||||
const activeTargetRef = useRef<HTMLElement | null>(null);
|
||||
const activeTypeRef = useRef<"hug" | "underline" | null>(null);
|
||||
const timelineRef = useRef<gsap.core.Timeline | null>(null);
|
||||
const isMorphedRef = useRef(false);
|
||||
const isSplitRef = useRef(false); // true when underline + block cursor are both visible
|
||||
const mouseRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const morphRef = useRef<{
|
||||
baseX: number;
|
||||
baseY: number;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const hasFinePointer = window.matchMedia("(pointer: fine)").matches;
|
||||
const prefersAnimation = window.matchMedia(
|
||||
"(prefers-reduced-motion: no-preference)"
|
||||
).matches;
|
||||
|
||||
if (!hasFinePointer || !prefersAnimation || !cursorRef.current || !phantomRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = cursorRef.current;
|
||||
const phantom = phantomRef.current;
|
||||
|
||||
// --- Helpers ---
|
||||
const killTimeline = () => {
|
||||
if (timelineRef.current) {
|
||||
timelineRef.current.kill();
|
||||
timelineRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveZIndex = (target: HTMLElement): number => {
|
||||
let el: HTMLElement | null = target;
|
||||
let maxZ = -1;
|
||||
while (el) {
|
||||
const z = window.getComputedStyle(el).zIndex;
|
||||
if (z !== "auto") {
|
||||
const parsed = parseInt(z, 10);
|
||||
if (parsed > maxZ) maxZ = parsed;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
return maxZ >= 0 ? maxZ + 1 : 1;
|
||||
};
|
||||
|
||||
const resolveBorderRadius = (target: HTMLElement): string => {
|
||||
const style = window.getComputedStyle(target);
|
||||
const parsed = parseFloat(style.borderRadius);
|
||||
if (!isNaN(parsed) && parsed >= 8) return style.borderRadius;
|
||||
|
||||
const firstChild = target.firstElementChild as HTMLElement | null;
|
||||
if (firstChild) {
|
||||
const childStyle = window.getComputedStyle(firstChild);
|
||||
const childParsed = parseFloat(childStyle.borderRadius);
|
||||
if (!isNaN(childParsed) && childParsed >= 4) return childStyle.borderRadius;
|
||||
}
|
||||
|
||||
return MIN_BORDER_RADIUS;
|
||||
};
|
||||
|
||||
const updateMorphPosition = (instant?: boolean) => {
|
||||
const target = activeTargetRef.current;
|
||||
const type = activeTypeRef.current;
|
||||
if (!target) return;
|
||||
if (!isMorphedRef.current && !isSplitRef.current) return;
|
||||
|
||||
if (type === "hug" && isMorphedRef.current) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
const baseX = rect.left - HUG_PADDING;
|
||||
const baseY = rect.top - HUG_PADDING;
|
||||
morphRef.current = {
|
||||
baseX,
|
||||
baseY,
|
||||
centerX: rect.left + rect.width / 2,
|
||||
centerY: rect.top + rect.height / 2,
|
||||
};
|
||||
|
||||
const m = mouseRef.current;
|
||||
const pullX = (m.x - morphRef.current.centerX) * PULL_STRENGTH;
|
||||
const pullY = (m.y - morphRef.current.centerY) * PULL_STRENGTH;
|
||||
gsap.to(cursor, {
|
||||
x: baseX + pullX,
|
||||
y: baseY + pullY,
|
||||
duration: instant ? 0 : 0.15,
|
||||
ease: "power2.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
} else if (type === "underline" && isMorphedRef.current && !isSplitRef.current) {
|
||||
// Only track underline position when NOT split (when split, cursor is free)
|
||||
const textEl = target.querySelector("h3") || target;
|
||||
const rect = textEl.getBoundingClientRect();
|
||||
const baseX = rect.left;
|
||||
const baseY = rect.bottom + 4;
|
||||
morphRef.current = {
|
||||
baseX,
|
||||
baseY,
|
||||
centerX: rect.left + rect.width / 2,
|
||||
centerY: rect.top + rect.height / 2,
|
||||
};
|
||||
|
||||
const m = mouseRef.current;
|
||||
const pullX = (m.x - morphRef.current.centerX) * PULL_STRENGTH;
|
||||
const pullY = (m.y - morphRef.current.centerY) * PULL_STRENGTH;
|
||||
gsap.to(cursor, {
|
||||
x: baseX + pullX,
|
||||
y: baseY + pullY,
|
||||
duration: instant ? 0 : 0.15,
|
||||
ease: "power2.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
}
|
||||
|
||||
// Also update phantom position if split
|
||||
if (isSplitRef.current && type === "underline") {
|
||||
const textEl = target.querySelector("h3") || target;
|
||||
const rect = textEl.getBoundingClientRect();
|
||||
gsap.to(phantom, {
|
||||
x: rect.left,
|
||||
y: rect.bottom + 4,
|
||||
width: rect.width,
|
||||
duration: instant ? 0 : 0.15,
|
||||
ease: "power2.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Hide phantom
|
||||
const hidePhantom = () => {
|
||||
gsap.to(phantom, { opacity: 0, duration: 0.15, overwrite: "auto" });
|
||||
isSplitRef.current = false;
|
||||
};
|
||||
|
||||
const resetCursor = () => {
|
||||
killTimeline();
|
||||
isMorphedRef.current = false;
|
||||
morphRef.current = null;
|
||||
if (isSplitRef.current) hidePhantom();
|
||||
gsap.to(cursor, {
|
||||
x: mouseRef.current.x - HALF_W,
|
||||
y: mouseRef.current.y - HALF_H,
|
||||
width: CURSOR_W,
|
||||
height: CURSOR_H,
|
||||
borderRadius: CURSOR_RADIUS,
|
||||
background: "white",
|
||||
outline: "2px solid white",
|
||||
outlineOffset: "1px",
|
||||
opacity: 1,
|
||||
mixBlendMode: "difference",
|
||||
zIndex: Z_NORMAL,
|
||||
duration: 0.3,
|
||||
ease: "power3.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
};
|
||||
|
||||
// --- Split: underline spawns a block cursor ---
|
||||
const splitCursor = (target: HTMLElement) => {
|
||||
if (isSplitRef.current) return;
|
||||
isSplitRef.current = true;
|
||||
|
||||
const textEl = target.querySelector("h3") || target;
|
||||
const rect = textEl.getBoundingClientRect();
|
||||
const zIndex = resolveZIndex(target);
|
||||
const underlineX = rect.left;
|
||||
const underlineY = rect.bottom + 4;
|
||||
|
||||
// 1. Transfer the underline shape to the phantom
|
||||
gsap.set(phantom, {
|
||||
x: underlineX,
|
||||
y: underlineY,
|
||||
width: rect.width,
|
||||
height: 3,
|
||||
borderRadius: "2px",
|
||||
opacity: 1,
|
||||
zIndex: zIndex,
|
||||
});
|
||||
|
||||
// 2. Pulse the main cursor (underline thickens briefly at mouse X)
|
||||
const tl = gsap.timeline();
|
||||
timelineRef.current = tl;
|
||||
|
||||
// Brief pulse at mouse position
|
||||
tl.to(cursor, {
|
||||
height: 6,
|
||||
y: underlineY - 1.5,
|
||||
duration: 0.1,
|
||||
ease: "power2.out",
|
||||
});
|
||||
|
||||
// 3. Peel off: cursor morphs back to block shape, drops down to mouse
|
||||
tl.to(cursor, {
|
||||
x: mouseRef.current.x - HALF_W,
|
||||
y: underlineY + 20, // drop below the underline
|
||||
width: CURSOR_W,
|
||||
height: CURSOR_H,
|
||||
borderRadius: CURSOR_RADIUS,
|
||||
background: "white",
|
||||
outline: "2px solid white",
|
||||
outlineOffset: "1px",
|
||||
mixBlendMode: "difference",
|
||||
zIndex: Z_NORMAL,
|
||||
duration: 0.2,
|
||||
ease: "power2.out",
|
||||
});
|
||||
|
||||
// After the split animation, cursor is free to follow the mouse
|
||||
tl.call(() => {
|
||||
isMorphedRef.current = false;
|
||||
morphRef.current = null;
|
||||
});
|
||||
};
|
||||
|
||||
// --- Merge: block cursor returns to underline ---
|
||||
const mergeCursor = (target: HTMLElement) => {
|
||||
if (!isSplitRef.current) return;
|
||||
|
||||
const textEl = target.querySelector("h3") || target;
|
||||
const rect = textEl.getBoundingClientRect();
|
||||
const zIndex = resolveZIndex(target);
|
||||
const underlineX = rect.left;
|
||||
const underlineY = rect.bottom + 4;
|
||||
|
||||
killTimeline();
|
||||
const tl = gsap.timeline();
|
||||
timelineRef.current = tl;
|
||||
|
||||
// Cursor flies back up to the underline position
|
||||
tl.to(cursor, {
|
||||
x: underlineX,
|
||||
y: underlineY,
|
||||
width: rect.width,
|
||||
height: 3,
|
||||
borderRadius: "2px",
|
||||
background: "white",
|
||||
outline: "none",
|
||||
outlineOffset: "0px",
|
||||
mixBlendMode: "difference",
|
||||
zIndex: zIndex,
|
||||
duration: 0.2,
|
||||
ease: "power2.in",
|
||||
});
|
||||
|
||||
// Fade out phantom as cursor arrives
|
||||
tl.to(phantom, {
|
||||
opacity: 0,
|
||||
duration: 0.1,
|
||||
}, "-=0.05");
|
||||
|
||||
// Set state: cursor is now the underline again
|
||||
tl.call(() => {
|
||||
isSplitRef.current = false;
|
||||
isMorphedRef.current = true;
|
||||
morphRef.current = {
|
||||
baseX: underlineX,
|
||||
baseY: underlineY,
|
||||
centerX: rect.left + rect.width / 2,
|
||||
centerY: rect.top + rect.height / 2,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// --- Mouse tracking ---
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
mouseRef.current = { x: e.clientX, y: e.clientY };
|
||||
|
||||
if (isMorphedRef.current && morphRef.current && !isSplitRef.current) {
|
||||
// Morphed and not split — rubber-band to element
|
||||
const m = morphRef.current;
|
||||
const pullX = (e.clientX - m.centerX) * PULL_STRENGTH;
|
||||
const pullY = (e.clientY - m.centerY) * PULL_STRENGTH;
|
||||
gsap.to(cursor, {
|
||||
x: m.baseX + pullX,
|
||||
y: m.baseY + pullY,
|
||||
duration: 0.25,
|
||||
ease: "power2.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
} else if (!isMorphedRef.current) {
|
||||
// Free cursor (normal or split mode)
|
||||
gsap.to(cursor, {
|
||||
x: e.clientX - HALF_W,
|
||||
y: e.clientY - HALF_H,
|
||||
duration: 0.15,
|
||||
ease: "power2.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
}
|
||||
|
||||
if (cursor.style.opacity === "0") {
|
||||
gsap.to(cursor, { opacity: 1, duration: 0.3 });
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!activeTargetRef.current) return;
|
||||
if (isMorphedRef.current || isSplitRef.current) {
|
||||
updateMorphPosition();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDocEnter = () => {
|
||||
gsap.to(cursor, { opacity: 1, duration: 0.3 });
|
||||
};
|
||||
|
||||
const handleDocLeave = () => {
|
||||
gsap.to(cursor, { opacity: 0, duration: 0.3 });
|
||||
};
|
||||
|
||||
// --- Hug enter ---
|
||||
const enterHug = (target: HTMLElement) => {
|
||||
const rect = target.getBoundingClientRect();
|
||||
const borderRadius = resolveBorderRadius(target);
|
||||
const zIndex = resolveZIndex(target);
|
||||
|
||||
// If we were split, clean up phantom first
|
||||
if (isSplitRef.current) hidePhantom();
|
||||
|
||||
killTimeline();
|
||||
isMorphedRef.current = true;
|
||||
|
||||
const baseX = rect.left - HUG_PADDING;
|
||||
const baseY = rect.top - HUG_PADDING;
|
||||
morphRef.current = {
|
||||
baseX,
|
||||
baseY,
|
||||
centerX: rect.left + rect.width / 2,
|
||||
centerY: rect.top + rect.height / 2,
|
||||
};
|
||||
|
||||
timelineRef.current = gsap.timeline();
|
||||
|
||||
timelineRef.current.to(cursor, {
|
||||
x: baseX,
|
||||
y: baseY,
|
||||
width: rect.width + HUG_PADDING * 2,
|
||||
height: rect.height + HUG_PADDING * 2,
|
||||
borderRadius: borderRadius,
|
||||
background: "transparent",
|
||||
outline: "2px solid white",
|
||||
outlineOffset: "0px",
|
||||
mixBlendMode: "difference",
|
||||
opacity: 1,
|
||||
zIndex: zIndex,
|
||||
duration: 0.3,
|
||||
ease: "power3.out",
|
||||
}, 0);
|
||||
|
||||
target.classList.add("cursor-hugged");
|
||||
|
||||
timelineRef.current.to(
|
||||
target,
|
||||
{ scale: 1.05, duration: 0.3, ease: "power3.out" },
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const leaveHug = (target: HTMLElement) => {
|
||||
target.classList.remove("cursor-hugged");
|
||||
gsap.to(target, { scale: 1, duration: 0.3, ease: "power3.out" });
|
||||
resetCursor();
|
||||
};
|
||||
|
||||
// --- Underline enter ---
|
||||
const enterUnderline = (target: HTMLElement) => {
|
||||
const textEl = target.querySelector("h3") || target;
|
||||
const rect = textEl.getBoundingClientRect();
|
||||
const zIndex = resolveZIndex(target);
|
||||
|
||||
// If we were split from a different FAQ, clean up
|
||||
if (isSplitRef.current) hidePhantom();
|
||||
|
||||
killTimeline();
|
||||
isMorphedRef.current = true;
|
||||
|
||||
const baseX = rect.left;
|
||||
const baseY = rect.bottom + 4;
|
||||
morphRef.current = {
|
||||
baseX,
|
||||
baseY,
|
||||
centerX: rect.left + rect.width / 2,
|
||||
centerY: rect.top + rect.height / 2,
|
||||
};
|
||||
|
||||
timelineRef.current = gsap.timeline();
|
||||
|
||||
timelineRef.current.to(cursor, {
|
||||
x: baseX,
|
||||
y: baseY,
|
||||
width: rect.width,
|
||||
height: 3,
|
||||
borderRadius: "2px",
|
||||
background: "white",
|
||||
outline: "none",
|
||||
outlineOffset: "0px",
|
||||
mixBlendMode: "difference",
|
||||
opacity: 1,
|
||||
zIndex: zIndex,
|
||||
duration: 0.3,
|
||||
ease: "power3.out",
|
||||
}, 0);
|
||||
|
||||
// If this FAQ is already open, immediately split
|
||||
if (target.hasAttribute("data-faq-open")) {
|
||||
timelineRef.current.call(() => {
|
||||
splitCursor(target);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const leaveUnderline = () => {
|
||||
if (isSplitRef.current) hidePhantom();
|
||||
resetCursor();
|
||||
};
|
||||
|
||||
// --- MutationObserver: watch for data-faq-open changes ---
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "attributes" || mutation.attributeName !== "data-faq-open") continue;
|
||||
const target = mutation.target as HTMLElement;
|
||||
|
||||
// Only act if this is the currently active underline target
|
||||
if (target !== activeTargetRef.current || activeTypeRef.current !== "underline") continue;
|
||||
|
||||
if (target.hasAttribute("data-faq-open")) {
|
||||
// FAQ opened — split
|
||||
splitCursor(target);
|
||||
} else {
|
||||
// FAQ closed — merge back
|
||||
mergeCursor(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Observe the whole document for data-faq-open changes (FAQ items are dynamic)
|
||||
observer.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-faq-open"],
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
// --- Mouse over/out delegation ---
|
||||
const handleMouseOver = (e: MouseEvent) => {
|
||||
const el = e.target as HTMLElement;
|
||||
|
||||
const hugTarget = el.closest<HTMLElement>('[data-cursor="hug"]');
|
||||
const underlineTarget = el.closest<HTMLElement>('[data-cursor="underline"]');
|
||||
|
||||
const target = hugTarget || underlineTarget;
|
||||
const type = hugTarget ? "hug" : underlineTarget ? "underline" : null;
|
||||
|
||||
if (!target || !type) return;
|
||||
if (activeTargetRef.current === target) return;
|
||||
|
||||
if (activeTargetRef.current && activeTargetRef.current !== target) {
|
||||
if (activeTypeRef.current === "hug") leaveHug(activeTargetRef.current);
|
||||
if (activeTypeRef.current === "underline") leaveUnderline();
|
||||
}
|
||||
|
||||
activeTargetRef.current = target;
|
||||
activeTypeRef.current = type;
|
||||
|
||||
if (type === "hug") enterHug(target);
|
||||
if (type === "underline") enterUnderline(target);
|
||||
};
|
||||
|
||||
const handleMouseOut = (e: MouseEvent) => {
|
||||
const el = e.target as HTMLElement;
|
||||
const hugTarget = el.closest<HTMLElement>('[data-cursor="hug"]');
|
||||
const underlineTarget = el.closest<HTMLElement>('[data-cursor="underline"]');
|
||||
const target = hugTarget || underlineTarget;
|
||||
|
||||
if (!target || target !== activeTargetRef.current) return;
|
||||
|
||||
const related = e.relatedTarget as HTMLElement | null;
|
||||
if (related && target.contains(related)) return;
|
||||
|
||||
if (activeTypeRef.current === "hug") leaveHug(target);
|
||||
if (activeTypeRef.current === "underline") leaveUnderline();
|
||||
|
||||
activeTargetRef.current = null;
|
||||
activeTypeRef.current = null;
|
||||
};
|
||||
|
||||
// --- Click bounce (hug elements only) ---
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
const target = (e.target as HTMLElement).closest<HTMLElement>('[data-cursor="hug"]');
|
||||
if (!target) return;
|
||||
gsap.to(target, { scale: 0.95, duration: 0.1, ease: "power2.in" });
|
||||
};
|
||||
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
const target = (e.target as HTMLElement).closest<HTMLElement>('[data-cursor="hug"]');
|
||||
if (!target) return;
|
||||
const tl = gsap.timeline();
|
||||
tl.to(target, { scale: 1.08, duration: 0.15, ease: "back.out(2)" }).to(
|
||||
target,
|
||||
{ scale: 1.05, duration: 0.3, ease: "power2.out" }
|
||||
);
|
||||
};
|
||||
|
||||
// Add listeners
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
document.documentElement.addEventListener("mouseenter", handleDocEnter);
|
||||
document.documentElement.addEventListener("mouseleave", handleDocLeave);
|
||||
document.addEventListener("mouseover", handleMouseOver);
|
||||
document.addEventListener("mouseout", handleMouseOut);
|
||||
document.addEventListener("mousedown", handleMouseDown);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
document.documentElement.removeEventListener("mouseenter", handleDocEnter);
|
||||
document.documentElement.removeEventListener("mouseleave", handleDocLeave);
|
||||
document.removeEventListener("mouseover", handleMouseOver);
|
||||
document.removeEventListener("mouseout", handleMouseOut);
|
||||
document.removeEventListener("mousedown", handleMouseDown);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
observer.disconnect();
|
||||
killTimeline();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sharedStyle: React.CSSProperties = {
|
||||
position: "fixed",
|
||||
left: 0,
|
||||
top: 0,
|
||||
pointerEvents: "none",
|
||||
mixBlendMode: "difference",
|
||||
willChange: "transform",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Main cursor */}
|
||||
<div
|
||||
ref={cursorRef}
|
||||
className="pointer-events-none"
|
||||
style={{
|
||||
...sharedStyle,
|
||||
width: CURSOR_W,
|
||||
height: CURSOR_H,
|
||||
background: "white",
|
||||
borderRadius: CURSOR_RADIUS,
|
||||
outline: "2px solid white",
|
||||
outlineOffset: "1px",
|
||||
opacity: 0,
|
||||
zIndex: Z_NORMAL,
|
||||
}}
|
||||
/>
|
||||
{/* Phantom underline — visible only during split */}
|
||||
<div
|
||||
ref={phantomRef}
|
||||
className="pointer-events-none"
|
||||
style={{
|
||||
...sharedStyle,
|
||||
width: 0,
|
||||
height: 3,
|
||||
background: "white",
|
||||
borderRadius: "2px",
|
||||
outline: "none",
|
||||
opacity: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,21 +2,20 @@ import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-surface-container-low w-full py-20 px-6 lg:px-12 border-t border-outline-variant/20 relative overflow-hidden transition-colors duration-300">
|
||||
<footer className="bg-white w-full pt-16 pb-0 px-6 lg:px-12 relative overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-20">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-16">
|
||||
{/* Logo + Social */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-2xl font-black uppercase tracking-tight text-on-surface">
|
||||
CIMA PROGETTI Srls
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl font-bold uppercase tracking-tight text-on-background">
|
||||
CIMA PROGETTI <span className="capitalize">Srls</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex space-y-3 flex-col w-full">
|
||||
<p className="text-secondary font-medium">
|
||||
<p className="text-secondary text-sm tracking-wide">
|
||||
Scoprirci anche sui nostri canali social
|
||||
</p>
|
||||
<div className="flex gap-4 w-full">
|
||||
<div className="flex gap-4 pt-2">
|
||||
{/* Facebook */}
|
||||
<a
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
@@ -24,6 +23,7 @@ export default function Footer() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Facebook"
|
||||
data-cursor="hug"
|
||||
>
|
||||
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -39,6 +39,7 @@ export default function Footer() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Instagram"
|
||||
data-cursor="hug"
|
||||
>
|
||||
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -62,6 +63,7 @@ export default function Footer() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn"
|
||||
data-cursor="hug"
|
||||
>
|
||||
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -77,6 +79,7 @@ export default function Footer() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="TikTok"
|
||||
data-cursor="hug"
|
||||
>
|
||||
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -87,30 +90,28 @@ export default function Footer() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legals */}
|
||||
<div>
|
||||
<h4 className="font-bold uppercase tracking-widest text-xs mb-8 text-on-surface">
|
||||
<h4 className="font-bold uppercase tracking-widest text-xs mb-6 text-on-background">
|
||||
Legals
|
||||
</h4>
|
||||
<div className="space-y-4 text-secondary font-medium">
|
||||
<div className="space-y-3 text-secondary text-sm tracking-wide">
|
||||
<p>
|
||||
Via Otranto 39
|
||||
<br />
|
||||
00192 Roma, Italia
|
||||
</p>
|
||||
<p>P.IVA 18328621000</p>
|
||||
<p>REA RM-1778381</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Supporto */}
|
||||
<div>
|
||||
<h4 className="font-bold uppercase tracking-widest text-xs mb-8 text-on-surface">
|
||||
<h4 className="font-bold uppercase tracking-widest text-xs mb-6 text-on-background">
|
||||
Supporto
|
||||
</h4>
|
||||
<ul className="space-y-4 text-secondary font-medium">
|
||||
<ul className="space-y-3 text-secondary text-sm tracking-wide">
|
||||
<li>
|
||||
<Link
|
||||
href="#"
|
||||
@@ -140,10 +141,10 @@ export default function Footer() {
|
||||
|
||||
{/* Contatti */}
|
||||
<div>
|
||||
<h4 className="font-bold uppercase tracking-widest text-xs mb-8 text-on-surface">
|
||||
<h4 className="font-bold uppercase tracking-widest text-xs mb-6 text-on-background">
|
||||
Contatti
|
||||
</h4>
|
||||
<ul className="space-y-4 text-secondary font-medium">
|
||||
<ul className="space-y-3 text-secondary text-sm tracking-wide">
|
||||
<li>
|
||||
<a
|
||||
href="mailto:info@cimaprogetti.it"
|
||||
@@ -172,9 +173,9 @@ export default function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 border-t border-outline-variant/20 text-center">
|
||||
<p className="text-on-surface-variant/60 text-xs font-medium uppercase tracking-[0.2em]">
|
||||
© 2026 CIMA PROGETTI. Soluzioni Digitali.
|
||||
<div className="pt-8 pb-4 border-t h-[132px] border-zinc-100 text-center">
|
||||
<p className="text-secondary text-xs uppercase tracking-widest">
|
||||
© 2025 CIMA PROGETTI. <span className="capitalize">Soluzioni digitali</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,12 @@ import dynamic from "next/dynamic";
|
||||
import { useGsapScrollTrigger } from "@/components/hooks/useGsapScrollTrigger";
|
||||
import MaterialSymbolsFont from "@/components/MaterialSymbolsFont";
|
||||
|
||||
const ApproccioSection = dynamic(() => import("@/components/sections/ApproccioSection"), { ssr: false });
|
||||
const QuoteSection = dynamic(() => import("@/components/sections/QuoteSection"), { ssr: false });
|
||||
const ServicesSection = dynamic(() => import("@/components/sections/ServicesSection"), { ssr: false });
|
||||
const QuoteSection = dynamic(() => import("@/components/sections/QuoteSection"), { ssr: false });
|
||||
const ApproccioSection = dynamic(() => import("@/components/sections/ApproccioSection"), { ssr: false });
|
||||
const AboutSection = dynamic(() => import("@/components/sections/AboutSection"), { ssr: false });
|
||||
const FaqSection = dynamic(() => import("@/components/sections/FaqSection"), { ssr: false });
|
||||
const CtaSection = dynamic(() => import("@/components/sections/CtaSection"), { ssr: false });
|
||||
const FaqSection = dynamic(() => import("@/components/sections/FaqSection"), { ssr: false });
|
||||
|
||||
export default function HomeSections() {
|
||||
useGsapScrollTrigger();
|
||||
@@ -16,12 +16,12 @@ export default function HomeSections() {
|
||||
return (
|
||||
<>
|
||||
<MaterialSymbolsFont />
|
||||
<ApproccioSection />
|
||||
<QuoteSection />
|
||||
<ServicesSection />
|
||||
<QuoteSection />
|
||||
<ApproccioSection />
|
||||
<AboutSection />
|
||||
<FaqSection />
|
||||
<CtaSection />
|
||||
<FaqSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,53 +3,170 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "Chi siamo", href: "/#approccio" },
|
||||
{ label: "Servizi", href: "/#servizi" },
|
||||
{ label: "Metodo", href: "/#filosofia" },
|
||||
{ label: "Il metodo", href: "/#approccio" },
|
||||
{ label: "Progetti", href: "/#progetti" },
|
||||
{ label: "Chi Siamo", href: "/#chi-siamo" },
|
||||
];
|
||||
|
||||
const sectionIds = ["servizi", "approccio", "progetti", "chi-siamo"];
|
||||
|
||||
export default function Navbar() {
|
||||
const pathname = usePathname();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [activeSection, setActiveSection] = useState<string | null>(null);
|
||||
const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0, opacity: 0 });
|
||||
const navLinksRef = useRef<(HTMLAnchorElement | null)[]>([]);
|
||||
const indicatorContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updateIndicatorPosition = useCallback((sectionId: string | null) => {
|
||||
if (!sectionId) {
|
||||
setIndicatorStyle((prev) => ({ ...prev, opacity: 0 }));
|
||||
return;
|
||||
}
|
||||
|
||||
const linkIndex = navLinks.findIndex(
|
||||
(link) => link.href === `/#${sectionId}`
|
||||
);
|
||||
|
||||
if (linkIndex !== -1 && navLinksRef.current[linkIndex]) {
|
||||
const link = navLinksRef.current[linkIndex];
|
||||
const container = indicatorContainerRef.current;
|
||||
|
||||
if (container && link) {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const linkRect = link.getBoundingClientRect();
|
||||
|
||||
setIndicatorStyle({
|
||||
left: linkRect.left - containerRect.left,
|
||||
width: linkRect.width,
|
||||
opacity: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Combined scroll handler: scrolled state + active section detection
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 50);
|
||||
|
||||
if (pathname !== "/") return;
|
||||
|
||||
// Find which section is currently in the top portion of the viewport
|
||||
const viewportThreshold = window.innerHeight * 0.4;
|
||||
let current: string | null = null;
|
||||
|
||||
for (const id of sectionIds) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) continue;
|
||||
const rect = el.getBoundingClientRect();
|
||||
// Section is active if its top has scrolled past the top of the viewport
|
||||
// but its bottom is still visible
|
||||
if (rect.top <= viewportThreshold && rect.bottom > 0) {
|
||||
current = id;
|
||||
}
|
||||
}
|
||||
|
||||
setActiveSection(current);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
// Run once on mount to set initial state
|
||||
handleScroll();
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [pathname]);
|
||||
|
||||
// Update indicator position when activeSection or scrolled changes
|
||||
useEffect(() => {
|
||||
if (pathname !== "/") {
|
||||
setIndicatorStyle((prev) => ({ ...prev, opacity: 0 }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay slightly so the navbar transition (py change) settles
|
||||
const timeout = setTimeout(() => updateIndicatorPosition(activeSection), 50);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [pathname, activeSection, scrolled, updateIndicatorPosition]);
|
||||
|
||||
// Recalculate on resize
|
||||
useEffect(() => {
|
||||
if (pathname !== "/" || !activeSection) return;
|
||||
|
||||
const handleResize = () => updateIndicatorPosition(activeSection);
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [pathname, activeSection, updateIndicatorPosition]);
|
||||
|
||||
return (
|
||||
<nav className="fixed top-0 w-full z-50 bg-white/80 backdrop-blur-md border-b border-zinc-200">
|
||||
<div className="bg-transparent flex justify-between max-h-16 items-center px-6 lg:px-8 py-6 max-w-7xl mx-auto">
|
||||
<nav
|
||||
className={`fixed px-6 lg:px-[5vw] xl:px-[15vw] top-0 w-full z-50 transition-all duration-300 ${
|
||||
scrolled
|
||||
? "bg-white/80 backdrop-blur-md shadow-sm"
|
||||
: "bg-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className={`flex justify-between items-center transition-all duration-300 ${scrolled ? "py-2" : "py-4 lg:py-8"}`}>
|
||||
<Link href="/" className="block">
|
||||
<Image
|
||||
src="/logo.svg"
|
||||
alt="CiMa Progetti"
|
||||
width={130}
|
||||
height={50}
|
||||
className="h-8 w-auto"
|
||||
width={140}
|
||||
height={100}
|
||||
className={`w-fit transition-all duration-300 ${scrolled ? "h-10" : "h-12 lg:h-20"}`}
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<div className="hidden lg:flex gap-8 items-center">
|
||||
{navLinks.map((link) => (
|
||||
<div className="hidden lg:flex lg:gap-8 xl:gap-20 items-center">
|
||||
<div
|
||||
ref={indicatorContainerRef}
|
||||
className="flex lg:gap-6 xl:gap-[50px] items-center relative"
|
||||
>
|
||||
{navLinks.map((link, index) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
ref={(el) => {
|
||||
navLinksRef.current[index] = el;
|
||||
}}
|
||||
href={link.href}
|
||||
className="font-sans uppercase tracking-tighter font-bold text-sm text-zinc-600 hover:text-primary transition-colors duration-300"
|
||||
className={`font-sans lg:text-lg xl:text-2xl transition-colors duration-300 whitespace-nowrap relative z-10 ${
|
||||
activeSection === link.href.substring(2) // remove #
|
||||
? "text-primary"
|
||||
: "text-on-background hover:text-primary"
|
||||
}`}
|
||||
data-cursor="hug"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
{/* Sliding indicator bar */}
|
||||
{pathname === "/" && (
|
||||
<div
|
||||
className="absolute bottom-0 h-1 bg-primary rounded transition-all duration-300 ease-out"
|
||||
style={{
|
||||
left: `${indicatorStyle.left}px`,
|
||||
width: `${indicatorStyle.width}px`,
|
||||
opacity: indicatorStyle.opacity,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
href="/contatti"
|
||||
className={`px-6 py-2 font-bold text-sm uppercase tracking-widest transition-all active:scale-95 rounded-[10px] ${
|
||||
className={`lg:px-5 xl:px-8 py-3 font-bold lg:text-lg xl:text-2xl transition-all active:scale-95 rounded-[10px] ${
|
||||
pathname === "/contatti"
|
||||
? "bg-primary text-white"
|
||||
: "bg-primary text-white hover:brightness-110"
|
||||
}`}
|
||||
data-cursor="hug"
|
||||
>
|
||||
Contatti
|
||||
Contattaci
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -89,14 +206,14 @@ export default function Navbar() {
|
||||
style={{ gridTemplateRows: menuOpen ? "1fr" : "0fr" }}
|
||||
>
|
||||
<div className={`overflow-hidden min-h-0 ${menuOpen ? "border-t border-zinc-100" : ""}`}>
|
||||
<div className="px-6 py-6 space-y-4 bg-white/80 backdrop-blur-md">
|
||||
<div className="px-6 py-6 space-y-4 bg-white/90 backdrop-blur-md">
|
||||
{navLinks.map((link, index) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`block font-sans uppercase tracking-tighter font-bold text-sm transition-all duration-150 ease-out ${
|
||||
className={`block font-sans text-lg transition-all duration-150 ease-out ${
|
||||
menuOpen
|
||||
? "text-zinc-600 hover:text-primary opacity-100 translate-y-0"
|
||||
? "text-on-background hover:text-primary opacity-100 translate-y-0"
|
||||
: "opacity-0 translate-y-2"
|
||||
}`}
|
||||
style={{ transitionDelay: menuOpen ? `${index * 40}ms` : "0ms" }}
|
||||
@@ -107,13 +224,13 @@ export default function Navbar() {
|
||||
))}
|
||||
<Link
|
||||
href="/contatti"
|
||||
className={`block bg-primary text-white px-6 py-2 font-bold text-sm uppercase tracking-widest text-center rounded-[10px] transition-all duration-150 ease-out ${
|
||||
className={`block bg-primary text-white px-6 py-3 font-bold text-lg text-center rounded-[10px] transition-all duration-150 ease-out ${
|
||||
menuOpen ? "opacity-100 translate-y-0" : "opacity-0 translate-y-2"
|
||||
}`}
|
||||
style={{ transitionDelay: menuOpen ? "160ms" : "0ms" }}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
Contatti
|
||||
Contattaci
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,50 +55,52 @@ export default function AboutSection() {
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id="chi-siamo"
|
||||
className="snap-section py-24 px-6 lg:px-12 bg-white overflow-hidden"
|
||||
className="snap-section py-24 px-6 lg:px-12 overflow-hidden"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto flex flex-col lg:flex-row gap-10 lg:gap-16 items-center lg:items-start justify-center">
|
||||
{/* Image */}
|
||||
<div ref={imageRef} className="relative w-full max-w-sm lg:max-w-[400px] shrink-0">
|
||||
<div ref={imageRef} className="relative w-full max-w-sm lg:max-w-[455px] shrink-0" data-cursor="hug">
|
||||
<Image
|
||||
alt="Nicola Leone Ciardi e Valentina Madaudo"
|
||||
src="/images/team.jpg"
|
||||
width={640}
|
||||
height={800}
|
||||
className="w-full h-auto object-cover grayscale hover:grayscale-0 transition-all duration-500"
|
||||
className="w-full h-auto object-cover rounded-[10px] shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div ref={textRef} className="space-y-10">
|
||||
<div className="space-y-6">
|
||||
<p className="text-secondary font-medium leading-relaxed max-w-xl text-base lg:text-lg">
|
||||
<div ref={textRef} className="space-y-8">
|
||||
<p className="text-secondary leading-relaxed max-w-xl text-lg lg:text-xl">
|
||||
Due figure giovani, ma strutturate: il dualismo di tecnica e
|
||||
visione, processo e creatività, velocità e consapevolezza.
|
||||
visione, processo e creatività, velocità e consapevolezza.
|
||||
</p>
|
||||
<h2 className="text-4xl lg:text-5xl xl:text-6xl font-black uppercase leading-[0.85] tracking-tighter">
|
||||
DALL'IDEA
|
||||
|
||||
<h2 className="font-heading text-4xl lg:text-5xl xl:text-6xl font-bold lowercase leading-tight tracking-tight">
|
||||
Dall'idea
|
||||
<br />
|
||||
AI PROCESSI.
|
||||
ai processi.
|
||||
<br />
|
||||
DALLA <span className="text-primary">VISIONE</span>
|
||||
Dalla <span className="text-primary">visione</span>
|
||||
<br />
|
||||
AL SISTEMA.
|
||||
al sistema.
|
||||
</h2>
|
||||
<p className="text-secondary font-medium leading-relaxed max-w-xl text-base lg:text-lg">
|
||||
Perché per noi innovare significa integrare competenze e
|
||||
|
||||
<p className="text-secondary leading-relaxed max-w-xl text-lg lg:text-xl">
|
||||
Perché per noi innovare significa integrare competenze e
|
||||
prospettive, per costruire soluzioni che non siano solo funzionali,
|
||||
ma solide nel tempo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-zinc-200 pt-8" />
|
||||
|
||||
{/* Name badges */}
|
||||
<div className="pt-8 flex flex-col sm:flex-row gap-8 items-start relative">
|
||||
<div className="border border-on-background px-6 sm:px-8 py-4 sm:py-5 flex items-center justify-center font-bold text-lg sm:text-xl bg-white shadow-sm hover:shadow-xl transition-shadow cursor-default z-10 w-full sm:w-auto sm:min-w-[280px]">
|
||||
Nicola Leone <span className="text-primary ml-2">Ciardi</span>
|
||||
<div className="flex flex-col sm:flex-row gap-6 items-start relative">
|
||||
<div className="border-2 border-on-background px-8 py-5 flex items-center justify-center text-xl bg-white rounded-[10px] w-full sm:w-auto sm:min-w-[280px]">
|
||||
Nicola Leone <span className="text-primary ml-2">Ci</span>ardi
|
||||
</div>
|
||||
<div className="border border-outline-variant px-6 sm:px-8 py-4 sm:py-5 flex items-center justify-center font-bold text-lg sm:text-xl bg-white shadow-lg hover:shadow-2xl transition-all cursor-default sm:rotate-[-6deg] sm:-mt-4 sm:ml-[-20px] z-20 w-full sm:w-auto sm:min-w-[280px]">
|
||||
Valentina <span className="text-primary ml-2">Madaudo</span>
|
||||
<div className="border-2 border-on-background px-8 py-5 flex items-center justify-center text-xl bg-white rounded-[10px] sm:rotate-[20deg] sm:-mt-2 w-full sm:w-auto sm:min-w-[280px]">
|
||||
Valentina <span className="text-primary ml-2">Ma</span>daudo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,26 +50,27 @@ export default function ApproccioSection() {
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id="approccio"
|
||||
className="snap-section py-24 px-6 lg:px-12 bg-white"
|
||||
className="snap-section py-24 px-6 lg:px-12"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div
|
||||
ref={headerRef}
|
||||
className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-12 items-start"
|
||||
className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start mb-16"
|
||||
>
|
||||
<div className="lg:col-span-5">
|
||||
<p className="text-primary font-black uppercase tracking-widest text-xs mb-4">
|
||||
<p className="text-primary font-bold uppercase tracking-widest text-sm mb-4">
|
||||
L'Approccio
|
||||
</p>
|
||||
<h2 className="text-5xl lg:text-6xl font-black uppercase leading-[0.9] mb-8">
|
||||
<h2 className="font-heading text-5xl lg:text-6xl font-bold lowercase leading-[0.95]">
|
||||
Ecosistema
|
||||
<br />
|
||||
Integrato
|
||||
integrato
|
||||
</h2>
|
||||
</div>
|
||||
<div className="lg:col-span-7">
|
||||
<p className="text-xl lg:text-2xl font-medium leading-relaxed text-on-background">
|
||||
<p className="text-xl lg:text-2xl leading-relaxed text-on-background">
|
||||
Non costruiamo software isolati, ma infrastrutture digitali portanti.
|
||||
Ogni riga di codice è un pilastro, ogni interfaccia un varco
|
||||
Ogni riga di codice è un pilastro, ogni interfaccia un varco
|
||||
funzionale verso l'efficienza operativa. La nostra visione
|
||||
architettonica trasforma il caos informativo in un sistema di
|
||||
precisione millimetrica.
|
||||
@@ -79,22 +80,22 @@ export default function ApproccioSection() {
|
||||
|
||||
<div
|
||||
ref={cardsRef}
|
||||
className="max-w-7xl mx-auto mt-20 grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-px bg-zinc-200 rounded-[10px] overflow-hidden max-w-5xl mx-auto"
|
||||
>
|
||||
{/* Prima */}
|
||||
<div className="bg-surface-container-low p-12 lg:p-16 min-h-[400px]">
|
||||
<span className="material-symbols-outlined text-primary text-5xl mb-8 block">
|
||||
<div className="bg-white p-12 lg:p-16">
|
||||
<span className="material-symbols-outlined text-primary text-4xl mb-8 block">
|
||||
architecture
|
||||
</span>
|
||||
<h3 className="text-4xl font-black uppercase mb-10">Prima</h3>
|
||||
<ul className="space-y-6 text-secondary font-medium">
|
||||
<h3 className="text-3xl font-black uppercase mb-10">Prima</h3>
|
||||
<ul className="space-y-4 text-secondary">
|
||||
{[
|
||||
"Database frammentati",
|
||||
"Processi manuali obsoleti",
|
||||
"Vulnerabilità di sistema",
|
||||
"Vulnerabilit\u00e0 di sistema",
|
||||
"Perdita di controllo strategico",
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-center gap-3">
|
||||
<li key={item} className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-primary shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
@@ -103,19 +104,19 @@ export default function ApproccioSection() {
|
||||
</div>
|
||||
|
||||
{/* Dopo */}
|
||||
<div className="bg-surface-container-low p-12 lg:p-16 min-h-[400px]">
|
||||
<span className="material-symbols-outlined text-primary text-5xl mb-8 block">
|
||||
<div className="bg-white p-12 lg:p-16">
|
||||
<span className="material-symbols-outlined text-primary text-4xl mb-8 block">
|
||||
domain
|
||||
</span>
|
||||
<h3 className="text-4xl font-black uppercase mb-10">Dopo</h3>
|
||||
<ul className="space-y-6 text-on-background font-medium">
|
||||
<h3 className="text-3xl font-black uppercase mb-10">Dopo</h3>
|
||||
<ul className="space-y-4 text-on-background">
|
||||
{[
|
||||
"Flussi centralizzati",
|
||||
"Automazione intelligente",
|
||||
"Crittografia militare",
|
||||
"Dashboard decisionali real-time",
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-center gap-3">
|
||||
<li key={item} className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-primary shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
@@ -123,6 +124,7 @@ export default function ApproccioSection() {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,24 +37,29 @@ export default function CtaSection() {
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id="progetti"
|
||||
className="snap-section py-20 sm:py-28 lg:py-32 px-6 bg-primary text-white text-center"
|
||||
className="snap-section py-20 sm:py-28 lg:py-32 px-6 bg-dark-bg text-white text-center"
|
||||
>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2
|
||||
ref={headingRef}
|
||||
className="text-5xl sm:text-7xl lg:text-9xl font-black uppercase leading-none mb-8 sm:mb-10"
|
||||
className="text-5xl sm:text-7xl lg:text-8xl font-black uppercase leading-none mb-8 sm:mb-10"
|
||||
>
|
||||
SYSTEM
|
||||
<br />
|
||||
UPGRADE
|
||||
</h2>
|
||||
<div ref={contentRef}>
|
||||
<p className="text-lg sm:text-xl lg:text-2xl font-medium mb-8 sm:mb-12 max-w-2xl mx-auto opacity-90">
|
||||
Progettiamo insieme il prossimo tassello digitale della tua azienda.
|
||||
<p className="text-lg sm:text-xl lg:text-2xl mb-8 sm:mb-12 max-w-2xl mx-auto opacity-90">
|
||||
Progettiamo insieme
|
||||
<br />
|
||||
il prossimo tassello digitale
|
||||
<br />
|
||||
della tua azienda.
|
||||
</p>
|
||||
<Link
|
||||
href="/contatti"
|
||||
className="inline-block border-2 border-white hover:bg-transparent hover:text-white bg-white text-primary px-8 sm:px-12 py-4 sm:py-5 font-black uppercase tracking-widest text-base sm:text-lg transition-all active:scale-[0.98] rounded-[10px]"
|
||||
className="inline-block bg-white text-on-background px-12 py-6 font-black uppercase tracking-widest text-lg sm:text-xl transition-all hover:bg-zinc-100 active:scale-[0.98] rounded-[10px]"
|
||||
data-cursor="hug"
|
||||
>
|
||||
Contattaci
|
||||
</Link>
|
||||
|
||||
@@ -12,27 +12,27 @@ const faqs = [
|
||||
{
|
||||
question: "Cosa vi distingue da una classica agenzia?",
|
||||
answer:
|
||||
"Non ci fermiamo al design o allo sviluppo di singoli asset. Lavoriamo come partner tecnico e strategico, progettando soluzioni che collegano immagine, processi, strumenti e performance in un sistema più ordinato, efficiente e misurabile.",
|
||||
"Non ci fermiamo al design o allo sviluppo di singoli asset. Lavoriamo come partner tecnico e strategico, progettando soluzioni che collegano immagine, processi, strumenti e performance in un sistema pi\u00f9 ordinato, efficiente e misurabile.",
|
||||
},
|
||||
{
|
||||
question: "Come utilizzate l'IA nei progetti?",
|
||||
question: "Come utilizzate l\u2019IA nei progetti?",
|
||||
answer:
|
||||
"Usiamo l’IA come leva per accelerare, organizzare e potenziare i processi, senza sostituire il valore umano. La tecnologia aumenta velocità e precisione; strategia, controllo e decisioni restano guidati dall’esperienza.",
|
||||
"Usiamo l\u2019IA come leva per accelerare, organizzare e potenziare i processi, senza sostituire il valore umano. La tecnologia aumenta velocit\u00e0 e precisione; strategia, controllo e decisioni restano guidati dall\u2019esperienza.",
|
||||
},
|
||||
{
|
||||
question: "Possiamo integrare i vostri servizi con strumenti che utilizziamo già?",
|
||||
question: "Possiamo integrare i vostri servizi con strumenti che utilizziamo gi\u00e0?",
|
||||
answer:
|
||||
"Sì. Quando possibile partiamo da ciò che l’azienda ha già costruito, integrando strumenti, processi e flussi di lavoro esistenti per migliorare l’operatività senza creare complessità inutile.",
|
||||
"S\u00ec. Quando possibile partiamo da ci\u00f2 che l\u2019azienda ha gi\u00e0 costruito, integrando strumenti, processi e flussi di lavoro esistenti per migliorare l\u2019operativit\u00e0 senza creare complessit\u00e0 inutile.",
|
||||
},
|
||||
{
|
||||
question: "Ci seguite anche dopo la realizzazione?",
|
||||
answer:
|
||||
"Sì. Un progetto digitale funziona davvero quando può evolversi nel tempo. Per questo accompagniamo il cliente anche nelle fasi di ottimizzazione, miglioramento e crescita del sistema implementato.",
|
||||
"S\u00ec. Un progetto digitale funziona davvero quando pu\u00f2 evolversi nel tempo. Per questo accompagniamo il cliente anche nelle fasi di ottimizzazione, miglioramento e crescita del sistema implementato.",
|
||||
},
|
||||
{
|
||||
question: "Perché scegliere CiMa come partner?",
|
||||
question: "Perch\u00e9 scegliere CiMa come partner?",
|
||||
answer:
|
||||
"Perché uniamo visione progettuale, competenze tecniche e un approccio aggiornato ai sistemi digitali contemporanei. Costruiamo soluzioni concrete, pensate per essere utili, sostenibili e davvero integrate nel lavoro quotidiano dell’azienda.",
|
||||
"Perch\u00e9 uniamo visione progettuale, competenze tecniche e un approccio aggiornato ai sistemi digitali contemporanei. Costruiamo soluzioni concrete, pensate per essere utili, sostenibili e davvero integrate nel lavoro quotidiano dell\u2019azienda.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -82,28 +82,36 @@ export default function FaqSection() {
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
className="snap-section py-24 px-6 lg:px-12 bg-dark-bg text-white"
|
||||
className="snap-section py-24 px-6 lg:px-12"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-12">
|
||||
<div ref={titleRef} className="lg:col-span-4">
|
||||
<h2 className="text-7xl font-black uppercase">FAQs</h2>
|
||||
<div ref={titleRef} className="lg:col-span-3">
|
||||
<h2 className="font-heading text-6xl lg:text-7xl font-bold">FAQs</h2>
|
||||
</div>
|
||||
<div ref={listRef} className="lg:col-span-8 space-y-2">
|
||||
<div ref={listRef} className="lg:col-span-9 space-y-0">
|
||||
{faqs.map((faq, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border-b border-zinc-700 py-6 cursor-pointer"
|
||||
className="border-b border-zinc-300 py-8"
|
||||
data-cursor="underline"
|
||||
data-faq-open={openIndex === i ? "true" : undefined}
|
||||
onClick={() => setOpenIndex(openIndex === i ? null : i)}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-xl lg:text-2xl font-bold uppercase tracking-tight pr-4">
|
||||
<h3 className="text-2xl lg:text-3xl pr-4">
|
||||
{faq.question}
|
||||
</h3>
|
||||
<span className="material-symbols-outlined text-4xl shrink-0 transition-transform duration-300"
|
||||
style={{ transform: openIndex === i ? "rotate(45deg)" : "rotate(0deg)" }}
|
||||
<svg
|
||||
className={`w-6 h-6 shrink-0 transition-transform duration-300 ${
|
||||
openIndex === i ? "rotate-45" : ""
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
add
|
||||
</span>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="overflow-hidden transition-all duration-300"
|
||||
@@ -112,7 +120,7 @@ export default function FaqSection() {
|
||||
opacity: openIndex === i ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<p className="text-zinc-400 font-medium leading-relaxed pt-4">
|
||||
<p className="text-secondary leading-relaxed pt-4 text-lg">
|
||||
{faq.answer}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,56 +1,60 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import InlineLogo from "./ui/inline-logo.svg";
|
||||
|
||||
export default function HeroSection() {
|
||||
return (
|
||||
<section className="snap-section min-h-screen flex flex-col justify-center items-center text-center px-6 pt-24 pb-12">
|
||||
<div className="mb-3 hero-fade-up">
|
||||
<span className="bg-primary text-on-primary px-6 py-2 font-black uppercase text-sm tracking-widest inline-block rounded-[10px]">
|
||||
<section className="snap-section min-h-screen flex flex-col justify-center items-center text-center px-6 pt-36 pb-6">
|
||||
<div className="mb-4 hero-fade-up">
|
||||
<span className="text-primary font-bold text-xl lg:text-2xl lowercase rounded-[10px] bg-transparent px-4 py-2 inline-block">
|
||||
soluzioni digitali
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-huge font-black uppercase mb-10 max-w-5xl hero-slide-up">
|
||||
PORTA IN <span className="text-primary">CIMA</span>
|
||||
<h1 className="font-heading text-[4.5rem] font-black mb-6 max-w-5xl hero-slide-up tracking-tight leading-[0.55]">
|
||||
Porta in <InlineLogo alt="cima_" className="inline h-[4.5rem] pb-2" />
|
||||
<br />
|
||||
IL TUO BUSINESS
|
||||
il tuo business.
|
||||
</h1>
|
||||
<p className="text-lg lg:text-xl font-medium max-w-2xl mb-12 hero-fade-up">
|
||||
<p className="text-xl lg:text-2xl max-w-2xl mb-8 text-on-background hero-fade-up leading-relaxed">
|
||||
Il tuo partner per scalare nella transizione 5.0:
|
||||
<br />
|
||||
<span className="font-black">
|
||||
<span className="font-bold">
|
||||
leader in automazioni IA, conversione
|
||||
<br className="hidden sm:inline" />
|
||||
online e infrastrutture digitali per le aziende.
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto hero-fade-up hero-fade-up-delay-2">
|
||||
<div className="flex flex-col sm:flex-row gap-6 w-full sm:w-auto hero-fade-up hero-fade-up-delay-2">
|
||||
<Link
|
||||
href="/#servizi"
|
||||
className="bg-primary text-on-primary px-10 py-5 font-black uppercase tracking-widest text-base transition-all border-3 border-primary hover:bg-transparent hover:text-primary active:scale-[0.98] text-center rounded-[10px]"
|
||||
className="bg-primary text-on-primary px-10 py-5 font-bold uppercase tracking-widest text-lg transition-all border-2 border-primary hover:bg-transparent hover:text-primary active:scale-[0.98] text-center rounded-[10px]"
|
||||
data-cursor="hug"
|
||||
>
|
||||
Vai ai servizi
|
||||
</Link>
|
||||
<Link
|
||||
href="/contatti"
|
||||
className="border-2 border-on-background text-on-background px-10 py-5 font-black uppercase tracking-widest text-base transition-all hover:bg-on-background hover:text-background active:scale-[0.98] text-center rounded-[10px]"
|
||||
className="border-2 border-on-background text-on-background px-10 py-5 font-bold uppercase tracking-widest text-lg transition-all hover:bg-on-background hover:text-background active:scale-[0.98] text-center rounded-[10px]"
|
||||
data-cursor="hug"
|
||||
>
|
||||
Contattaci
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-15 flex flex-col items-center gap-4 text-secondary hero-fade-up hero-fade-up-delay-2">
|
||||
<span className="text-[10px] uppercase tracking-[0.3em] font-bold">
|
||||
Scopri di più
|
||||
<div className="mt-10 flex flex-col items-center gap-3 text-on-background hero-fade-up hero-fade-up-delay-2">
|
||||
<span className="text-lg uppercase tracking-widest">
|
||||
Scopri di più
|
||||
</span>
|
||||
<svg
|
||||
className="w-10 h-10 animate-bounce"
|
||||
className="w-6 h-6 animate-scroll-down"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m9 12.75 3 3m0 0 3-3m-3 3v-7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
d="M12 5v14m0 0l-5-5m5 5l5-5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -30,17 +30,17 @@ export default function QuoteSection() {
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
className="snap-section py-24 px-6 bg-dark-bg text-white flex flex-col items-center justify-center text-center"
|
||||
className="snap-section py-24 lg:py-32 px-6 bg-dark-bg text-white flex flex-col items-center justify-center text-center"
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<h2
|
||||
ref={quoteRef}
|
||||
className="text-2xl md:text-3xl lg:text-4xl italic leading-tight"
|
||||
className="text-2xl md:text-3xl lg:text-4xl italic leading-snug"
|
||||
>
|
||||
<span className="font-black">“Trasformiamo le tue idee in realtà digitale:</span>
|
||||
<span className="font-bold">“Trasformiamo le tue idee in realtà digitale</span>
|
||||
<span>: </span>
|
||||
<br />
|
||||
<span className="font-normal">velocità dell'IA ed esperienza umana, animate dalla tua
|
||||
esperienza.”</span>
|
||||
<span>velocità dell'IA ed esperienza umana, animate della tua esperienza.”</span>
|
||||
</h2>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3,6 +3,55 @@ import { useRef } from "react";
|
||||
import { useGSAP } from "@gsap/react";
|
||||
import gsap from "gsap";
|
||||
|
||||
const topServices = [
|
||||
{
|
||||
title: "POTENZIA\nI TUOI PROCESSI",
|
||||
description: (
|
||||
<>
|
||||
Dalle <strong>automazioni</strong> che alleggeriscono lavori ripetitivi, fino a report esaustivi sui tuoi dati realizzati dall'<strong>intelligenza artificiale</strong>.
|
||||
</>
|
||||
),
|
||||
icon: "memory",
|
||||
},
|
||||
{
|
||||
title: "ORGANIZZA\n& GESTISCI",
|
||||
description: (
|
||||
<>
|
||||
Sviluppiamo <strong>dashboard</strong> e <strong>gestionali</strong> strutturati sulle tue esigenze aziendali: sistemi su misura che riflettono i vostri processi reali.
|
||||
</>
|
||||
),
|
||||
icon: "schema",
|
||||
},
|
||||
{
|
||||
title: "PROTEGGI\nI TUOI DATI",
|
||||
description: (
|
||||
<>
|
||||
Blindiamo l'ecosistema digitale con <strong>soluzioni di cybersicurezza</strong> reali: dai protocolli di difesa proattivi a un monitoraggio costante degli asset.
|
||||
</>
|
||||
),
|
||||
icon: "verified_user",
|
||||
},
|
||||
];
|
||||
|
||||
const bottomServices = [
|
||||
{
|
||||
title: "CONNETTI & DAI FORMA\nALLA TUA IDEA",
|
||||
description: (
|
||||
<>
|
||||
<strong>App</strong>, <strong>piattaforme</strong> e <strong>infrastrutture digitali</strong>: architetture progettate per dare vita ai tuoi progetti e connettere il tuo lavoro.
|
||||
</>
|
||||
),
|
||||
icon: "hub",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
title: "VENDI I TUOI\nPRODOTTI",
|
||||
description: "Sviluppiamo E-commerce per gestire ogni fase della vendita.",
|
||||
icon: "shopping_bag",
|
||||
wide: false,
|
||||
},
|
||||
];
|
||||
|
||||
export default function ServicesSection() {
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -11,7 +60,6 @@ export default function ServicesSection() {
|
||||
useGSAP(() => {
|
||||
if (!headerRef.current || !cardsRef.current) return;
|
||||
|
||||
// Check for reduced motion preference
|
||||
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
if (prefersReducedMotion) {
|
||||
gsap.set(Array.from(headerRef.current.children), { y: 0, opacity: 1 });
|
||||
@@ -19,14 +67,11 @@ export default function ServicesSection() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Desktop: Pinned ScrollTrigger for header + first 3 cards,
|
||||
// then independent triggers for the bottom 2 cards after unpin
|
||||
gsap.matchMedia().add("(min-width: 768px)", () => {
|
||||
const allCards = Array.from(cardsRef.current!.children);
|
||||
const topCards = allCards.slice(0, 3);
|
||||
const bottomCards = allCards.slice(3);
|
||||
|
||||
// Set bottom cards to invisible initially
|
||||
gsap.set(bottomCards, { y: 60, opacity: 0 });
|
||||
|
||||
const tl = gsap.timeline({
|
||||
@@ -53,7 +98,6 @@ export default function ServicesSection() {
|
||||
0.3
|
||||
);
|
||||
|
||||
// Bottom 2 cards animate independently after section unpins
|
||||
bottomCards.forEach((card) => {
|
||||
gsap.fromTo(
|
||||
card,
|
||||
@@ -73,7 +117,6 @@ export default function ServicesSection() {
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile: Independent ScrollTriggers without pinning
|
||||
gsap.matchMedia().add("(max-width: 767px)", () => {
|
||||
gsap.fromTo(
|
||||
Array.from(headerRef.current!.children),
|
||||
@@ -92,7 +135,7 @@ export default function ServicesSection() {
|
||||
}
|
||||
);
|
||||
|
||||
Array.from(cardsRef.current!.children).forEach((card, index) => {
|
||||
Array.from(cardsRef.current!.children).forEach((card) => {
|
||||
gsap.fromTo(
|
||||
card,
|
||||
{ y: 80, opacity: 0 },
|
||||
@@ -122,98 +165,67 @@ export default function ServicesSection() {
|
||||
{/* Header */}
|
||||
<div
|
||||
ref={headerRef}
|
||||
className="grid grid-cols-1 lg:grid-cols-12 gap-12 mb-20 items-end"
|
||||
className="text-center mb-20 max-w-4xl mx-auto"
|
||||
>
|
||||
<div className="lg:col-span-8">
|
||||
<h2 className="text-4xl lg:text-5xl font-black uppercase mb-6">
|
||||
Cosa Facciamo
|
||||
<h2 className="font-heading text-4xl lg:text-6xl font-bold lowercase mb-6">
|
||||
Servizi <span className="text-primary">essenziali</span>. Impatto concreto.
|
||||
</h2>
|
||||
<div className="w-24 h-2 bg-primary mb-8" />
|
||||
<p className="text-xl text-secondary leading-relaxed max-w-2xl">
|
||||
Soluzioni su misura per il vostro metodo di lavoro. Automazione,
|
||||
gestione dati e competenze umane in un unico flusso.
|
||||
<div className="w-24 h-2 bg-primary mx-auto mb-8" />
|
||||
<p className="text-xl text-on-background leading-relaxed">
|
||||
Trasformiamo esigenze operative e obiettivi aziendali in strumenti concreti.
|
||||
<br className="hidden md:inline" />
|
||||
Ogni servizio è pensato per semplificare la gestione, migliorare i processi
|
||||
<br className="hidden md:inline" />
|
||||
e dare struttura alla crescita.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Services Grid */}
|
||||
<div
|
||||
ref={cardsRef}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
||||
>
|
||||
{/* Portali */}
|
||||
<div className="bg-surface-container p-8 lg:p-12 border-t-4 border-primary hover:bg-zinc-900 hover:text-white transition-all duration-500 group">
|
||||
<span className="material-symbols-outlined text-4xl mb-8 block text-primary group-hover:text-white">
|
||||
hub
|
||||
{/* Top 3 cards */}
|
||||
{topServices.map((service) => (
|
||||
<div
|
||||
key={service.icon}
|
||||
className="bg-zinc-800 text-white p-8 lg:p-12 rounded-[10px] flex flex-col justify-between min-h-[390px] group hover:bg-zinc-900 transition-colors duration-300"
|
||||
>
|
||||
<span className="material-symbols-outlined text-4xl mb-8 block text-white">
|
||||
{service.icon}
|
||||
</span>
|
||||
<h3 className="text-xl lg:text-2xl font-black uppercase mb-4">
|
||||
Connetti & dai forma alla tua idea
|
||||
<div className="flex flex-col gap-4 mt-auto">
|
||||
<h3 className="text-2xl lg:text-3xl font-black uppercase whitespace-pre-line leading-tight">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="opacity-70 group-hover:opacity-100">
|
||||
App, piattaforme e infrastrutture digitali: architetture progettate per dare vita ai tuoi progetti e connettere il tuo lavoro.
|
||||
<p className="text-white/80 text-lg leading-relaxed">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Database */}
|
||||
<div className="bg-surface-container p-8 lg:p-12 border-t-4 border-primary hover:bg-zinc-900 hover:text-white transition-all duration-500 group">
|
||||
<span className="material-symbols-outlined text-4xl mb-8 block text-primary group-hover:text-white">
|
||||
schema
|
||||
</span>
|
||||
<h3 className="text-xl lg:text-2xl font-black uppercase mb-4">
|
||||
Organizza e gestisci
|
||||
{/* Bottom 2 cards */}
|
||||
{bottomServices.map((service) => (
|
||||
<div
|
||||
key={service.icon}
|
||||
className={`bg-zinc-800 text-white p-8 lg:p-12 rounded-[10px] flex flex-col justify-between min-h-[230px] group hover:bg-zinc-900 transition-colors duration-300 ${
|
||||
service.wide ? "md:col-span-2" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<h3 className="text-xl lg:text-2xl font-black uppercase whitespace-pre-line leading-tight">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="opacity-70 group-hover:opacity-100">
|
||||
Sviluppiamo dashboard e gestionali strutturati sulle tue esigenze aziendali: organizzazione del dato al servizio della decisione. Sistemi su misura che riflettono i vostri processi reali.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* E-commerce */}
|
||||
<div className="bg-surface-container p-8 lg:p-12 border-t-4 border-primary hover:bg-zinc-900 hover:text-white transition-all duration-500 group">
|
||||
<span className="material-symbols-outlined text-4xl mb-8 block text-primary group-hover:text-white">
|
||||
shopping_bag
|
||||
</span>
|
||||
<h3 className="text-xl lg:text-2xl font-black uppercase mb-4">
|
||||
Mostra e vendi i tuoi prodotti
|
||||
</h3>
|
||||
<p className="opacity-70 group-hover:opacity-100">
|
||||
Sviluppiamo siti web ed e-commerce pensati non solo per mostrare, ma per gestire in modo completo vendita, pagamenti, ordini e logistica.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Automazioni & IA */}
|
||||
<div className="md:col-span-2 bg-zinc-900 text-white p-8 lg:p-12 flex flex-col md:flex-row justify-between items-center gap-8 group transition-colors duration-300 hover:bg-primary">
|
||||
<div className="max-w-xl">
|
||||
<h3 className="text-2xl lg:text-3xl font-black uppercase mb-4">
|
||||
Potenzia i tuoi processi
|
||||
</h3>
|
||||
<p className="opacity-60 text-base group-hover:opacity-100 transition-all lg:text-lg">
|
||||
Dalle automazioni che alleggeriscono i lavori ripetitivi, fino a report
|
||||
<br />
|
||||
esaustivi sui tuoi dati realizzati dall'intelligenza artificiale.
|
||||
<br />
|
||||
IA anche in locale per la massima protezione dei tuoi dati.
|
||||
</p>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-7xl text-white animate-pulse">
|
||||
memory
|
||||
<span className="material-symbols-outlined text-3xl text-white shrink-0 ml-4">
|
||||
{service.icon}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cybersicurezza */}
|
||||
<div className="bg-zinc-900 text-white p-8 lg:p-12 flex flex-col justify-between hover:bg-primary transition-colors duration-300">
|
||||
<div>
|
||||
<h3 className="text-xl lg:text-2xl font-black uppercase mb-4">
|
||||
Cybersicurezza
|
||||
</h3>
|
||||
<p className="opacity-80 group-hover:opacity-100 transition-all">
|
||||
Protezione proattiva e monitoraggio continuo dei vostri asset
|
||||
digitali.
|
||||
<p className="text-white/80 text-lg leading-relaxed">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-4xl mt-8">
|
||||
verified_user
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg viewBox="0 0 256 86" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M256 77H222V86H256V77Z" fill="#0000FF"/>
|
||||
<g clip-path="url(#clip0_761_3244)">
|
||||
<path d="M125.197 74.9833V36.835C125.197 33.8065 126.385 32.2911 128.765 32.2911C131.019 32.2911 132.144 33.8065 132.144 36.835V74.9833H143.225V36.835C143.225 33.8065 144.35 32.2911 146.605 32.2911C148.982 32.2911 150.172 33.8065 150.172 36.835V74.9833H162.284V35.2254C162.284 31.3137 161.236 28.1266 159.138 25.6663C157.041 23.2042 154.458 21.975 151.393 21.975C148.951 21.975 146.917 22.7635 145.289 24.3406C143.661 25.9196 142.755 28.032 142.567 30.682C142.064 28.096 140.985 25.997 139.327 24.387C137.669 22.7788 135.711 21.975 133.459 21.975C131.019 21.975 129.013 22.8117 127.449 24.4816C125.885 26.1552 125.009 28.3163 124.821 30.9658H124.633V22.9198H113.085V74.9837L125.197 74.9833ZM197.447 73.2786C200.106 71.5141 201.749 69.0826 202.375 65.9905V74.9833H215.99V39.9586C215.99 34.2801 214.081 29.8637 210.262 26.7058C206.444 23.5516 201.124 21.9745 194.301 21.9745C187.79 21.9745 182.626 23.4876 178.809 26.5165C174.99 29.5469 172.987 33.6172 172.799 38.7294H186.132C186.257 36.899 187.024 35.5093 188.433 34.5644C189.842 33.6172 191.734 33.1441 194.113 33.1441C196.617 33.1441 198.541 33.697 199.887 34.8005C201.233 35.9059 201.906 37.4366 201.906 39.3908V42.7055H191.767C185.066 42.7055 179.873 44.2191 176.179 47.2494C172.485 50.2779 170.64 54.4123 170.64 59.6497C170.64 64.5085 172.141 68.4378 175.147 71.4334C178.151 74.4331 182.22 75.9309 187.353 75.9309C191.421 75.9309 194.786 75.0454 197.447 73.2786ZM186.366 62.6781C184.959 61.416 184.254 59.8078 184.254 57.8503C184.254 55.8943 184.927 54.3 186.272 53.0708C187.618 51.8397 189.542 51.2251 192.048 51.2251H201.906V56.5247C201.906 58.9251 201.045 60.8654 199.324 62.346C197.602 63.8304 195.271 64.5707 192.33 64.5707C189.761 64.5707 187.775 63.9403 186.366 62.6781Z" fill="#0000FF"/>
|
||||
<path d="M39.154 70.8058C43.2221 67.3366 45.3827 62.6035 45.6316 56.6058H31.9245C31.7368 58.8787 30.8125 60.6298 29.1543 61.8608C27.4943 63.0901 25.3815 63.7065 22.8165 63.7065C20.0003 63.7065 17.8397 62.9644 16.337 61.4819C14.8362 59.9994 14.0844 57.9005 14.0844 55.1869V42.8813C14.0844 40.1042 14.8357 37.9914 16.337 36.5381C17.8401 35.0867 20.0007 34.3617 22.8165 34.3617C25.3815 34.3617 27.4943 34.9916 29.1543 36.2538C30.8125 37.5178 31.7364 39.2517 31.9245 41.4605H45.6316C45.3823 35.4652 43.2221 30.7321 39.154 27.2606C35.084 23.7914 29.6379 22.0557 22.817 22.0557C18.2469 22.0557 14.2399 22.9235 10.7989 24.657C7.35598 26.3946 4.6948 28.8085 2.81807 31.8987C0.939512 34.9912 0 38.6514 0 42.8804V55.186C0 59.3509 0.939512 62.994 2.81807 66.1195C4.6948 69.2426 7.35598 71.6723 10.7989 73.408C14.2399 75.1438 18.2464 76.0116 22.817 76.0116C29.6379 76.0116 35.0845 74.2768 39.154 70.8058ZM102.718 75.0645V62.2853H87.8834V23.0005H58.5888V35.7797H73.7986V62.2853H56.7098V75.0645H102.718ZM86.3348 12.7786C87.8683 11.4529 88.6352 9.65361 88.6352 7.38258C88.6352 5.11155 87.8683 3.31226 86.3348 1.98656C84.799 0.662725 82.7497 -0.00012207 80.1847 -0.00012207C77.6178 -0.00012207 75.5663 0.662725 74.0346 1.98656C72.4988 3.31226 71.7342 5.11155 71.7342 7.38258C71.7342 9.65361 72.4988 11.4529 74.0346 12.7786C75.5663 14.1024 77.6174 14.7653 80.1847 14.7653C82.7497 14.7653 84.799 14.1024 86.3348 12.7786Z" fill="#0000FF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_761_3244">
|
||||
<rect width="217" height="77" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module "*.svg" {
|
||||
import type { FC, SVGProps } from "react";
|
||||
const content: FC<SVGProps<SVGSVGElement>>;
|
||||
export default content;
|
||||
}
|
||||
Reference in New Issue
Block a user