Skip to content

Commit bdd3f92

Browse files
authored
Merge pull request #196 from LoRy24/rassegna-stampa
Implementata la rassegna stampa nel sito web
2 parents 1142d3f + 879a02e commit bdd3f92

6 files changed

Lines changed: 421 additions & 2 deletions

File tree

public/app_data/press/img1.png

35.4 KB
Loading

public/app_data/press/img2.png

627 KB
Loading

public/app_data/press/img3.png

1.03 MB
Loading

src/app/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {
1414
import Image from "next/image";
1515
import {HomeExploreButton} from "@/components/ui/home/HomeExploreButton";
1616
import {HomeTryInsulligenceButton} from "@/components/ui/home/HomeTryInsulligenceButton";
17-
18-
// Icons
17+
import {PressReviewSection} from "@/components/ui/home/PressReviewSection";
18+
import {pressReviewRow1, pressReviewRow2} from "@/lib/insu/press/PressReviewData";
1919

2020
export default function Home() {
2121
return (
@@ -170,6 +170,9 @@ export default function Home() {
170170
</div>
171171
</div>
172172
</div>
173+
<div id={"rassegna-stampa"}>
174+
<PressReviewSection rows={[pressReviewRow1, pressReviewRow2]} />
175+
</div>
173176
<div id={"launch-page"} className={"w-full flex flex-col items-center py-30 pl-16 pr-16"}>
174177
<div className={"flex flex-col items-center"}>
175178
<ScrollFloat
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
"use client";
2+
3+
import Image from "next/image";
4+
import Link from "next/link";
5+
import React, { useEffect, useMemo, useRef, useState } from "react";
6+
7+
export type PressReviewCard = {
8+
id: string;
9+
title: string;
10+
source: string;
11+
url: string;
12+
imageUrl?: string;
13+
tag?: string;
14+
date?: string;
15+
};
16+
17+
type PressReviewSectionProps = {
18+
rows: [PressReviewCard[], PressReviewCard[]];
19+
className?: string;
20+
};
21+
22+
type MarqueeRowProps = {
23+
items: PressReviewCard[];
24+
direction?: "left" | "right";
25+
baseSpeed?: number;
26+
pauseOnHover?: boolean;
27+
};
28+
29+
function cn(...classes: Array<string | false | null | undefined>) {
30+
return classes.filter(Boolean).join(" ");
31+
}
32+
33+
function getHostname(url: string) {
34+
try {
35+
return new URL(url).hostname.replace("www.", "");
36+
} catch {
37+
return url;
38+
}
39+
}
40+
41+
function isPdfUrl(url: string) {
42+
return /\.pdf(\?|#|$)/i.test(url);
43+
}
44+
45+
function getAutoPreviewImage(url: string) {
46+
return `https://image.thum.io/get/width/1200/crop/800/noanimate/${encodeURIComponent(url)}`;
47+
}
48+
49+
function PressCard({ item }: { item: PressReviewCard }) {
50+
const [previewFailed, setPreviewFailed] = useState(false);
51+
52+
const hasCustomImage = Boolean(item.imageUrl?.trim());
53+
const pdf = isPdfUrl(item.url);
54+
const domain = getHostname(item.url);
55+
const showRemotePreview = !hasCustomImage && !pdf && !previewFailed;
56+
57+
return (
58+
<Link
59+
href={item.url}
60+
target="_blank"
61+
rel="noopener noreferrer"
62+
className={cn(
63+
"group relative shrink-0",
64+
"w-90 max-[900px]:w-75 max-[640px]:w-67.5",
65+
"h-60 max-[900px]:h-55",
66+
"overflow-hidden rounded-[28px]",
67+
"border border-white/10 bg-[#101010]",
68+
"shadow-[0_8px_30px_rgba(0,0,0,0.28)]",
69+
"transition-all duration-400 ease-out",
70+
"hover:-translate-y-1.5 hover:border-[#9EF0A8]/30 hover:shadow-[0_18px_60px_rgba(0,0,0,0.38)]"
71+
)}
72+
>
73+
<div className="absolute inset-0">
74+
{hasCustomImage ? (
75+
<Image
76+
src={item.imageUrl!}
77+
alt={item.title}
78+
fill
79+
className="object-cover transition-transform duration-700 ease-out group-hover:scale-[1.04]"
80+
sizes="(max-width: 640px) 270px, (max-width: 900px) 300px, 360px"
81+
/>
82+
) : showRemotePreview ? (
83+
<img
84+
src={getAutoPreviewImage(item.url)}
85+
alt={item.title}
86+
className="h-full w-full object-cover transition-transform duration-700 ease-out group-hover:scale-[1.04]"
87+
onError={() => setPreviewFailed(true)}
88+
/>
89+
) : (
90+
<div className="relative h-full w-full overflow-hidden bg-[radial-gradient(circle_at_top_left,rgba(170,255,176,0.16),transparent_35%),linear-gradient(135deg,#161616_0%,#111111_45%,#0b0b0b_100%)]">
91+
<div className="absolute -right-10 -top-10 h-40 w-40 rounded-full bg-[#9EF0A8]/10 blur-3xl" />
92+
93+
<div className="absolute inset-0 flex flex-col justify-between p-6">
94+
<div className="flex items-center justify-between gap-3">
95+
<div className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-white/70">
96+
{pdf ? "PDF" : "Preview"}
97+
</div>
98+
99+
<div className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[11px] text-white/60 backdrop-blur-md">
100+
{domain}
101+
</div>
102+
</div>
103+
104+
<div>
105+
<div className="mb-3 line-clamp-2 text-[15px] font-semibold text-white/90">
106+
{item.title}
107+
</div>
108+
<div className="line-clamp-2 break-all text-[13px] text-white/55">
109+
{item.url}
110+
</div>
111+
</div>
112+
</div>
113+
</div>
114+
)}
115+
116+
<div className="absolute inset-0 bg-linear-to-t from-black via-black/40 to-black/10" />
117+
<div className="absolute inset-0 opacity-0 transition-opacity duration-400 group-hover:opacity-100 bg-[linear-gradient(135deg,rgba(158,240,168,0.10),transparent_55%,rgba(255,255,255,0.04))]" />
118+
</div>
119+
120+
<div className="absolute inset-x-0 bottom-0 z-10 p-5">
121+
<div className="mb-3 flex items-center gap-2">
122+
{item.tag ? (
123+
<span className="rounded-full border border-[#9EF0A8]/20 bg-[#9EF0A8]/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-[#D6FFDB]">
124+
{item.tag}
125+
</span>
126+
) : null}
127+
128+
{item.date ? (
129+
<span className="rounded-full border border-white/10 bg-white/6 px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.12em] text-white/70 backdrop-blur-md">
130+
{item.date}
131+
</span>
132+
) : null}
133+
</div>
134+
135+
<div className="mb-1 line-clamp-2 text-[20px] font-black leading-tight text-white transition-transform duration-400 group-hover:translate-x-0.5">
136+
{item.title}
137+
</div>
138+
139+
<div className="flex items-center justify-between gap-4">
140+
<div className="text-[13px] text-white/65">{item.source}</div>
141+
142+
<div className="flex items-center gap-2 text-[13px] font-medium text-[#C9FFD0]">
143+
<span>Apri</span>
144+
<svg
145+
className="transition-transform duration-300 group-hover:-translate-y-1 group-hover:translate-x-1"
146+
width="16"
147+
height="16"
148+
viewBox="0 0 24 24"
149+
fill="none"
150+
>
151+
<path
152+
d="M7 17L17 7M17 7H9M17 7V15"
153+
stroke="currentColor"
154+
strokeWidth="2"
155+
strokeLinecap="round"
156+
strokeLinejoin="round"
157+
/>
158+
</svg>
159+
</div>
160+
</div>
161+
</div>
162+
163+
<div className="pointer-events-none absolute inset-0 rounded-[28px] ring-1 ring-inset ring-white/6" />
164+
</Link>
165+
);
166+
}
167+
168+
function MarqueeRow({
169+
items,
170+
direction = "left",
171+
baseSpeed = 0.45,
172+
pauseOnHover = true
173+
}: MarqueeRowProps) {
174+
const trackRef = useRef<HTMLDivElement | null>(null);
175+
176+
const duplicatedItems = useMemo(() => {
177+
if (items.length === 0) return [];
178+
return [...items, ...items, ...items, ...items];
179+
}, [items]);
180+
181+
const offsetRef = useRef(0);
182+
const animationFrameRef = useRef<number | null>(null);
183+
const hoveredRef = useRef(false);
184+
const draggingRef = useRef(false);
185+
const boostRef = useRef(0);
186+
const lastXRef = useRef(0);
187+
const loopWidthRef = useRef(0);
188+
189+
useEffect(() => {
190+
const handleMouseUp = () => {
191+
draggingRef.current = false;
192+
};
193+
194+
window.addEventListener("mouseup", handleMouseUp);
195+
196+
return () => {
197+
window.removeEventListener("mouseup", handleMouseUp);
198+
};
199+
}, []);
200+
201+
useEffect(() => {
202+
const track = trackRef.current;
203+
if (!track) return;
204+
205+
const updateLoopWidth = () => {
206+
loopWidthRef.current = track.scrollWidth / 4;
207+
};
208+
209+
updateLoopWidth();
210+
211+
const tick = () => {
212+
const loopWidth = loopWidthRef.current;
213+
214+
if (!loopWidth) {
215+
animationFrameRef.current = requestAnimationFrame(tick);
216+
return;
217+
}
218+
219+
const directionMultiplier = direction === "left" ? 1 : -1;
220+
const paused = pauseOnHover && hoveredRef.current && !draggingRef.current;
221+
222+
if (!paused) {
223+
const speed = baseSpeed + boostRef.current;
224+
offsetRef.current += speed * directionMultiplier;
225+
226+
if (offsetRef.current >= loopWidth) {
227+
offsetRef.current -= loopWidth;
228+
} else if (offsetRef.current < 0) {
229+
offsetRef.current += loopWidth;
230+
}
231+
232+
boostRef.current *= 0.94;
233+
if (Math.abs(boostRef.current) < 0.01) {
234+
boostRef.current = 0;
235+
}
236+
}
237+
238+
track.style.transform = `translate3d(${-offsetRef.current}px, 0, 0)`;
239+
animationFrameRef.current = requestAnimationFrame(tick);
240+
};
241+
242+
animationFrameRef.current = requestAnimationFrame(tick);
243+
244+
const resizeObserver = new ResizeObserver(() => {
245+
updateLoopWidth();
246+
});
247+
248+
resizeObserver.observe(track);
249+
250+
return () => {
251+
resizeObserver.disconnect();
252+
if (animationFrameRef.current) {
253+
cancelAnimationFrame(animationFrameRef.current);
254+
}
255+
};
256+
}, [baseSpeed, direction, pauseOnHover, duplicatedItems]);
257+
258+
const onWheel = (e: React.WheelEvent<HTMLDivElement>) => {
259+
const delta = Math.abs(e.deltaY) + Math.abs(e.deltaX);
260+
boostRef.current = Math.min(4.5, boostRef.current + delta * 0.0045);
261+
};
262+
263+
const onMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
264+
draggingRef.current = true;
265+
lastXRef.current = e.clientX;
266+
};
267+
268+
const onMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
269+
if (!draggingRef.current) return;
270+
271+
const deltaX = e.clientX - lastXRef.current;
272+
lastXRef.current = e.clientX;
273+
274+
offsetRef.current -= deltaX;
275+
276+
const loopWidth = loopWidthRef.current;
277+
if (loopWidth > 0) {
278+
while (offsetRef.current < 0) {
279+
offsetRef.current += loopWidth;
280+
}
281+
while (offsetRef.current >= loopWidth) {
282+
offsetRef.current -= loopWidth;
283+
}
284+
}
285+
286+
if (trackRef.current) {
287+
trackRef.current.style.transform = `translate3d(${-offsetRef.current}px, 0, 0)`;
288+
}
289+
};
290+
291+
const stopDragging = () => {
292+
draggingRef.current = false;
293+
};
294+
295+
return (
296+
<div
297+
className={cn(
298+
"group relative overflow-hidden",
299+
"before:pointer-events-none before:absolute before:left-0 before:top-0 before:z-20 before:h-full before:w-24 before:bg-linear-to-r before:from-[#0a0a0a] before:to-transparent",
300+
"after:pointer-events-none after:absolute after:right-0 after:top-0 after:z-20 after:h-full after:w-24 after:bg-linear-to-l after:from-[#0a0a0a] after:to-transparent"
301+
)}
302+
onMouseEnter={() => {
303+
hoveredRef.current = true;
304+
}}
305+
onMouseLeave={() => {
306+
hoveredRef.current = false;
307+
draggingRef.current = false;
308+
}}
309+
onWheel={onWheel}
310+
onMouseDown={onMouseDown}
311+
onMouseMove={onMouseMove}
312+
onMouseUp={stopDragging}
313+
>
314+
<div
315+
ref={trackRef}
316+
className="flex w-max gap-5 py-3 select-none"
317+
style={{
318+
willChange: "transform",
319+
cursor: draggingRef.current ? "grabbing" : "grab"
320+
}}
321+
>
322+
{duplicatedItems.map((item, index) => (
323+
<PressCard key={`${item.id}-${index}`} item={item} />
324+
))}
325+
</div>
326+
</div>
327+
);
328+
}
329+
330+
export function PressReviewSection({ rows, className }: PressReviewSectionProps) {
331+
return (
332+
<section className={cn("relative w-full overflow-hidden py-26", className)}>
333+
<div className="mx-auto w-full max-w-375">
334+
<div className="mb-10 flex flex-col items-center px-6">
335+
<div className="mb-3 rounded-full border border-[#9EF0A8]/18 bg-[#9EF0A8]/8 px-4 py-1.5 text-[11px] font-semibold uppercase tracking-[0.24em] text-[#D6FFDB]">
336+
Rassegna stampa
337+
</div>
338+
339+
<h2 className="text-center text-[64px] font-black leading-none max-[900px]:text-[42px]">
340+
Ci raccontano
341+
</h2>
342+
343+
<p className="mt-4 mb-6 max-w-190 text-center text-[18px] text-white/65 max-[900px]:text-[16px]">
344+
Una selezione di articoli, approfondimenti e pubblicazioni che provano della necessità e della veridicità del nostro progetto.
345+
</p>
346+
</div>
347+
348+
<div className="space-y-4">
349+
<MarqueeRow items={rows[0]} direction="left" baseSpeed={0.48} />
350+
<MarqueeRow items={rows[1]} direction="right" baseSpeed={0.42} />
351+
</div>
352+
</div>
353+
</section>
354+
);
355+
}

0 commit comments

Comments
 (0)