Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 | 3x 6x 6x 6x 6x 6x 6x 6x 6x 3x 3x 3x 2x 2x 2x 3x 6x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 6x 3x 3x 3x 3x 3x 3x 3x 6x 6x 6x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 6x 36x 6x 30x 6x 6x 6x 6x 6x 6x 24x 54x 36x 36x 4x | 'use client';
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Section } from '../components/Section';
import { useShop } from '../context/ShopContext';
import { ProductCard } from '../components/ProductCard';
import { CATEGORIES, CATEGORY_IMAGE_DEFAULTS } from '../constants';
import { Truck, ShieldCheck, Zap, CreditCard, ArrowRight, Star, ArrowUpRight, Activity, CheckCircle2 } from 'lucide-react';
import { ReviewSection } from '../components/ReviewSection';
const Home: React.FC = () => {
const { products, reviews } = useShop();
const [categoryImages, setCategoryImages] = React.useState<Record<string, string>>(CATEGORY_IMAGE_DEFAULTS);
const carouselRef = React.useRef<HTMLDivElement | null>(null);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const safeProducts = React.useMemo(() => (Array.isArray(products) ? products : []), [products]);
const featuredOffers = React.useMemo(() => safeProducts.filter(p => p.salePrice), [safeProducts]);
const weeklyHighlights = React.useMemo(() => {
const seen = new Set<string>();
const combined = [...featuredOffers, ...safeProducts];
const unique = combined.filter((item) => {
Iif (seen.has(item.id)) {
return false;
}
seen.add(item.id);
return true;
});
return unique.slice(0, 12);
}, [featuredOffers, safeProducts]);
// Fetch category images
React.useEffect(() => {
const fetchCategoryImages = async () => {
try {
const res = await fetch('/api/category-images', {
next: { revalidate: 60 } // Cache for 60 seconds
});
Iif (!res.ok) {
throw new Error(`Failed to fetch: ${res.status}`);
}
const data = await res.json();
Eif (Array.isArray(data)) {
const imageMap: Record<string, string> = {};
data.forEach((item: { categoryName: string; imageUrl?: string | null }) => {
if (item?.imageUrl && !item.imageUrl.includes('picsum.photos')) {
imageMap[item.categoryName] = item.imageUrl;
}
});
setCategoryImages({
...CATEGORY_IMAGE_DEFAULTS,
...imageMap,
});
}
} catch (error) {
console.error('Error fetching category images:', error);
}
};
fetchCategoryImages();
}, []);
const updateCarouselState = React.useCallback(() => {
const node = carouselRef.current;
Iif (!node) {
return;
}
const maxScrollLeft = node.scrollWidth - node.clientWidth;
const current = node.scrollLeft;
const threshold = 4;
setCanScrollPrev(current > threshold);
setCanScrollNext(current < maxScrollLeft - threshold);
}, []);
const getScrollAmount = React.useCallback((node: HTMLDivElement) => {
const firstItem = node.querySelector<HTMLElement>('[data-carousel-item]');
if (firstItem) {
const styles = window.getComputedStyle(node);
const gapValue = parseFloat(styles.columnGap || styles.gap || '0');
const gap = Number.isNaN(gapValue) ? 0 : gapValue;
return firstItem.offsetWidth + gap;
}
return node.clientWidth * 0.9;
}, []);
const scrollCarousel = React.useCallback((direction: 'prev' | 'next') => {
const node = carouselRef.current;
if (!node) {
return;
}
const amount = getScrollAmount(node);
node.scrollBy({
left: direction === 'next' ? amount : -amount,
behavior: 'smooth'
});
}, [getScrollAmount]);
React.useEffect(() => {
const node = carouselRef.current;
Iif (!node) {
return;
}
updateCarouselState();
const handleScroll = () => updateCarouselState();
node.addEventListener('scroll', handleScroll, { passive: true });
let resizeObserver: ResizeObserver | null = null;
const hasResizeObserver = typeof ResizeObserver !== 'undefined';
if (hasResizeObserver) {
resizeObserver = new ResizeObserver(() => updateCarouselState());
resizeObserver.observe(node);
} else E{
window.addEventListener('resize', handleScroll);
}
return () => {
node.removeEventListener('scroll', handleScroll);
if (resizeObserver) {
resizeObserver.disconnect();
} else E{
window.removeEventListener('resize', handleScroll);
}
};
}, [updateCarouselState, weeklyHighlights.length]);
// Helper to map categories to specific bento grid areas
const getCategoryStyle = (index: number, categoryName: string) => {
if (categoryName === "VARJOLIITIMIEN PAKKAUSTARVIKKEET") {
return "md:col-span-2 md:row-span-1";
}
switch(index) {
case 0: return "md:col-span-2 md:row-span-2"; // Big Square
case 1: return "md:col-span-1 md:row-span-1"; // Small
case 2: return "md:col-span-1 md:row-span-2"; // Tall
case 3: return "md:col-span-1 md:row-span-1"; // Small
default: return "md:col-span-1 md:row-span-1";
}
};
return (
<>
{/* 1. MINIMALIST EDITORIAL HERO */}
<div className="max-w-[100rem] mx-auto px-6 sm:px-8 lg:px-12 pt-6 pb-12">
{/* The Main Container: Clean, thin border, unified */}
<div className="flex flex-col lg:flex-row bg-white rounded-[2.4rem] overflow-hidden border border-gray-200 shadow-xl shadow-gray-100 min-h-[650px]">
{/* LEFT: PURE VISUAL (55%) - No Clutter */}
<div className="lg:w-[55%] relative group min-h-[400px] lg:min-h-full bg-gray-100">
<Image
src="/img2.webp"
alt="Main Hero"
fill
sizes="(max-width: 1024px) 100vw, 55vw"
className="object-cover transition-transform group-hover:scale-105"
style={{ transitionDuration: '3s' }}
priority
quality={85}
/>
{/* Subtle gradient for text readability only at bottom */}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent"></div>
{/* Hero Text - Standing Alone */}
<div className="absolute bottom-0 left-0 p-10 md:p-14 w-full z-20">
<h1 className="text-5xl md:text-7xl font-bold text-white leading-[0.9] tracking-tight drop-shadow-sm">
LIIDÄ <br/>KORKEALLA.
</h1>
</div>
</div>
{/* RIGHT: CONTEXT & NAVIGATION (45%) */}
<div className="lg:w-[45%] bg-white p-10 md:p-14 flex flex-col justify-between relative border-l border-gray-100">
{/* Content Top */}
<div>
<div className="flex items-center gap-2 mb-8 text-gray-400">
<div className="w-8 h-8 rounded-full bg-gray-50 border border-gray-100 flex items-center justify-center text-black">
<CheckCircle2 size={16} strokeWidth={2.5} />
</div>
<span className="text-xs font-bold uppercase tracking-widest text-black">Luotettava kumppani</span>
</div>
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-black leading-tight tracking-tight">
Tervetuloa <br/>Varjoliitokauppaan!
</h2>
<div className="prose prose-base text-gray-600 font-medium leading-relaxed space-y-4 mb-8">
<p>
Meiltä saat kaikki varusteet ja tarvikkeet Varjoliitoharrastukseesi, oli sitten kyseessä perinteinen Varjoliito tai moottoroitu lentäminen.
</p>
<p>
Yli 10 vuoden kokemus Varjoliidon kouluttamisesta ja varusteiden myymisestä takaa ammattitaitoisen opastuksen.
</p>
</div>
{/* PILL BUTTON - Minimalist */}
<div>
<Link
href="/yhteystiedot"
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-full border border-black bg-black text-sm font-bold text-white hover:border-black hover:bg-white hover:text-black transition-all duration-300"
>
Ota yhteyttä <ArrowRight size={16} />
</Link>
</div>
</div>
{/* Bottom Action Grid - Clean Tiles */}
<div className="mt-12 grid grid-cols-2 gap-4">
<Link href="/kauppa?sort=newest" className="group flex flex-col justify-between p-5 rounded-2xl bg-gray-50 border border-gray-100 hover:border-black transition-all duration-300">
<div className="flex justify-between items-start mb-4">
<ArrowUpRight size={20} className="text-gray-400 group-hover:text-black transition-colors" />
</div>
<div>
<h3 className="text-lg font-bold leading-none mt-1">Uusimmat<br/>Siivet</h3>
</div>
</Link>
<Link href="/kaytetyt" className="group flex flex-col justify-between p-5 rounded-2xl bg-gray-50 border border-gray-100 hover:border-black transition-all duration-300">
<div className="flex justify-between items-start mb-4">
<Activity size={20} className="text-gray-400 group-hover:text-black transition-colors" />
</div>
<div>
<h3 className="text-lg font-bold leading-none mt-1">Käytetyt<br/>Varusteet</h3>
</div>
</Link>
</div>
</div>
</div>
</div>
{/* 2. VALUE PROPS STRIP */}
<div className="border-b border-gray-100 bg-white mb-8">
<div className="max-w-[100rem] mx-auto px-6 sm:px-8 lg:px-12 py-10">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-6">
{[
{ icon: Truck, title: "Nopea toimitus", desc: "Tuotteet heti varastosta" },
{ icon: ShieldCheck, title: "Turvallisuus", desc: "Sertifioidut varusteet" },
{ icon: Zap, title: "Asiantuntemus", desc: "10v kokemus alalta" },
{ icon: CreditCard, title: "Joustava maksu", desc: "Tilaa nyt, maksa laskulla" }
].map((item, i) => (
<div key={i} className="flex items-center gap-4 opacity-80 hover:opacity-100 transition-opacity">
<div className="text-black">
<item.icon size={24} strokeWidth={1.5} />
</div>
<div>
<h4 className="font-bold text-sm uppercase tracking-tight text-black">{item.title}</h4>
<p className="text-xs text-gray-500 font-medium">{item.desc}</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* 3. CATEGORY MOSAIC */}
<Section variant="white" className="py-20">
<div className="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-4">
<div>
<span className="text-xs font-bold text-gray-400 uppercase tracking-widest">Osastot</span>
<h2 className="text-4xl md:text-5xl font-bold mt-2 tracking-tight text-black uppercase">Selaa tuoteryhmiä</h2>
</div>
<Link href="/kauppa" className="group flex items-center gap-3 text-sm font-bold pb-1 hover:text-black transition-colors">
Näytä kaikki osastot
<div className="w-10 h-10 rounded-full bg-gray-100 text-black flex items-center justify-center group-hover:bg-black group-hover:text-white transition-all">
<ArrowRight size={16} strokeWidth={2}/>
</div>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 auto-rows-[300px] gap-4 md:gap-6">
{CATEGORIES.filter(cat => cat.name !== "MITTARIT").slice(0, 6).map((cat, idx) => {
const imageUrl = categoryImages[cat.name];
return (
<Link
key={idx}
href={`/kauppa?category=${encodeURIComponent(cat.name)}`}
className={`group relative rounded-[2rem] overflow-hidden border border-gray-200 bg-gradient-to-br from-gray-200 to-gray-300 hover:shadow-xl transition-all duration-500 ${getCategoryStyle(idx, cat.name)}`}
>
{imageUrl && (
<Image
src={imageUrl}
alt={cat.name}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover transition-transform duration-700 group-hover:scale-105"
priority={true}
quality={90}
/>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/10 to-transparent opacity-80 group-hover:opacity-90 transition-opacity"></div>
<div className="absolute bottom-0 left-0 p-8 w-full z-20">
<div className="flex items-end justify-between">
<div className="max-w-[75%]">
<h3 className={`font-bold text-white leading-none uppercase tracking-tight mb-2 ${idx === 0 ? 'text-4xl' : 'text-2xl'} break-words`}>
{cat.name}
</h3>
<p className="text-gray-300 text-xs font-bold uppercase tracking-widest opacity-0 group-hover:opacity-100 transform translate-y-4 group-hover:translate-y-0 transition-all duration-300">
{cat.subcategories?.length || 0} Alakategoriaa
</p>
</div>
<div className="w-12 h-12 rounded-full bg-white text-black flex items-center justify-center shadow-lg transform translate-y-4 opacity-0 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-300">
<ArrowRight size={20} strokeWidth={2} />
</div>
</div>
</div>
</Link>
);
})}
<Link href="/kauppa" className="md:col-span-1 md:row-span-1 rounded-[2rem] border border-gray-200 flex flex-col items-center justify-center bg-white hover:bg-[#3d3d3d] hover:border-black hover:text-white transition-colors group relative overflow-hidden duration-300">
<div className="relative z-10 flex flex-col items-center">
<div className="w-16 h-16 rounded-full bg-gray-50 border border-gray-200 group-hover:bg-white/10 group-hover:border-white/20 flex items-center justify-center mb-6 transition-all shadow-sm">
<ArrowRight size={24} className="text-black group-hover:text-white" />
</div>
<span className="font-bold text-xs uppercase tracking-widest text-black group-hover:text-white transition-colors">Kaikki tuotteet</span>
</div>
</Link>
</div>
</Section>
{/* 4. PRODUCT CAROUSEL */}
<Section variant="gray" className="py-24">
<div className="max-w-[100rem] mx-auto">
<div className="flex items-center justify-between mb-12">
<h2 className="text-3xl md:text-4xl font-bold uppercase tracking-tight">Viikon nostot</h2>
<div className="flex gap-2">
<button
type="button"
onClick={() => scrollCarousel('prev')}
disabled={!canScrollPrev}
aria-label="Edellinen"
className="w-12 h-12 rounded-full border border-gray-200 flex items-center justify-center hover:bg-black hover:text-white hover:border-black transition-all bg-white shadow-sm disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-white disabled:hover:text-black disabled:hover:border-gray-200"
>
<ArrowRight className="rotate-180" size={20} />
</button>
<button
type="button"
onClick={() => scrollCarousel('next')}
disabled={!canScrollNext}
aria-label="Seuraava"
className="w-12 h-12 rounded-full border border-gray-200 flex items-center justify-center hover:bg-black hover:text-white hover:border-black transition-all bg-white shadow-sm disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-white disabled:hover:text-black disabled:hover:border-gray-200"
>
<ArrowRight size={20} />
</button>
</div>
</div>
<div className="relative">
<div
ref={carouselRef}
className="grid grid-flow-col auto-cols-[85%] sm:auto-cols-[45%] lg:auto-cols-[23%] gap-6 overflow-x-auto scroll-smooth snap-x snap-mandatory pb-4"
>
{weeklyHighlights.map(p => (
<div key={p.id} data-carousel-item className="snap-start">
<ProductCard product={p} />
</div>
))}
</div>
<div
className={`pointer-events-none absolute inset-y-0 left-0 w-8 sm:w-10 lg:w-12 bg-gradient-to-r from-[#f5f5f5] to-transparent transition-opacity duration-300 ${canScrollPrev ? 'opacity-100' : 'opacity-0'}`}
/>
<div
className={`pointer-events-none absolute inset-y-0 right-0 w-8 sm:w-10 lg:w-12 bg-gradient-to-l from-[#f5f5f5] to-transparent transition-opacity duration-300 ${canScrollNext ? 'opacity-100' : 'opacity-0'}`}
/>
</div>
</div>
</Section>
{/* 5. SPLIT SECTION: TRUST & BRAND */}
<div className="bg-white py-24 border-t border-gray-100">
<div className="max-w-[100rem] mx-auto px-6 sm:px-8 lg:px-12">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
{/* Reviews Block */}
<div className="bg-[#f8f8f8] rounded-[2rem] p-10 md:p-14 border border-gray-200">
<div className="flex items-center gap-3 mb-8">
<div className="w-8 h-8 rounded-full bg-black flex items-center justify-center text-white">
<Star className="fill-white w-4 h-4" />
</div>
<span className="font-bold uppercase tracking-widest text-xs">Kokemuksia</span>
</div>
{/* Complete Review Section with Summary, Form, and Reviews */}
<ReviewSection reviews={reviews} />
</div>
{/* Brand Story Block */}
<div className="relative rounded-[2rem] overflow-hidden min-h-[500px] group">
<Image
src="/img2.webp"
alt="About us"
fill
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover transition-transform duration-1000 group-hover:scale-105"
loading="lazy"
quality={85}
/>
<div className="absolute inset-0 bg-black/40 group-hover:bg-black/30 transition-colors"></div>
<div className="absolute inset-0 p-12 flex flex-col justify-end text-white">
<h3 className="text-4xl md:text-5xl font-bold mb-6 uppercase tracking-tight">Turvallisuus<br/>edellä.</h3>
<p className="text-lg text-gray-100 font-medium mb-10 max-w-lg leading-relaxed">
Emme myy mitään mitä emme itse käyttäisi. Jokainen tuote valikoimassamme on testattu ja hyväksytty ammattilaisten toimesta.
</p>
<Link href="/tietoa-meista" className="bg-white text-black px-8 py-4 rounded-full font-bold text-sm inline-flex items-center gap-2 hover:bg-gray-100 transition-all hover:scale-105 w-fit border-2 border-white">
Lue tarinamme <ArrowRight size={18} />
</Link>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default Home;
|