Apply type-themed styling to grid, games, search
- Cards: primary-type gradient tint, radial spotlight behind the sprite, ghost dex number, official artwork by default, spring hover lift - Game picker cards + search rows: rounder, soft shadows, hover motion - Progress bar gradient; pill-shaped search inputs - Shared typeHex() helper (src/lib/type-color.js), reused by detail + cards Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
c9247cd677
commit
b4f2460341
24
README.md
24
README.md
@ -49,14 +49,26 @@ src/
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Scaffold — build steps 1–5 of the design are in place: responsive shell,
|
Working app with a type-themed UI:
|
||||||
snapshot pipeline, dex grid with per-dex progress, game picker, lazy detail
|
|
||||||
page, offline search, settings with export/import, and the service worker.
|
|
||||||
|
|
||||||
Not yet done: evolution chain, version-exclusive badges, richer filters
|
- **Dex grid** — type-tinted cards (official artwork, spotlight, ghost number),
|
||||||
(type/generation/legendary — the snapshot would need legendary flags),
|
per-dex progress, name/number filter, caught/favorite filters.
|
||||||
|
- **Detail** — type-gradient hero + tabbed sheet (About / Stats / Evolution /
|
||||||
|
Moves / Locations); coloured animated stat bars; defensive type matchups;
|
||||||
|
evolution chain re-parented to the selected game's generation; learnset with
|
||||||
|
per-move power/type/accuracy/PP/effect (era-accurate via `past_values`,
|
||||||
|
incl. pre-Gen-4 physical/special-by-type); wild encounter locations.
|
||||||
|
- **Game-aware** — abilities gated to Gen 3+ (hidden to Gen 5+); type chart
|
||||||
|
applies Gen 1 / pre-Gen 6 rules.
|
||||||
|
- **Games / Search / Settings** — game picker with sub-dex switch, offline
|
||||||
|
global search, theme + sprite-style + JSON export/import.
|
||||||
|
- **PWA** — Workbox SW: precache shell + snapshot, SWR for API JSON,
|
||||||
|
cache-first LRU for sprites, in-app update toast.
|
||||||
|
|
||||||
|
Not yet done: version-exclusive badges, type/generation filters on the grid,
|
||||||
|
an interactive region map (PokéAPI has no map imagery or coordinates),
|
||||||
skeleton loaders, install-prompt handling, PNG/maskable raster icons
|
skeleton loaders, install-prompt handling, PNG/maskable raster icons
|
||||||
(currently an SVG icon only), and a Lighthouse PWA pass.
|
(currently SVG only), and a Lighthouse PWA pass.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@ -2,14 +2,17 @@ import { el } from '../lib/dom.js';
|
|||||||
import { Sprite } from './Sprite.js';
|
import { Sprite } from './Sprite.js';
|
||||||
import { TypeChip } from './TypeChip.js';
|
import { TypeChip } from './TypeChip.js';
|
||||||
import { entry, toggle } from '../store/selection.js';
|
import { entry, toggle } from '../store/selection.js';
|
||||||
|
import { typeHex } from '../lib/type-color.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One Pokémon in the dex grid. The card is a link to the detail route; the
|
* One Pokémon in the dex grid. Tinted by its primary type: a soft gradient,
|
||||||
* caught toggle is a real button layered on top and updates itself in place
|
* a type-coloured spotlight behind the sprite, and a big translucent number.
|
||||||
* so toggling never repaints the whole grid.
|
* The card is a link to the detail route; the caught toggle is a real button
|
||||||
|
* layered on top that updates itself so toggling never repaints the grid.
|
||||||
*/
|
*/
|
||||||
export function Card(species, number, { spriteStyle = 'default', versionGroup } = {}) {
|
export function Card(species, number, { spriteStyle = 'official', versionGroup } = {}) {
|
||||||
const state = entry(species.id);
|
const state = entry(species.id);
|
||||||
|
const mainType = (species.types || [])[0] || 'normal';
|
||||||
|
|
||||||
const caughtBtn = el('button', {
|
const caughtBtn = el('button', {
|
||||||
class: 'card__caught',
|
class: 'card__caught',
|
||||||
@ -31,15 +34,15 @@ export function Card(species, number, { spriteStyle = 'default', versionGroup }
|
|||||||
{
|
{
|
||||||
class: `card${state.caught ? ' is-caught' : ''}`,
|
class: `card${state.caught ? ' is-caught' : ''}`,
|
||||||
href: `#/pokemon/${species.id}`,
|
href: `#/pokemon/${species.id}`,
|
||||||
|
dataset: { type: mainType, sprite: spriteStyle },
|
||||||
|
style: `--type-main:${typeHex(mainType)}`,
|
||||||
},
|
},
|
||||||
|
el('span', { class: 'card__bignum' }, String(number ?? species.id).padStart(3, '0')),
|
||||||
|
el('span', { class: 'card__spot', 'aria-hidden': 'true' }),
|
||||||
|
Sprite(species.id, { style: spriteStyle, versionGroup, alt: species.name, size: 112 }),
|
||||||
el('span', { class: 'card__num' }, `#${String(number ?? species.id).padStart(4, '0')}`),
|
el('span', { class: 'card__num' }, `#${String(number ?? species.id).padStart(4, '0')}`),
|
||||||
Sprite(species.id, { style: spriteStyle, versionGroup, alt: species.name, size: 96 }),
|
|
||||||
el('span', { class: 'card__name' }, species.name.replace(/-/g, ' ')),
|
el('span', { class: 'card__name' }, species.name.replace(/-/g, ' ')),
|
||||||
el(
|
el('span', { class: 'card__types' }, ...(species.types || []).map(TypeChip)),
|
||||||
'span',
|
|
||||||
{ class: 'card__types' },
|
|
||||||
...(species.types || []).map(TypeChip),
|
|
||||||
),
|
|
||||||
caughtBtn,
|
caughtBtn,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
21
src/lib/type-color.js
Normal file
21
src/lib/type-color.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Resolve a Pokémon type to its hex colour by reading the --type-* CSS
|
||||||
|
// custom properties once and memoising. Type colours don't change between
|
||||||
|
// light and dark, so the cache is safe for the session.
|
||||||
|
const TYPES = [
|
||||||
|
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
|
||||||
|
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark',
|
||||||
|
'steel', 'fairy',
|
||||||
|
];
|
||||||
|
|
||||||
|
let cache;
|
||||||
|
|
||||||
|
export function typeHex(type) {
|
||||||
|
if (!cache) {
|
||||||
|
cache = {};
|
||||||
|
const cs = getComputedStyle(document.documentElement);
|
||||||
|
for (const t of TYPES) {
|
||||||
|
cache[t] = cs.getPropertyValue(`--type-${t}`).trim() || '#8a8f98';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cache[type] || '#8a8f98';
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ export const settings = createStore('pdx.settings', {
|
|||||||
versionGroup: 'scarlet-violet',
|
versionGroup: 'scarlet-violet',
|
||||||
pokedex: null,
|
pokedex: null,
|
||||||
theme: 'system',
|
theme: 'system',
|
||||||
spriteStyle: 'default',
|
spriteStyle: 'official',
|
||||||
showShiny: false,
|
showShiny: false,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -206,7 +206,8 @@
|
|||||||
}
|
}
|
||||||
.progress-header__titles h1 {
|
.progress-header__titles h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.7rem;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
.progress-header__sub,
|
.progress-header__sub,
|
||||||
@ -219,8 +220,8 @@
|
|||||||
margin: 0 6px;
|
margin: 0 6px;
|
||||||
}
|
}
|
||||||
.progress-header__track {
|
.progress-header__track {
|
||||||
margin-top: 10px;
|
margin-top: 12px;
|
||||||
height: 6px;
|
height: 8px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -228,8 +229,9 @@
|
|||||||
.progress-header__bar {
|
.progress-header__bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0;
|
width: 0;
|
||||||
background: var(--good);
|
border-radius: 999px;
|
||||||
transition: width 0.3s ease;
|
background: linear-gradient(90deg, var(--good), color-mix(in srgb, var(--good) 60%, #74c0fc));
|
||||||
|
transition: width 0.4s var(--ease-spring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dexgrid__controls {
|
.dexgrid__controls {
|
||||||
@ -241,27 +243,32 @@
|
|||||||
.dexgrid__search,
|
.dexgrid__search,
|
||||||
.search__input {
|
.search__input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 14px;
|
padding: 12px 16px;
|
||||||
border: 1px solid var(--border);
|
border: 1.5px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: 999px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
.dexgrid__search:focus,
|
||||||
|
.search__input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
|
||||||
gap: var(--gap);
|
gap: var(--gap);
|
||||||
}
|
}
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.grid {
|
.grid {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
.grid {
|
.grid {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.grid__empty {
|
.grid__empty {
|
||||||
@ -276,47 +283,78 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 5px;
|
||||||
padding: 14px 10px 12px;
|
padding: 16px 12px 14px;
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(
|
||||||
|
165deg,
|
||||||
|
color-mix(in srgb, var(--type-main) 26%, var(--surface)) 0%,
|
||||||
|
var(--surface) 60%
|
||||||
|
);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--type-main) 22%, var(--border));
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
contain-intrinsic-size: 210px;
|
contain-intrinsic-size: 232px;
|
||||||
transition: transform 0.12s ease, border-color 0.12s ease;
|
transition: transform 0.14s var(--ease-spring), box-shadow 0.14s ease;
|
||||||
|
}
|
||||||
|
.card > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.card:hover {
|
.card:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-4px);
|
||||||
border-color: var(--accent);
|
box-shadow: var(--shadow-lg);
|
||||||
}
|
}
|
||||||
.card.is-caught {
|
.card__spot {
|
||||||
border-color: color-mix(in srgb, var(--good) 55%, var(--border));
|
|
||||||
}
|
|
||||||
.card.is-caught::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
top: 30px;
|
||||||
border-radius: var(--radius);
|
left: 50%;
|
||||||
background: color-mix(in srgb, var(--good) 8%, transparent);
|
width: 118px;
|
||||||
|
height: 118px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
color-mix(in srgb, var(--type-main) 42%, transparent) 0%,
|
||||||
|
transparent 68%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.card__bignum {
|
||||||
|
position: absolute;
|
||||||
|
top: 74px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 0;
|
||||||
|
font-size: 4.2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: color-mix(in srgb, var(--type-main) 24%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.card .sprite {
|
||||||
|
width: 112px;
|
||||||
|
height: 112px;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.22));
|
||||||
|
}
|
||||||
|
.card[data-sprite="default"] .sprite,
|
||||||
|
.card[data-sprite="game"] .sprite {
|
||||||
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
.card__num {
|
.card__num {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
|
font-weight: 600;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.card .sprite {
|
|
||||||
width: 96px;
|
|
||||||
height: 96px;
|
|
||||||
object-fit: contain;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
}
|
|
||||||
.card__name {
|
.card__name {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.9rem;
|
font-size: 0.95rem;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@ -328,31 +366,34 @@
|
|||||||
}
|
}
|
||||||
.card__caught {
|
.card__caught {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 10px;
|
||||||
right: 8px;
|
right: 10px;
|
||||||
width: 26px;
|
z-index: 2;
|
||||||
height: 26px;
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid var(--border);
|
border: none;
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-size: 0.7rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #333;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
}
|
}
|
||||||
.card__caught[aria-pressed="true"] {
|
.card__caught[aria-pressed="true"] {
|
||||||
background: var(--good);
|
background: var(--good);
|
||||||
border-color: var(--good);
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.card__caught[aria-pressed="true"]::before {
|
.card__caught[aria-pressed="true"]::before {
|
||||||
content: "✓";
|
content: "✓";
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
}
|
||||||
.card__caught[aria-pressed="false"]::before {
|
.card__caught[aria-pressed="false"]::before {
|
||||||
content: "+";
|
content: "+";
|
||||||
}
|
}
|
||||||
|
.card.is-caught {
|
||||||
|
border-color: color-mix(in srgb, var(--good) 45%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Game picker ------------------------------------------- */
|
/* ---------- Game picker ------------------------------------------- */
|
||||||
.gamepicker__subdex {
|
.gamepicker__subdex {
|
||||||
@ -367,15 +408,16 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
.gamepicker__gen {
|
.gamepicker__gen {
|
||||||
font-size: 0.8rem;
|
font-size: 0.78rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
margin: 22px 0 10px;
|
font-weight: 600;
|
||||||
|
margin: 26px 0 12px;
|
||||||
}
|
}
|
||||||
.gamepicker__grid {
|
.gamepicker__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.gamecard {
|
.gamecard {
|
||||||
@ -383,20 +425,23 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 12px 14px;
|
padding: 14px 16px;
|
||||||
border: 1px solid var(--border);
|
border: 1.5px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: transform 0.14s var(--ease-spring), border-color 0.14s ease;
|
||||||
}
|
}
|
||||||
.gamecard:hover {
|
.gamecard:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
.gamecard.is-active {
|
.gamecard.is-active {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: inset 0 0 0 1px var(--accent);
|
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
|
||||||
}
|
}
|
||||||
.gamecard__name {
|
.gamecard__name {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -979,24 +1024,28 @@
|
|||||||
.search__row {
|
.search__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
padding: 8px 12px;
|
padding: 10px 14px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
transition: transform 0.12s var(--ease-spring), border-color 0.12s ease;
|
||||||
}
|
}
|
||||||
.search__row:hover {
|
.search__row:hover {
|
||||||
|
transform: translateX(3px);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
.search__row.is-caught {
|
.search__row.is-caught {
|
||||||
border-color: color-mix(in srgb, var(--good) 55%, var(--border));
|
border-color: color-mix(in srgb, var(--good) 55%, var(--border));
|
||||||
}
|
}
|
||||||
.search__row .sprite {
|
.search__row .sprite {
|
||||||
width: 48px;
|
width: 52px;
|
||||||
height: 48px;
|
height: 52px;
|
||||||
image-rendering: pixelated;
|
flex: none;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
.search__num {
|
.search__num {
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { EvolutionChain } from '../components/EvolutionChain.js';
|
|||||||
import { MovesList } from '../components/MovesList.js';
|
import { MovesList } from '../components/MovesList.js';
|
||||||
import { TypeMatchups } from '../components/TypeMatchups.js';
|
import { TypeMatchups } from '../components/TypeMatchups.js';
|
||||||
import { Locations } from '../components/Locations.js';
|
import { Locations } from '../components/Locations.js';
|
||||||
|
import { typeHex } from '../lib/type-color.js';
|
||||||
|
|
||||||
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
||||||
|
|
||||||
@ -31,13 +32,6 @@ const NO_ENCOUNTER_DATA = new Set([
|
|||||||
'legends-arceus',
|
'legends-arceus',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function typeHex(type) {
|
|
||||||
const v = getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue(`--type-${type}`)
|
|
||||||
.trim();
|
|
||||||
return v || '#8a8f98';
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PokemonDetail(nationalId) {
|
export async function PokemonDetail(nationalId) {
|
||||||
const view = el('section', { class: 'view pdetail' });
|
const view = el('section', { class: 'view pdetail' });
|
||||||
const snap = await loadSnapshot();
|
const snap = await loadSnapshot();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user