feat: hero illustration — настоящая картинка вместо blob-фона
- HeroImage компонент: WebP с 3 размерами (800/1280/1920) + <picture> srcset - На десктопе: справа, fade с левого края, lёгкий parallax при скролле - На мобиле: сверху, фейд к контенту, без parallax - В тёмной теме картинка приглушается opacity 0.55 + filter - max-width текстового блока скорректирован чтобы не наезжать на иллюстрацию
This commit is contained in:
+6
-6
@@ -2,7 +2,7 @@ import Link from 'next/link';
|
|||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
import ArticleCard from '@/components/ArticleCard';
|
import ArticleCard from '@/components/ArticleCard';
|
||||||
import HeroBackground from '@/components/HeroBackground';
|
import HeroImage from '@/components/HeroImage';
|
||||||
import Stats from '@/components/Stats';
|
import Stats from '@/components/Stats';
|
||||||
import Reveal from '@/components/Reveal';
|
import Reveal from '@/components/Reveal';
|
||||||
import { listArticles, listTags, getStats } from '@/lib/engine';
|
import { listArticles, listTags, getStats } from '@/lib/engine';
|
||||||
@@ -31,11 +31,11 @@ export default async function HomePage() {
|
|||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<section className="relative">
|
<section className="relative overflow-hidden">
|
||||||
<HeroBackground />
|
<HeroImage />
|
||||||
<div className="container-wide pt-12 pb-12 sm:pt-24 sm:pb-20 relative">
|
<div className="container-wide pt-8 pb-12 sm:pt-20 sm:pb-24 lg:pt-28 lg:pb-32 relative">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<div className="max-w-3xl reveal">
|
<div className="max-w-xl lg:max-w-2xl reveal">
|
||||||
<div
|
<div
|
||||||
className="inline-flex items-center gap-2 text-xs accent px-3 py-1.5 rounded-full mb-6"
|
className="inline-flex items-center gap-2 text-xs accent px-3 py-1.5 rounded-full mb-6"
|
||||||
style={{ background: 'rgb(var(--accent) / 0.1)', border: '1px solid rgb(var(--accent) / 0.25)' }}
|
style={{ background: 'rgb(var(--accent) / 0.1)', border: '1px solid rgb(var(--accent) / 0.25)' }}
|
||||||
@@ -47,7 +47,7 @@ export default async function HomePage() {
|
|||||||
Практический ИИ.<br />
|
Практический ИИ.<br />
|
||||||
<span className="mute">Без воды и хайпа.</span>
|
<span className="mute">Без воды и хайпа.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-base sm:text-xl mute mb-8 max-w-2xl leading-relaxed">
|
<p className="text-base sm:text-lg lg:text-xl mute mb-8 max-w-lg leading-relaxed">
|
||||||
Промпты, кейсы, инструменты и разборы. Эксперимент: блог, который ведёт ИИ — а человек только следит за курсом.
|
Промпты, кейсы, инструменты и разборы. Эксперимент: блог, который ведёт ИИ — а человек только следит за курсом.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
'use client';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hero иллюстрация с лёгким parallax при скролле + плавный fade с левого края,
|
||||||
|
* чтобы картинка органично «вытаивала» в фон под текстом.
|
||||||
|
*/
|
||||||
|
export default function HeroImage() {
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
let raf = 0;
|
||||||
|
const onScroll = () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
raf = requestAnimationFrame(() => {
|
||||||
|
const y = window.scrollY;
|
||||||
|
// едет вверх медленнее, чем страница
|
||||||
|
const offset = Math.min(60, y * 0.15);
|
||||||
|
if (ref.current) ref.current.style.transform = `translateY(-${offset}px)`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="hero-image-wrap" aria-hidden="true">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
srcSet="/uploads/hero-1920.webp 1920w, /uploads/hero-1280.webp 1280w, /uploads/hero-800.webp 800w"
|
||||||
|
sizes="(min-width: 1024px) 65vw, 100vw"
|
||||||
|
type="image/webp"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
ref={ref}
|
||||||
|
src="/uploads/hero-1280.webp"
|
||||||
|
alt=""
|
||||||
|
className="hero-image"
|
||||||
|
loading="eager"
|
||||||
|
fetchPriority="high"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
.hero-image-wrap {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; right: 0; bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.hero-image {
|
||||||
|
position: absolute;
|
||||||
|
top: -40px;
|
||||||
|
right: -2%;
|
||||||
|
width: auto;
|
||||||
|
height: 115%;
|
||||||
|
max-width: none;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: right top;
|
||||||
|
opacity: 0.85;
|
||||||
|
will-change: transform;
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
|
/* плавный фейд от центра к левому краю — картинка тает */
|
||||||
|
-webkit-mask-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(0,0,0,0.4) 30%,
|
||||||
|
black 55%,
|
||||||
|
black 100%
|
||||||
|
);
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(0,0,0,0.4) 30%,
|
||||||
|
black 55%,
|
||||||
|
black 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
:global(.dark) .hero-image {
|
||||||
|
opacity: 0.55;
|
||||||
|
filter: brightness(0.85) saturate(0.9);
|
||||||
|
}
|
||||||
|
/* нижний fade-out к контенту */
|
||||||
|
.hero-image-wrap::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; bottom: 0;
|
||||||
|
height: 120px;
|
||||||
|
background: linear-gradient(to bottom, transparent, rgb(var(--bg)));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
/* На мобилке картинка сверху, под ней — текст */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.hero-image-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100vw;
|
||||||
|
margin-left: -1rem;
|
||||||
|
margin-right: -1rem;
|
||||||
|
height: 240px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.hero-image {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-position: right center;
|
||||||
|
opacity: 0.95;
|
||||||
|
-webkit-mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
|
||||||
|
mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
|
||||||
|
}
|
||||||
|
.hero-image-wrap::after { display: none; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user