Add a dark theme, default, with a switch in the top bar
Colours move into theme tokens (light values in :root, dark in [data-theme=dark]); the closing contact band inverts on each theme. The choice is stored in localStorage and applied by a head script before the first paint. Voxel covers and image shadows follow the theme. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XtcZtnG4GQA1d6mW5PNrS9
This commit is contained in:
@@ -54,6 +54,10 @@ npm run build # static export in out/
|
||||
|
||||
`./deploy-shanaty.sh` builds and publishes the site to shanaty.ru (needs the ssh key `~/.ssh/MyGitea`). Only needed after changing code or styles.
|
||||
|
||||
## Themes
|
||||
|
||||
The page is **dark by default** (a night version of the viewport: graphite backdrop, brighter axis colours, and a light closing contact band). A button in the top bar switches to the light theme; the choice is remembered in the browser and applied before the first paint, so there is no flash. Colours live in the tokens at the top of `app/globals.css` (`:root` for light, `:root[data-theme='dark']` for dark); components only use those tokens, so a new theme is a new block of values.
|
||||
|
||||
## Design
|
||||
|
||||
The page is laid out like a 3D-editor viewport, a nod to the game-engine work it presents: a studio backdrop with a perspective floor whose red X and blue Z axes match the axis colours of Blender and Unity, and an orientation gizmo in the corner that orbits as the page scrolls (press it to go back to the top). Projects without a demo image get a deterministic isometric voxel sculpture generated from the repo name.
|
||||
|
||||
+82
-3
@@ -1,16 +1,59 @@
|
||||
/* Design tokens: a studio-lit 3D viewport. Axis colours (X red, Y green, Z blue) are marks and lines, not text. */
|
||||
:root {
|
||||
--studio: #e6e9ed;
|
||||
--studio-deep: #cdd3db;
|
||||
color-scheme: light;
|
||||
|
||||
/* Palette. The page is a 3D viewport: a studio backdrop and the three axis colours (X red, Y green, Z blue),
|
||||
which are marks and lines rather than text. Dark is the default theme; see :root[data-theme='dark']. */
|
||||
--paper: #f4f6f8;
|
||||
--ink: #15181d;
|
||||
--muted: #5b6571;
|
||||
--line: rgba(21, 24, 29, 0.14);
|
||||
--x: #e0384b;
|
||||
--y: #5aa300;
|
||||
--z: #2c63e0;
|
||||
--z-text: #2455c9;
|
||||
|
||||
/* Surfaces */
|
||||
--hero-top: #f5f7fa;
|
||||
--hero-bottom: #e6e9ee;
|
||||
--stage-top: #d8dde4;
|
||||
--stage-bottom: #cdd3db;
|
||||
--plate-a: #f3f5f8;
|
||||
--plate-b: #e6e9ed;
|
||||
--plate-c: #d3d9e0;
|
||||
--pill-bg: rgba(255, 255, 255, 0.72);
|
||||
--disc-bg: rgba(244, 246, 248, 0.62);
|
||||
--topbar-bg: rgba(244, 246, 248, 0.86);
|
||||
--grid: rgba(21, 24, 29, 0.2);
|
||||
--img-shadow: rgba(21, 24, 29, 0.28);
|
||||
--dot-glow: rgba(90, 163, 0, 0.2);
|
||||
|
||||
/* Voxel covers of projects without a demo image */
|
||||
--cube-top: #f9fafb;
|
||||
--cube-left: #d6dbe2;
|
||||
--cube-right: #b8c0cb;
|
||||
--cube-edge: rgba(21, 24, 29, 0.16);
|
||||
--cover-grid: rgba(21, 24, 29, 0.09);
|
||||
|
||||
/* The closing contact band is the inverse of the page */
|
||||
--band-bg: #15181d;
|
||||
--band-ink: #f4f6f8;
|
||||
|
||||
/* Light type on a dark ground looks heavier, so the display weight is eased off there */
|
||||
--name-weight: 800;
|
||||
|
||||
/* Derived from the palette above, so they follow the theme */
|
||||
--line: color-mix(in srgb, var(--ink) 14%, transparent);
|
||||
--ring: color-mix(in srgb, var(--ink) 9%, transparent);
|
||||
--horizon: color-mix(in srgb, var(--ink) 22%, transparent);
|
||||
--chip: color-mix(in srgb, var(--ink) 7%, transparent);
|
||||
--ink-2: color-mix(in srgb, var(--ink) 86%, transparent);
|
||||
--ink-3: color-mix(in srgb, var(--ink) 76%, transparent);
|
||||
--ink-line: color-mix(in srgb, var(--ink) 32%, transparent);
|
||||
--band-2: color-mix(in srgb, var(--band-ink) 70%, transparent);
|
||||
--band-3: color-mix(in srgb, var(--band-ink) 58%, transparent);
|
||||
--band-line: color-mix(in srgb, var(--band-ink) 22%, transparent);
|
||||
--band-underline: color-mix(in srgb, var(--band-ink) 38%, transparent);
|
||||
|
||||
--radius-plate: 18px;
|
||||
--radius-control: 10px;
|
||||
--shell: min(1240px, calc(100% - 48px));
|
||||
@@ -21,6 +64,42 @@
|
||||
--font-head: var(--font-display), 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
color-scheme: dark;
|
||||
--paper: #1c2026;
|
||||
--ink: #e9edf2;
|
||||
--muted: #9aa5b1;
|
||||
--x: #ff5a6e;
|
||||
--y: #86d62c;
|
||||
--z: #5b8dff;
|
||||
--z-text: #8db0ff;
|
||||
|
||||
--hero-top: #282d35;
|
||||
--hero-bottom: #1c2026;
|
||||
--stage-top: #14171b;
|
||||
--stage-bottom: #0f1114;
|
||||
--plate-a: #363c46;
|
||||
--plate-b: #2a2f37;
|
||||
--plate-c: #1f2329;
|
||||
--pill-bg: rgba(255, 255, 255, 0.07);
|
||||
--disc-bg: rgba(40, 45, 53, 0.72);
|
||||
--topbar-bg: rgba(28, 32, 38, 0.86);
|
||||
--grid: rgba(255, 255, 255, 0.15);
|
||||
--img-shadow: rgba(0, 0, 0, 0.55);
|
||||
--dot-glow: rgba(134, 214, 44, 0.24);
|
||||
|
||||
--cube-top: #e9edf2;
|
||||
--cube-left: #9aa5b3;
|
||||
--cube-right: #69737f;
|
||||
--cube-edge: rgba(0, 0, 0, 0.4);
|
||||
--cover-grid: rgba(255, 255, 255, 0.09);
|
||||
|
||||
--band-bg: #e8ebef;
|
||||
--band-ink: #15181d;
|
||||
|
||||
--name-weight: 730;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { background: var(--paper); scroll-behavior: smooth; scroll-padding-top: 88px; }
|
||||
body {
|
||||
|
||||
+7
-1
@@ -12,9 +12,15 @@ export const metadata: Metadata = {
|
||||
authors: [{ name: 'Emil Shanayev' }],
|
||||
}
|
||||
|
||||
// Dark is the default theme; a stored choice of "light" is applied before the first paint so the page never flashes.
|
||||
const THEME_SCRIPT = "try{if(localStorage.getItem('theme')==='light')document.documentElement.dataset.theme='light'}catch(e){}"
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en" className={`${display.variable} ${text.variable}`}>
|
||||
<html lang="en" data-theme="dark" className={`${display.variable} ${text.variable}`} suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: THEME_SCRIPT }} />
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ const GRID = 5
|
||||
const TILE = 54
|
||||
const RISE = 30
|
||||
const ACCENTS = ['#e0384b', '#5aa300', '#2c63e0']
|
||||
const NEUTRAL = { top: '#f9fafb', left: '#d6dbe2', right: '#b8c0cb' }
|
||||
const NEUTRAL = { top: 'var(--cube-top)', left: 'var(--cube-left)', right: 'var(--cube-right)' }
|
||||
|
||||
function mix(hex: string, other: number, amount: number): string {
|
||||
const channel = (i: number) => parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16)
|
||||
@@ -82,7 +82,7 @@ export default function Cover({ name }: Readonly<{ name: string }>) {
|
||||
key={n}
|
||||
points={face([point(i, j, 0, 0, 0), point(i, j, 0, TILE / 2, TILE / 4), point(i, j, 0, 0, TILE / 2), point(i, j, 0, -TILE / 2, TILE / 4)])}
|
||||
fill="none"
|
||||
stroke="rgba(21,24,29,0.09)"
|
||||
style={{ stroke: 'var(--cover-grid)' }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -90,10 +90,10 @@ export default function Cover({ name }: Readonly<{ name: string }>) {
|
||||
const z = (k + 1) * RISE
|
||||
const base = k * RISE
|
||||
return (
|
||||
<g key={`${i}-${j}-${k}`} strokeLinejoin="round" stroke="rgba(21,24,29,0.16)" strokeWidth="0.75">
|
||||
<polygon points={face([point(i, j, z, -TILE / 2, TILE / 4), point(i, j, z, 0, TILE / 2), point(i, j, base, 0, TILE / 2), point(i, j, base, -TILE / 2, TILE / 4)])} fill={left} />
|
||||
<polygon points={face([point(i, j, z, 0, TILE / 2), point(i, j, z, TILE / 2, TILE / 4), point(i, j, base, TILE / 2, TILE / 4), point(i, j, base, 0, TILE / 2)])} fill={right} />
|
||||
<polygon points={face([point(i, j, z, 0, 0), point(i, j, z, TILE / 2, TILE / 4), point(i, j, z, 0, TILE / 2), point(i, j, z, -TILE / 2, TILE / 4)])} fill={top} />
|
||||
<g key={`${i}-${j}-${k}`} strokeLinejoin="round" strokeWidth="0.75" style={{ stroke: 'var(--cube-edge)' }}>
|
||||
<polygon points={face([point(i, j, z, -TILE / 2, TILE / 4), point(i, j, z, 0, TILE / 2), point(i, j, base, 0, TILE / 2), point(i, j, base, -TILE / 2, TILE / 4)])} style={{ fill: left }} />
|
||||
<polygon points={face([point(i, j, z, 0, TILE / 2), point(i, j, z, TILE / 2, TILE / 4), point(i, j, base, TILE / 2, TILE / 4), point(i, j, base, 0, TILE / 2)])} style={{ fill: right }} />
|
||||
<polygon points={face([point(i, j, z, 0, 0), point(i, j, z, TILE / 2, TILE / 4), point(i, j, z, 0, TILE / 2), point(i, j, z, -TILE / 2, TILE / 4)])} style={{ fill: top }} />
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import styles from './topbar.module.css'
|
||||
|
||||
type Theme = 'dark' | 'light'
|
||||
|
||||
/** Switches between the dark (default) and light theme; the choice is remembered in this browser. */
|
||||
export default function ThemeToggle() {
|
||||
const [theme, setTheme] = useState<Theme>('dark')
|
||||
|
||||
// The page starts dark; a small script in <head> applies a stored "light" choice before the first paint.
|
||||
useEffect(() => {
|
||||
setTheme(document.documentElement.dataset.theme === 'light' ? 'light' : 'dark')
|
||||
}, [])
|
||||
|
||||
const toggle = () => {
|
||||
const next: Theme = theme === 'dark' ? 'light' : 'dark'
|
||||
document.documentElement.dataset.theme = next
|
||||
try {
|
||||
localStorage.setItem('theme', next)
|
||||
} catch {
|
||||
// Storage can be blocked; the theme still switches for this visit.
|
||||
}
|
||||
setTheme(next)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.toggle}
|
||||
onClick={toggle}
|
||||
aria-label={theme === 'dark' ? 'Switch to the light theme' : 'Switch to the dark theme'}
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2.5v2.2M12 19.3v2.2M4.2 4.2l1.6 1.6M18.2 18.2l1.6 1.6M2.5 12h2.2M19.3 12h2.2M4.2 19.8l1.6-1.6M18.2 5.8l1.6-1.6" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M20.5 14.2A8.5 8.5 0 0 1 9.8 3.5a8.5 8.5 0 1 0 10.7 10.7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useScrolled } from '@/lib/useScroll'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
import styles from './topbar.module.css'
|
||||
|
||||
type Props = { name: string; mailHref: string; showProjects: boolean }
|
||||
@@ -17,6 +18,7 @@ export default function TopBar({ name, mailHref, showProjects }: Readonly<Props>
|
||||
<a href="#experience">Experience</a>
|
||||
<a href="#education">Education</a>
|
||||
</nav>
|
||||
<ThemeToggle />
|
||||
<a className={styles.mail} href={mailHref}>Email me</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(244, 246, 248, 0.62);
|
||||
box-shadow: 0 0 0 1px rgba(21, 24, 29, 0.08);
|
||||
background: var(--disc-bg);
|
||||
box-shadow: 0 0 0 1px var(--ring);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
flex-direction: column;
|
||||
min-height: min(100svh, 1000px);
|
||||
overflow: clip;
|
||||
background: linear-gradient(180deg, #f5f7fa 0%, #e6e9ee 100%);
|
||||
background: linear-gradient(180deg, var(--hero-top) 0%, var(--hero-bottom) 100%);
|
||||
}
|
||||
|
||||
/* The floor is a stage of its own below the text, so the grid never runs through the copy at any window height. */
|
||||
@@ -15,8 +15,8 @@
|
||||
flex: 1;
|
||||
min-height: 260px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #d8dde4 0%, var(--studio-deep) 100%);
|
||||
border-top: 1px solid rgba(21, 24, 29, 0.2);
|
||||
background: linear-gradient(180deg, var(--stage-top) 0%, var(--stage-bottom) 100%);
|
||||
border-top: 1px solid var(--horizon);
|
||||
}
|
||||
|
||||
.stage::after {
|
||||
@@ -44,8 +44,8 @@
|
||||
transform-origin: 50% 0;
|
||||
transform: rotateX(76deg) rotateZ(var(--sway));
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(21, 24, 29, 0.2) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(21, 24, 29, 0.2) 1px, transparent 1px);
|
||||
linear-gradient(to right, var(--grid) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--grid) 1px, transparent 1px);
|
||||
background-size: 80px 80px;
|
||||
background-position: center top;
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 18%, #000 100%);
|
||||
@@ -89,7 +89,7 @@
|
||||
max-width: 100%;
|
||||
padding: 8px 16px 8px 13px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
background: var(--pill-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--line);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
@@ -102,12 +102,12 @@
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--y);
|
||||
box-shadow: 0 0 0 4px rgba(90, 163, 0, 0.2);
|
||||
box-shadow: 0 0 0 4px var(--dot-glow);
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 30px 0 0 -0.045em;
|
||||
font: 800 clamp(56px, 12.6vw, 204px) / 0.88 var(--font-head);
|
||||
font: var(--name-weight) clamp(56px, 12.6vw, 204px) / 0.88 var(--font-head);
|
||||
letter-spacing: -0.048em;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
margin-top: 44px;
|
||||
font-size: 19px;
|
||||
line-height: 1.62;
|
||||
color: rgba(21, 24, 29, 0.88);
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
aspect-ratio: 3 / 2;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-plate);
|
||||
background: radial-gradient(120% 90% at 50% 0%, #f3f5f8 0%, var(--studio) 55%, #d3d9e0 100%);
|
||||
box-shadow: inset 0 0 0 1px rgba(21, 24, 29, 0.09);
|
||||
background: radial-gradient(120% 90% at 50% 0%, var(--plate-a) 0%, var(--plate-b) 55%, var(--plate-c) 100%);
|
||||
box-shadow: inset 0 0 0 1px var(--ring);
|
||||
}
|
||||
|
||||
.plate img,
|
||||
@@ -76,7 +76,7 @@
|
||||
position: relative;
|
||||
object-fit: contain;
|
||||
padding: 6%;
|
||||
filter: drop-shadow(0 10px 24px rgba(21, 24, 29, 0.28));
|
||||
filter: drop-shadow(0 10px 24px var(--img-shadow));
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
@@ -108,14 +108,14 @@
|
||||
|
||||
.description {
|
||||
max-width: 62ch;
|
||||
color: rgba(21, 24, 29, 0.86);
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.points {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
font-size: 15px;
|
||||
color: rgba(21, 24, 29, 0.78);
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.points li {
|
||||
@@ -168,7 +168,7 @@
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1.5px;
|
||||
text-underline-offset: 4px;
|
||||
text-decoration-color: rgba(21, 24, 29, 0.3);
|
||||
text-decoration-color: var(--ink-line);
|
||||
transition: text-decoration-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
.skeleton span {
|
||||
aspect-ratio: 3 / 2;
|
||||
border-radius: var(--radius-plate);
|
||||
background: linear-gradient(160deg, #eef1f4, var(--studio));
|
||||
background: linear-gradient(160deg, var(--plate-a), var(--plate-c));
|
||||
}
|
||||
|
||||
@media (min-width: 641px) and (max-width: 1000px) {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
.skill p {
|
||||
max-width: 52ch;
|
||||
color: rgba(21, 24, 29, 0.82);
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.tags {
|
||||
@@ -49,7 +49,7 @@
|
||||
.tags li {
|
||||
padding: 3px 11px;
|
||||
border-radius: 999px;
|
||||
background: rgba(21, 24, 29, 0.06);
|
||||
background: var(--chip);
|
||||
font-size: 13.5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -136,8 +136,8 @@
|
||||
.contact {
|
||||
margin-top: var(--section);
|
||||
padding: clamp(72px, 9vw, 128px) 0 40px;
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
background: var(--band-bg);
|
||||
color: var(--band-ink);
|
||||
}
|
||||
|
||||
.contactTitle {
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
.contactLead {
|
||||
margin-top: 16px;
|
||||
color: rgba(244, 246, 248, 0.7);
|
||||
color: var(--band-2);
|
||||
}
|
||||
|
||||
.mail {
|
||||
@@ -177,19 +177,19 @@
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 18px 0;
|
||||
border-top: 1px solid rgba(244, 246, 248, 0.2);
|
||||
border-top: 1px solid var(--band-line);
|
||||
}
|
||||
|
||||
.contacts span {
|
||||
font-size: 14px;
|
||||
color: rgba(244, 246, 248, 0.6);
|
||||
color: var(--band-3);
|
||||
}
|
||||
|
||||
.contacts a {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1.5px;
|
||||
text-underline-offset: 4px;
|
||||
text-decoration-color: rgba(244, 246, 248, 0.35);
|
||||
text-decoration-color: var(--band-underline);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
.legal {
|
||||
margin-top: clamp(48px, 6vw, 80px);
|
||||
font-size: 14px;
|
||||
color: rgba(244, 246, 248, 0.55);
|
||||
color: var(--band-3);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.solid {
|
||||
background: rgba(244, 246, 248, 0.86);
|
||||
background: var(--topbar-bg);
|
||||
border-bottom-color: var(--line);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
@@ -48,6 +48,7 @@
|
||||
}
|
||||
|
||||
.mail {
|
||||
margin-left: -12px;
|
||||
padding: 10px 18px;
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--ink);
|
||||
@@ -67,3 +68,32 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: var(--radius-control);
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
box-shadow: inset 0 0 0 1px var(--line);
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.toggle:hover {
|
||||
box-shadow: inset 0 0 0 1.5px var(--ink);
|
||||
}
|
||||
|
||||
.toggle svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user