feat: add custom cursor, navbar responsiveness, and active section indicator

This commit is contained in:
2026-04-12 21:04:30 +02:00
parent dfcba4965f
commit da0b03893f
23 changed files with 3565 additions and 354 deletions
+47
View File
@@ -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
+17
View File
@@ -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"],
+2297
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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

+3 -1
View File
@@ -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>
+40 -1
View File
@@ -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);
}
+11 -1
View File
@@ -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>
);
+591
View File
@@ -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,
}}
/>
</>
);
}
+90 -89
View File
@@ -2,115 +2,116 @@ 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">
Scoprirci anche sui nostri canali social
</p>
<div className="flex gap-4 w-full">
{/* Facebook */}
<a
className="hover:opacity-80 transition-opacity"
href="https://facebook.com"
target="_blank"
rel="noopener noreferrer"
aria-label="Facebook"
>
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24">
<path
d="M22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 16.9913 5.65684 21.1283 10.4375 21.8785V14.8906H7.89844V12H10.4375V9.79688C10.4375 7.29063 11.9305 5.90625 14.2146 5.90625C15.3088 5.90625 16.4531 6.10156 16.4531 6.10156V8.5625H15.1922C13.95 8.5625 13.5625 9.33313 13.5625 10.1242V12H16.3359L15.8926 14.8906H13.5625V21.8785C18.3432 21.1283 22 16.9913 22 12Z"
fill="currentColor"
/>
</svg>
</a>
{/* Instagram */}
<a
className="hover:opacity-80 transition-opacity"
href="https://instagram.com"
target="_blank"
rel="noopener noreferrer"
aria-label="Instagram"
>
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24">
<path
d="M12 2.16338C15.2037 2.16338 15.5842 2.17575 16.8517 2.23388C18.0229 2.28713 18.6592 2.48288 19.0822 2.64713C19.6429 2.86463 20.0426 3.123 20.463 3.54338C20.8834 3.96375 21.1414 4.3635 21.3589 4.92413C21.5231 5.34713 21.7192 5.9835 21.7725 7.15463C21.8306 8.42213 21.843 8.80238 21.843 12C21.843 15.1976 21.8306 15.5779 21.7725 16.8454C21.7192 18.0165 21.5231 18.6529 21.3589 19.0759C21.1414 19.6365 20.8834 20.0363 20.463 20.4566C20.0426 20.877 19.6429 21.135 19.0822 21.3525C18.6592 21.5168 18.0229 21.7129 16.8517 21.7661C15.5842 21.8243 15.2037 21.8366 12 21.8366C8.79637 21.8366 8.41575 21.8243 7.14825 21.7661C5.97712 21.7129 5.34075 21.5168 4.91775 21.3525C4.35712 21.135 3.95737 20.877 3.537 20.4566C3.11662 20.0363 2.85862 19.6365 2.64112 19.0759C2.47687 18.6529 2.28075 18.0165 2.2275 16.8454C2.16937 15.5779 2.157 15.1976 2.157 12C2.157 8.80238 2.16937 8.42213 2.2275 7.15463C2.28075 5.9835 2.47687 5.34713 2.64112 4.92413C2.85862 4.3635 3.11662 3.96375 3.537 3.54338C3.95737 3.123 4.35712 2.86463 4.91775 2.64713C5.34075 2.48288 5.97712 2.28713 7.14825 2.23388C8.41575 2.17575 8.79637 2.16338 12 2.16338ZM12 0C8.74125 0 8.33325 0.013875 7.053 0.072375C5.77537 0.1305 4.90275 0.33375 4.143 0.628875C3.35775 0.93375 2.69062 1.34175 2.02575 2.007C1.36012 2.67188 0.9525 3.33938 0.647625 4.12463C0.352125 4.88438 0.14925 5.75738 0.091125 7.035C0.032625 8.31525 0.01875 8.72325 0.01875 11.9816C0.01875 15.2396 0.032625 15.6476 0.091125 16.9279C0.14925 18.2055 0.352125 19.0781 0.647625 19.8379C0.9525 20.6231 1.36012 21.2906 2.02575 21.9555C2.69062 22.6204 3.35812 23.0284 4.14337 23.3332C4.90312 23.6288 5.77612 23.8316 7.05375 23.8898C8.334 23.9482 8.742 23.9621 12.0004 23.9621C15.2584 23.9621 15.6664 23.9482 16.9466 23.8898C18.2242 23.8316 19.0969 23.6288 19.8566 23.3332C20.6419 23.0284 21.3094 22.6204 21.9742 21.9555C22.6395 21.2906 23.0475 20.6231 23.3524 19.8379C23.6479 19.0781 23.8507 18.2051 23.9089 16.9275C23.9674 15.6472 23.9812 15.2393 23.9812 11.9809C23.9812 8.7225 23.9674 8.3145 23.9089 7.03425C23.8507 5.75662 23.6479 4.884 23.3524 4.12425C23.0475 3.339 22.6395 2.6715 21.9742 2.00663C21.3094 1.34138 20.6419 0.933375 19.8566 0.6285C19.0969 0.333375 18.2239 0.1305 16.9463 0.072C15.666 0.0135 15.258 0 11.9996 0H12Z"
fill="currentColor"
/>
<path
d="M12 5.8275C8.59087 5.8275 5.82712 8.59125 5.82712 12C5.82712 15.4088 8.59087 18.1725 12 18.1725C15.4091 18.1725 18.1729 15.4088 18.1729 12C18.1729 8.59125 15.4091 5.8275 12 5.8275ZM12 16.0151C9.78262 16.0151 7.98487 14.2174 7.98487 12C7.98487 9.78263 9.78262 7.98488 12 7.98488C14.2174 7.98488 16.0151 9.78263 16.0151 12C16.0151 14.2174 14.2174 16.0151 12 16.0151Z"
fill="currentColor"
/>
<path
d="M18.4219 4.14C17.6227 4.14 16.9763 4.7865 16.9763 5.58562C16.9763 6.38475 17.6227 7.03125 18.4219 7.03125C19.221 7.03125 19.8675 6.38475 19.8675 5.58562C19.8675 4.7865 19.221 4.14 18.4219 4.14Z"
fill="currentColor"
/>
</svg>
</a>
{/* LinkedIn */}
<a
className="hover:opacity-80 transition-opacity"
href="https://linkedin.com"
target="_blank"
rel="noopener noreferrer"
aria-label="LinkedIn"
>
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24">
<path
d="M20.4472 20.452H16.8904V14.88C16.8904 13.5524 16.8656 11.8448 15.0424 11.8448C13.1936 11.8448 12.9096 13.2888 12.9096 14.784V20.452H9.3528V8.9952H12.7656V10.5624H12.8136C13.2888 9.6616 14.4496 8.712 16.1816 8.712C19.7832 8.712 20.4472 11.0824 20.4472 14.168V20.452ZM5.3376 7.4336C4.1976 7.4336 3.2736 6.5096 3.2736 5.372C3.2736 4.2344 4.1976 3.3104 5.3376 3.3104C6.4752 3.3104 7.4016 4.2344 7.4016 5.372C7.4016 6.5096 6.4752 7.4336 5.3376 7.4336ZM7.1192 20.452H3.5552V8.9952H7.1192V20.452ZM22.2256 0H1.7712C0.792 0 0 0.7744 0 1.7296V22.2688C0 23.224 0.792 24 1.7712 24H22.2216C23.2 24 24 23.224 24 22.2688V1.7296C24 0.7744 23.2 0 22.2256 0Z"
fill="currentColor"
/>
</svg>
</a>
{/* TikTok */}
<a
className="hover:opacity-80 transition-opacity"
href="https://tiktok.com"
target="_blank"
rel="noopener noreferrer"
aria-label="TikTok"
>
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24">
<path
d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-2.88 2.5 2.89 2.89 0 0 1-2.89-2.89 2.89 2.89 0 0 1 2.89-2.89c.3 0 .59.04.86.11V9a6.27 6.27 0 0 0-.86-.06 6.33 6.33 0 0 0-6.33 6.33A6.33 6.33 0 0 0 9.49 21.6a6.33 6.33 0 0 0 6.33-6.33V8.78a8.18 8.18 0 0 0 3.77.92V6.69Z"
fill="currentColor"
/>
</svg>
</a>
</div>
<p className="text-secondary text-sm tracking-wide">
Scoprirci anche sui nostri canali social
</p>
<div className="flex gap-4 pt-2">
{/* Facebook */}
<a
className="hover:opacity-80 transition-opacity"
href="https://facebook.com"
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
d="M22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 16.9913 5.65684 21.1283 10.4375 21.8785V14.8906H7.89844V12H10.4375V9.79688C10.4375 7.29063 11.9305 5.90625 14.2146 5.90625C15.3088 5.90625 16.4531 6.10156 16.4531 6.10156V8.5625H15.1922C13.95 8.5625 13.5625 9.33313 13.5625 10.1242V12H16.3359L15.8926 14.8906H13.5625V21.8785C18.3432 21.1283 22 16.9913 22 12Z"
fill="currentColor"
/>
</svg>
</a>
{/* Instagram */}
<a
className="hover:opacity-80 transition-opacity"
href="https://instagram.com"
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
d="M12 2.16338C15.2037 2.16338 15.5842 2.17575 16.8517 2.23388C18.0229 2.28713 18.6592 2.48288 19.0822 2.64713C19.6429 2.86463 20.0426 3.123 20.463 3.54338C20.8834 3.96375 21.1414 4.3635 21.3589 4.92413C21.5231 5.34713 21.7192 5.9835 21.7725 7.15463C21.8306 8.42213 21.843 8.80238 21.843 12C21.843 15.1976 21.8306 15.5779 21.7725 16.8454C21.7192 18.0165 21.5231 18.6529 21.3589 19.0759C21.1414 19.6365 20.8834 20.0363 20.463 20.4566C20.0426 20.877 19.6429 21.135 19.0822 21.3525C18.6592 21.5168 18.0229 21.7129 16.8517 21.7661C15.5842 21.8243 15.2037 21.8366 12 21.8366C8.79637 21.8366 8.41575 21.8243 7.14825 21.7661C5.97712 21.7129 5.34075 21.5168 4.91775 21.3525C4.35712 21.135 3.95737 20.877 3.537 20.4566C3.11662 20.0363 2.85862 19.6365 2.64112 19.0759C2.47687 18.6529 2.28075 18.0165 2.2275 16.8454C2.16937 15.5779 2.157 15.1976 2.157 12C2.157 8.80238 2.16937 8.42213 2.2275 7.15463C2.28075 5.9835 2.47687 5.34713 2.64112 4.92413C2.85862 4.3635 3.11662 3.96375 3.537 3.54338C3.95737 3.123 4.35712 2.86463 4.91775 2.64713C5.34075 2.48288 5.97712 2.28713 7.14825 2.23388C8.41575 2.17575 8.79637 2.16338 12 2.16338ZM12 0C8.74125 0 8.33325 0.013875 7.053 0.072375C5.77537 0.1305 4.90275 0.33375 4.143 0.628875C3.35775 0.93375 2.69062 1.34175 2.02575 2.007C1.36012 2.67188 0.9525 3.33938 0.647625 4.12463C0.352125 4.88438 0.14925 5.75738 0.091125 7.035C0.032625 8.31525 0.01875 8.72325 0.01875 11.9816C0.01875 15.2396 0.032625 15.6476 0.091125 16.9279C0.14925 18.2055 0.352125 19.0781 0.647625 19.8379C0.9525 20.6231 1.36012 21.2906 2.02575 21.9555C2.69062 22.6204 3.35812 23.0284 4.14337 23.3332C4.90312 23.6288 5.77612 23.8316 7.05375 23.8898C8.334 23.9482 8.742 23.9621 12.0004 23.9621C15.2584 23.9621 15.6664 23.9482 16.9466 23.8898C18.2242 23.8316 19.0969 23.6288 19.8566 23.3332C20.6419 23.0284 21.3094 22.6204 21.9742 21.9555C22.6395 21.2906 23.0475 20.6231 23.3524 19.8379C23.6479 19.0781 23.8507 18.2051 23.9089 16.9275C23.9674 15.6472 23.9812 15.2393 23.9812 11.9809C23.9812 8.7225 23.9674 8.3145 23.9089 7.03425C23.8507 5.75662 23.6479 4.884 23.3524 4.12425C23.0475 3.339 22.6395 2.6715 21.9742 2.00663C21.3094 1.34138 20.6419 0.933375 19.8566 0.6285C19.0969 0.333375 18.2239 0.1305 16.9463 0.072C15.666 0.0135 15.258 0 11.9996 0H12Z"
fill="currentColor"
/>
<path
d="M12 5.8275C8.59087 5.8275 5.82712 8.59125 5.82712 12C5.82712 15.4088 8.59087 18.1725 12 18.1725C15.4091 18.1725 18.1729 15.4088 18.1729 12C18.1729 8.59125 15.4091 5.8275 12 5.8275ZM12 16.0151C9.78262 16.0151 7.98487 14.2174 7.98487 12C7.98487 9.78263 9.78262 7.98488 12 7.98488C14.2174 7.98488 16.0151 9.78263 16.0151 12C16.0151 14.2174 14.2174 16.0151 12 16.0151Z"
fill="currentColor"
/>
<path
d="M18.4219 4.14C17.6227 4.14 16.9763 4.7865 16.9763 5.58562C16.9763 6.38475 17.6227 7.03125 18.4219 7.03125C19.221 7.03125 19.8675 6.38475 19.8675 5.58562C19.8675 4.7865 19.221 4.14 18.4219 4.14Z"
fill="currentColor"
/>
</svg>
</a>
{/* LinkedIn */}
<a
className="hover:opacity-80 transition-opacity"
href="https://linkedin.com"
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
d="M20.4472 20.452H16.8904V14.88C16.8904 13.5524 16.8656 11.8448 15.0424 11.8448C13.1936 11.8448 12.9096 13.2888 12.9096 14.784V20.452H9.3528V8.9952H12.7656V10.5624H12.8136C13.2888 9.6616 14.4496 8.712 16.1816 8.712C19.7832 8.712 20.4472 11.0824 20.4472 14.168V20.452ZM5.3376 7.4336C4.1976 7.4336 3.2736 6.5096 3.2736 5.372C3.2736 4.2344 4.1976 3.3104 5.3376 3.3104C6.4752 3.3104 7.4016 4.2344 7.4016 5.372C7.4016 6.5096 6.4752 7.4336 5.3376 7.4336ZM7.1192 20.452H3.5552V8.9952H7.1192V20.452ZM22.2256 0H1.7712C0.792 0 0 0.7744 0 1.7296V22.2688C0 23.224 0.792 24 1.7712 24H22.2216C23.2 24 24 23.224 24 22.2688V1.7296C24 0.7744 23.2 0 22.2256 0Z"
fill="currentColor"
/>
</svg>
</a>
{/* TikTok */}
<a
className="hover:opacity-80 transition-opacity"
href="https://tiktok.com"
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
d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-2.88 2.5 2.89 2.89 0 0 1-2.89-2.89 2.89 2.89 0 0 1 2.89-2.89c.3 0 .59.04.86.11V9a6.27 6.27 0 0 0-.86-.06 6.33 6.33 0 0 0-6.33 6.33A6.33 6.33 0 0 0 9.49 21.6a6.33 6.33 0 0 0 6.33-6.33V8.78a8.18 8.18 0 0 0 3.77.92V6.69Z"
fill="currentColor"
/>
</svg>
</a>
</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]">
&copy; 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">
&copy; 2025 CIMA PROGETTI. <span className="capitalize">Soluzioni digitali</span>.
</p>
</div>
</div>
+6 -6
View File
@@ -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 />
</>
);
}
+142 -25
View File
@@ -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) => (
<Link
key={link.href}
href={link.href}
className="font-sans uppercase tracking-tighter font-bold text-sm text-zinc-600 hover:text-primary transition-colors duration-300"
>
{link.label}
</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 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">
Due figure giovani, ma strutturate: il dualismo di tecnica e
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&apos;IDEA
<br />
AI PROCESSI.
<br />
DALLA <span className="text-primary">VISIONE</span>
<br />
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
prospettive, per costruire soluzioni che non siano solo funzionali,
ma solide nel tempo.
</p>
</div>
<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&agrave;, velocit&agrave; e consapevolezza.
</p>
<h2 className="font-heading text-4xl lg:text-5xl xl:text-6xl font-bold lowercase leading-tight tracking-tight">
Dall&apos;idea
<br />
ai processi.
<br />
Dalla <span className="text-primary">visione</span>
<br />
al sistema.
</h2>
<p className="text-secondary leading-relaxed max-w-xl text-lg lg:text-xl">
Perch&eacute; per noi innovare significa integrare competenze e
prospettive, per costruire soluzioni che non siano solo funzionali,
ma solide nel tempo.
</p>
<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,77 +50,79 @@ 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
ref={headerRef}
className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-12 items-start"
>
<div className="lg:col-span-5">
<p className="text-primary font-black uppercase tracking-widest text-xs mb-4">
L&apos;Approccio
</p>
<h2 className="text-5xl lg:text-6xl font-black uppercase leading-[0.9] mb-8">
Ecosistema
<br />
Integrato
</h2>
</div>
<div className="lg:col-span-7">
<p className="text-xl lg:text-2xl font-medium leading-relaxed text-on-background">
Non costruiamo software isolati, ma infrastrutture digitali portanti.
Ogni riga di codice è un pilastro, ogni interfaccia un varco
funzionale verso l&apos;efficienza operativa. La nostra visione
architettonica trasforma il caos informativo in un sistema di
precisione millimetrica.
</p>
</div>
</div>
<div
ref={cardsRef}
className="max-w-7xl mx-auto mt-20 grid grid-cols-1 md:grid-cols-2 gap-8"
>
{/* 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">
architecture
</span>
<h3 className="text-4xl font-black uppercase mb-10">Prima</h3>
<ul className="space-y-6 text-secondary font-medium">
{[
"Database frammentati",
"Processi manuali obsoleti",
"Vulnerabilità di sistema",
"Perdita di controllo strategico",
].map((item) => (
<li key={item} className="flex items-center gap-3">
<span className="w-1.5 h-1.5 bg-primary shrink-0" />
{item}
</li>
))}
</ul>
<div className="max-w-7xl mx-auto">
<div
ref={headerRef}
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-bold uppercase tracking-widest text-sm mb-4">
L&apos;Approccio
</p>
<h2 className="font-heading text-5xl lg:text-6xl font-bold lowercase leading-[0.95]">
Ecosistema
<br />
integrato
</h2>
</div>
<div className="lg:col-span-7">
<p className="text-xl lg:text-2xl leading-relaxed text-on-background">
Non costruiamo software isolati, ma infrastrutture digitali portanti.
Ogni riga di codice &egrave; un pilastro, ogni interfaccia un varco
funzionale verso l&apos;efficienza operativa. La nostra visione
architettonica trasforma il caos informativo in un sistema di
precisione millimetrica.
</p>
</div>
</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">
domain
</span>
<h3 className="text-4xl font-black uppercase mb-10">Dopo</h3>
<ul className="space-y-6 text-on-background font-medium">
{[
"Flussi centralizzati",
"Automazione intelligente",
"Crittografia militare",
"Dashboard decisionali real-time",
].map((item) => (
<li key={item} className="flex items-center gap-3">
<span className="w-1.5 h-1.5 bg-primary shrink-0" />
{item}
</li>
))}
</ul>
<div
ref={cardsRef}
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-white p-12 lg:p-16">
<span className="material-symbols-outlined text-primary text-4xl mb-8 block">
architecture
</span>
<h3 className="text-3xl font-black uppercase mb-10">Prima</h3>
<ul className="space-y-4 text-secondary">
{[
"Database frammentati",
"Processi manuali obsoleti",
"Vulnerabilit\u00e0 di sistema",
"Perdita di controllo strategico",
].map((item) => (
<li key={item} className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-primary shrink-0" />
{item}
</li>
))}
</ul>
</div>
{/* Dopo */}
<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-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-2">
<span className="w-1.5 h-1.5 bg-primary shrink-0" />
{item}
</li>
))}
</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>
+27 -19
View File
@@ -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 lIA 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 dallesperienza.",
"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 lazienda ha già costruito, integrando strumenti, processi e flussi di lavoro esistenti per migliorare loperatività 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 dellazienda.",
"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&ugrave;
</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">&ldquo;Trasformiamo le tue idee in realtà digitale:</span>
<span className="font-bold">&ldquo;Trasformiamo le tue idee in realt&agrave; digitale</span>
<span>: </span>
<br />
<span className="font-normal">velocità dell&apos;IA ed esperienza umana, animate dalla tua
esperienza.&rdquo;</span>
<span>velocit&agrave; dell&apos;IA ed esperienza umana, animate della tua esperienza.&rdquo;</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&apos;<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&apos;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,18 +165,19 @@ 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>
<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.
</p>
</div>
<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 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 &egrave; pensato per semplificare la gestione, migliorare i processi
<br className="hidden md:inline" />
e dare struttura alla crescita.
</p>
</div>
{/* Services Grid */}
@@ -141,79 +185,47 @@ export default function ServicesSection() {
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
</span>
<h3 className="text-xl lg:text-2xl font-black uppercase mb-4">
Connetti &amp; dai forma alla tua idea
</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>
</div>
{/* 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>
<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="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
</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.
{/* 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>
<span className="material-symbols-outlined text-3xl text-white shrink-0 ml-4">
{service.icon}
</span>
</div>
<p className="text-white/80 text-lg leading-relaxed">
{service.description}
</p>
</div>
<span className="material-symbols-outlined text-7xl text-white animate-pulse">
memory
</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>
</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

+5
View File
@@ -0,0 +1,5 @@
declare module "*.svg" {
import type { FC, SVGProps } from "react";
const content: FC<SVGProps<SVGSVGElement>>;
export default content;
}