profile refactor

This commit is contained in:
2026-06-29 19:22:24 +03:00
parent 7af549fb1d
commit d3c9662f82
11 changed files with 281 additions and 49 deletions

View File

@@ -0,0 +1 @@
export { NotFoundPage } from './ui/NotFoundPage'

View File

@@ -0,0 +1,141 @@
.page {
position: relative;
min-height: 70vh;
display: flex;
align-items: center;
justify-content: center;
padding: 80px 20px;
overflow: hidden;
}
/* ---- Falling coins (decorative background) ---- */
.rain {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
}
.drop {
position: absolute;
top: -80px;
opacity: 0.55;
animation-name: fall;
animation-timing-function: linear;
animation-iteration-count: infinite;
filter: drop-shadow(0 6px 16px rgba(0, 0, 0, 0.35));
}
@keyframes fall {
0% {
transform: translateY(-80px) rotate(0deg);
opacity: 0;
}
10% {
opacity: 0.55;
}
90% {
opacity: 0.55;
}
100% {
transform: translateY(85vh) rotate(360deg);
opacity: 0;
}
}
/* ---- Foreground content ---- */
.content {
position: relative;
z-index: 1;
text-align: center;
max-width: 520px;
}
.code {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.digit {
font-family: var(--font-mono);
font-size: 160px;
font-weight: 800;
line-height: 1;
color: var(--text-primary);
text-shadow: 0 8px 40px rgba(74, 109, 255, 0.4);
}
/* The middle "0" is a coin that flips like it's spinning in the air. */
.coin {
display: inline-flex;
animation: flip 2.6s ease-in-out infinite;
transform-style: preserve-3d;
filter: drop-shadow(0 12px 30px rgba(247, 147, 26, 0.45));
}
@keyframes flip {
0%,
100% {
transform: rotateY(0deg) translateY(0);
}
50% {
transform: rotateY(180deg) translateY(-16px);
}
}
.title {
margin: 36px 0 12px;
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
}
.subtitle {
margin: 0 0 32px;
font-size: 16px;
line-height: 1.5;
color: var(--text-secondary);
}
.button {
display: inline-block;
padding: 14px 32px;
border-radius: 12px;
background: var(--interactive);
color: var(--text-primary);
font-size: 16px;
font-weight: 600;
text-decoration: none;
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.button:hover {
background: var(--highlight);
transform: translateY(-2px);
box-shadow: 0 10px 28px rgba(0, 212, 255, 0.35);
}
@media (max-width: 600px) {
.digit {
font-size: 96px;
}
.coin > :global(div) {
width: 88px !important;
height: 88px !important;
font-size: 40px !important;
}
.title {
font-size: 22px;
}
}
@media (prefers-reduced-motion: reduce) {
.drop,
.coin {
animation: none;
}
}

View File

@@ -0,0 +1,78 @@
import { Link } from 'react-router-dom'
import { Header } from '@widgets/header'
import { Footer } from '@widgets/footer'
import { TokenIcon } from '@shared/ui/TokenIcon'
import { ROUTES } from '@shared/config/routes'
import styles from './NotFoundPage.module.css'
/** Decorative coin set — purely visual, no live rates. */
const COINS = [
{ letter: '₿', color: '#f7931a' },
{ letter: 'Ξ', color: '#627eea' },
{ letter: '₮', color: '#26a17b' },
{ letter: '◎', color: '#9945ff' },
{ letter: '$', color: '#2775ca' },
{ letter: '₽', color: '#4a6dff' },
{ letter: 'Ʉ', color: '#00d4ff' },
]
/** Pre-baked rain layout so the scene looks the same on every render. */
const RAIN = [
{ left: 6, size: 34, delay: 0, duration: 7.5 },
{ left: 18, size: 26, delay: 2.1, duration: 9 },
{ left: 29, size: 44, delay: 4.3, duration: 6.5 },
{ left: 41, size: 22, delay: 1.2, duration: 10 },
{ left: 54, size: 38, delay: 3.4, duration: 8 },
{ left: 67, size: 28, delay: 0.6, duration: 9.5 },
{ left: 78, size: 48, delay: 2.8, duration: 7 },
{ left: 88, size: 24, delay: 5, duration: 8.5 },
{ left: 95, size: 32, delay: 1.7, duration: 9.2 },
]
export function NotFoundPage() {
return (
<>
<Header />
<main className={styles.page}>
<div className={styles.rain} aria-hidden="true">
{RAIN.map((coin, i) => {
const token = COINS[i % COINS.length]
return (
<span
key={i}
className={styles.drop}
style={{
left: `${coin.left}%`,
animationDelay: `${coin.delay}s`,
animationDuration: `${coin.duration}s`,
}}
>
<TokenIcon letter={token.letter} color={token.color} size={coin.size} />
</span>
)
})}
</div>
<div className={styles.content}>
<div className={styles.code} aria-hidden="true">
<span className={styles.digit}>4</span>
<span className={styles.coin}>
<TokenIcon letter="₿" color="#f7931a" size={140} />
</span>
<span className={styles.digit}>4</span>
</div>
<h1 className={styles.title}>Такой страницы не существует</h1>
<p className={styles.subtitle}>
Похоже, курс этого маршрута обнулился. Проверьте адрес или вернитесь на главную.
</p>
<Link to={ROUTES.HOME} className={styles.button}>
На главную
</Link>
</div>
</main>
<Footer />
</>
)
}