Swap video bg for aerial Antananarivo image, drive plane with the mouse
User-supplied aerial illustration of Antananarivo (Lake Anosy + Rova hill + surrounding city, 1920px wide JPG, 241 KB) replaces the parachute-drop video as the static intro backdrop. The video files are deleted from /videos. The plane no longer orbits a scroll timeline. Now: - Page is a single viewport, no scroll, no act labels, no scroll hint. - Mouse X (0..1) drives plane.position.x from -16 (offscreen left) to +16 (offscreen right), with plane.position.y descending from +5 to -2 — so the plane enters from the upper-left and exits lower-right. - Pitch/roll/yaw lerp toward small targets that depend on mouse X, so the plane banks naturally as it crosses. - Background image gets a softer mouse parallax (-16/-10px) via the existing --mx/--my CSS vars, now updated from intro-scene.js. - Three.js cloud spheres are gone; the photo is the entire backdrop. - ScrollTrigger + the GSAP timeline are removed; the page no longer needs gsap at all (the script tag stayed for now in case it comes back, but the dependency could be dropped on a future pass). - CTA button is back to plain visible/centered, no reveal animation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d99e2a5fc1
commit
eba88207c4
BIN
assets/antananarivo-bg.jpg
Normal file
BIN
assets/antananarivo-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
114
css/parallax.css
114
css/parallax.css
@ -1,7 +1,8 @@
|
||||
/* =========================================================================
|
||||
PARALLAX INTRO — MVA Global Fret
|
||||
Page scrollable (4 viewports) avec scène Three.js fixée. La timeline
|
||||
pilote la caméra et l'avion 3D. À 100% du scroll, le bouton CTA apparaît.
|
||||
Page unique. Photo aérienne d'Antananarivo en fond + avion 3D piloté
|
||||
par la souris (entre par le haut-gauche, sort par la droite). Bouton
|
||||
CTA doré centré, toujours visible.
|
||||
========================================================================= */
|
||||
|
||||
:root {
|
||||
@ -22,12 +23,10 @@ html, body {
|
||||
background: var(--navy-deep);
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body.parallax-body {
|
||||
.parallax-body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: var(--white);
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── HEADER ─────────────────────────────────────────────────────────────── */
|
||||
@ -86,16 +85,12 @@ body.parallax-body {
|
||||
}
|
||||
.lang-switcher button:hover:not(.active) { color: var(--white); }
|
||||
|
||||
/* ── SCÈNE FIXE (vidéo + voile + canvas Three.js) ────────────────────────
|
||||
.stage-fixed reste plein-écran pendant que la page se déroule derrière.
|
||||
*/
|
||||
.stage-fixed {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
/* ── STAGE ──────────────────────────────────────────────────────────────── */
|
||||
.stage {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.layer {
|
||||
@ -107,90 +102,20 @@ body.parallax-body {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.layer-video {
|
||||
/* Photo de fond — léger zoom + parallaxe souris discrète */
|
||||
.layer-bg {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
transform: translate(calc(var(--mx) * -22px), calc(var(--my) * -22px)) scale(1.06);
|
||||
transform: translate(calc(var(--mx) * -16px), calc(var(--my) * -10px)) scale(1.05);
|
||||
}
|
||||
|
||||
.layer-tint {
|
||||
background:
|
||||
radial-gradient(ellipse at center, transparent 0%, rgba(5, 5, 24, 0.55) 100%),
|
||||
linear-gradient(180deg, rgba(5, 5, 24, 0.25) 0%, rgba(5, 5, 24, 0.5) 100%);
|
||||
radial-gradient(ellipse at center, transparent 0%, rgba(5, 5, 24, 0.32) 100%),
|
||||
linear-gradient(180deg, transparent 55%, rgba(5, 5, 24, 0.40) 100%);
|
||||
}
|
||||
|
||||
.layer-three {
|
||||
display: block;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* ── SCROLL STAGE — sentinelles invisibles qui donnent la hauteur ──────── */
|
||||
.scroll-stage {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.scroll-stage .act {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ── ÉTIQUETTES D'ACTE ──────────────────────────────────────────────────── */
|
||||
.act-label {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 18%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4px;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.05rem;
|
||||
color: var(--white);
|
||||
text-shadow: 0 4px 18px rgba(0, 0, 0, 0.7);
|
||||
opacity: 0;
|
||||
}
|
||||
.act-label span {
|
||||
display: inline-block;
|
||||
padding: 14px 28px;
|
||||
background: rgba(5, 5, 24, 0.45);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 8px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* ── INDICATEUR SCROLL ─────────────────────────────────────────────────── */
|
||||
.scroll-hint {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 36px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 40;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
|
||||
animation: scrollBob 1.8s ease-in-out infinite;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
.scroll-hint.hidden { opacity: 0; pointer-events: none; }
|
||||
.scroll-hint i { font-size: 1rem; }
|
||||
|
||||
@keyframes scrollBob {
|
||||
0%, 100% { transform: translate(-50%, 0); }
|
||||
50% { transform: translate(-50%, 8px); }
|
||||
}
|
||||
.layer-three { display: block; z-index: 2; }
|
||||
|
||||
/* ── BOUTON CTA centré ──────────────────────────────────────────────────── */
|
||||
.cta-btn {
|
||||
@ -198,7 +123,7 @@ body.parallax-body {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 50;
|
||||
z-index: 10;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
@ -219,11 +144,7 @@ body.parallax-body {
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.45);
|
||||
transition: box-shadow 0.32s cubic-bezier(0.2, 0.8, 0.2, 1),
|
||||
transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
/* Caché au départ ; révélé via GSAP en fin de scroll */
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cta-btn.revealed { pointer-events: auto; }
|
||||
|
||||
.cta-btn:hover {
|
||||
transform: translate(-50%, -50%) scale(1.04);
|
||||
@ -275,13 +196,8 @@ body.parallax-body {
|
||||
padding: 16px 36px;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
.act-label { font-size: 0.85rem; bottom: 22%; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cta-btn::after { animation: none; }
|
||||
.scroll-hint { animation: none; display: none; }
|
||||
.scroll-stage { display: none; }
|
||||
.cta-btn { opacity: 1; pointer-events: auto; }
|
||||
html { scroll-behavior: auto; }
|
||||
}
|
||||
|
||||
45
index.html
45
index.html
@ -18,7 +18,7 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/parallax.css">
|
||||
|
||||
<!-- Three.js (ESM via importmap) + addons (GLTFLoader, etc.) -->
|
||||
<!-- Three.js (ESM via importmap) + addons (GLTFLoader) -->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
@ -27,9 +27,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- GSAP + ScrollTrigger (UMD) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
|
||||
</head>
|
||||
<body class="parallax-body">
|
||||
|
||||
@ -45,43 +42,24 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Couches fixes : vidéo + voile + canvas Three.js (le plan 3D vole dessus) -->
|
||||
<div class="stage-fixed">
|
||||
<video class="layer layer-video" id="introVideo"
|
||||
autoplay loop muted playsinline preload="auto"
|
||||
poster="videos/parachute-poster.jpg">
|
||||
<source src="videos/parachute-drop.mp4" type="video/mp4">
|
||||
</video>
|
||||
<main class="stage">
|
||||
|
||||
<!-- Photo aérienne d'Antananarivo en fond -->
|
||||
<img class="layer layer-bg" src="assets/antananarivo-bg.jpg" alt="" aria-hidden="true">
|
||||
|
||||
<!-- Voile très léger pour la lisibilité du bouton -->
|
||||
<div class="layer layer-tint"></div>
|
||||
|
||||
<!-- Canvas Three.js : avion piloté par la souris -->
|
||||
<canvas id="three-canvas" class="layer layer-three"></canvas>
|
||||
|
||||
<!-- Étiquettes des actes — apparaissent / disparaissent selon le scroll -->
|
||||
<div class="act-label" id="label-paris"><span>Paris · CDG</span></div>
|
||||
<div class="act-label" id="label-cruise"><span>Vol cargo<br>10 000 km</span></div>
|
||||
<div class="act-label" id="label-tana"><span>Antananarivo</span></div>
|
||||
|
||||
<!-- Indicateur scroll (visible jusqu'à ce qu'on scrolle) -->
|
||||
<div class="scroll-hint" id="scrollHint">
|
||||
<span data-i18n="intro.scrollHint">Faites défiler</span>
|
||||
<i class="fa-solid fa-chevron-down"></i>
|
||||
</div>
|
||||
|
||||
<!-- CTA — caché jusqu'à la fin du scroll -->
|
||||
<a href="accueil.html" class="cta-btn" id="ctaBtn">
|
||||
<!-- Bouton CTA centré -->
|
||||
<a href="accueil.html" class="cta-btn">
|
||||
<span class="cta-btn-shine"></span>
|
||||
<span data-i18n="intro.ctaBtn">Accéder au site</span>
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Sentinelles invisibles : leur scroll-position pilote la timeline -->
|
||||
<main class="scroll-stage" aria-hidden="true">
|
||||
<section class="act"></section>
|
||||
<section class="act"></section>
|
||||
<section class="act"></section>
|
||||
<section class="act"></section>
|
||||
</main>
|
||||
|
||||
<script src="js/translations.js"></script>
|
||||
@ -109,11 +87,6 @@
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
/* Scroll-hint : disparaît dès qu'on commence à scroller */
|
||||
window.addEventListener('scroll', () => {
|
||||
document.getElementById('scrollHint')?.classList.add('hidden');
|
||||
}, { once: true, passive: true });
|
||||
</script>
|
||||
|
||||
<script type="module" src="js/intro-scene.js"></script>
|
||||
|
||||
@ -1,20 +1,16 @@
|
||||
/* =========================================================================
|
||||
INTRO SCENE — MVA Global Fret
|
||||
Three.js scene driven by scroll. The cargo airliner is a GLTF model
|
||||
loaded at runtime; GSAP + ScrollTrigger animate the camera position
|
||||
and exposition labels as the page scrolls. The CTA fades in at the
|
||||
end of the timeline.
|
||||
Photo aérienne d'Antananarivo en fond, avion 3D piloté par la souris :
|
||||
il entre par le haut-gauche quand la souris est à gauche, traverse
|
||||
l'écran à mesure qu'elle bouge, et sort par la droite. Pas de scroll.
|
||||
|
||||
3D model credit (CC-BY): "Airplane" by Poly by Google
|
||||
3D model credit (CC-BY 3.0) : « Airplane » by Poly by Google
|
||||
https://poly.pizza/m/a3XrQkLNna9
|
||||
========================================================================= */
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
|
||||
const { gsap, ScrollTrigger } = window;
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
/* ── Renderer & camera ─────────────────────────────────────────────────── */
|
||||
const canvas = document.getElementById('three-canvas');
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
@ -27,8 +23,8 @@ renderer.toneMappingExposure = 1.05;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(38, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.set(0, 1.4, 18);
|
||||
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.set(0, 0, 22);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
/* ── Lighting (golden-hour fill) ───────────────────────────────────────── */
|
||||
@ -51,9 +47,6 @@ loader.load(
|
||||
'assets/airplane.glb',
|
||||
(gltf) => {
|
||||
const model = gltf.scene;
|
||||
/* Compute bbox at native scale, then offset model so its center sits
|
||||
at the wrapper's origin. The wrapper scales everything together,
|
||||
so the center offset stays valid after scaling. */
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
@ -63,95 +56,71 @@ loader.load(
|
||||
wrapper.add(model);
|
||||
const targetSize = 8.5;
|
||||
wrapper.scale.setScalar(targetSize / Math.max(size.x, size.y, size.z));
|
||||
/* The Poly by Google plane sits with nose along +X; turn it so the
|
||||
nose faces the camera at the start of the timeline. */
|
||||
wrapper.rotation.y = Math.PI / 2;
|
||||
/* Pivote le modèle pour que le nez pointe vers la droite (+X) */
|
||||
wrapper.rotation.y = -Math.PI / 2;
|
||||
planeHolder.add(wrapper);
|
||||
},
|
||||
undefined,
|
||||
(err) => console.error('Failed to load airplane.glb:', err)
|
||||
);
|
||||
|
||||
/* ── Cloud sprites ─────────────────────────────────────────────────────── */
|
||||
const clouds = new THREE.Group();
|
||||
const cloudMat = new THREE.MeshStandardMaterial({
|
||||
color: 0xffffff, roughness: 1.0, metalness: 0, transparent: true, opacity: 0.85
|
||||
});
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const cl = new THREE.Mesh(new THREE.SphereGeometry(1, 12, 10), cloudMat);
|
||||
const r = 18 + Math.random() * 14;
|
||||
const a = Math.random() * Math.PI * 2;
|
||||
cl.position.set(Math.cos(a) * r, (Math.random() - 0.4) * 9, Math.sin(a) * r - 8);
|
||||
cl.scale.set(1.6 + Math.random() * 1.4, 0.9 + Math.random() * 0.6, 1.6 + Math.random() * 1.4);
|
||||
clouds.add(cl);
|
||||
/* ── Souris ─────────────────────────────────────────────────────────────
|
||||
- mouseX, mouseY : 0..1 normalisés
|
||||
- Sur écran sans souris (touch/mobile), valeur lente d'auto-scroll
|
||||
*/
|
||||
const mouse = { tx: 0.0, ty: 0.5, x: 0.0, y: 0.5 };
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
mouse.tx = Math.max(0, Math.min(1, e.clientX / window.innerWidth));
|
||||
mouse.ty = Math.max(0, Math.min(1, e.clientY / window.innerHeight));
|
||||
}, { passive: true });
|
||||
|
||||
/* Sur mobile : utilise l'orientation gamma (gauche-droite) si dispo */
|
||||
window.addEventListener('deviceorientation', (e) => {
|
||||
if (e.gamma == null) return;
|
||||
mouse.tx = Math.max(0, Math.min(1, (e.gamma + 30) / 60));
|
||||
if (e.beta != null) {
|
||||
mouse.ty = Math.max(0, Math.min(1, (e.beta - 20) / 60));
|
||||
}
|
||||
scene.add(clouds);
|
||||
}, { passive: true });
|
||||
|
||||
/* ── Scroll-driven progress ────────────────────────────────────────────── */
|
||||
const state = { progress: 0 };
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: '.scroll-stage',
|
||||
start: 'top top',
|
||||
end: 'bottom bottom',
|
||||
scrub: 0.6,
|
||||
onUpdate: self => { state.progress = self.progress; }
|
||||
});
|
||||
|
||||
/* Cross-fade des étiquettes d'acte */
|
||||
const labelTl = gsap.timeline({
|
||||
scrollTrigger: { trigger: '.scroll-stage', start: 'top top', end: 'bottom bottom', scrub: 0.4 }
|
||||
});
|
||||
labelTl
|
||||
.fromTo('#label-paris', { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.05 }, 0.02)
|
||||
.to( '#label-paris', { opacity: 0, y: -20 }, 0.20)
|
||||
.fromTo('#label-cruise', { opacity: 0, y: 20 }, { opacity: 1, y: 0 }, 0.40)
|
||||
.to( '#label-cruise', { opacity: 0, y: -20 }, 0.60)
|
||||
.fromTo('#label-tana', { opacity: 0, y: 20 }, { opacity: 1, y: 0 }, 0.72)
|
||||
.to( '#label-tana', { opacity: 0, y: -20 }, 0.92);
|
||||
|
||||
/* CTA reveal — last ~10% of scroll */
|
||||
gsap.fromTo('#ctaBtn',
|
||||
{ opacity: 0, scale: 0.85 },
|
||||
{
|
||||
opacity: 1, scale: 1,
|
||||
scrollTrigger: {
|
||||
trigger: '.scroll-stage',
|
||||
start: 'bottom-=400 bottom',
|
||||
end: 'bottom bottom',
|
||||
scrub: 0.4,
|
||||
onLeave: () => document.getElementById('ctaBtn').classList.add('revealed'),
|
||||
onEnterBack: () => document.getElementById('ctaBtn').classList.remove('revealed'),
|
||||
}
|
||||
}
|
||||
);
|
||||
/* Variable CSS pour la parallaxe douce de la photo de fond */
|
||||
const root = document.documentElement;
|
||||
|
||||
/* ── Render loop ───────────────────────────────────────────────────────── */
|
||||
const clock = new THREE.Clock();
|
||||
|
||||
function tick() {
|
||||
const t = clock.getElapsedTime();
|
||||
const p = state.progress;
|
||||
|
||||
/* Camera arc around the plane */
|
||||
const camAngle = -0.6 + p * 1.7;
|
||||
const camRadius = 18 - p * 6 + Math.sin(p * Math.PI) * -3;
|
||||
const camHeight = 1.4 + Math.sin(p * Math.PI) * 1.8 - p * 0.6;
|
||||
/* Lerp doux vers la cible souris */
|
||||
mouse.x += (mouse.tx - mouse.x) * 0.06;
|
||||
mouse.y += (mouse.ty - mouse.y) * 0.06;
|
||||
|
||||
camera.position.set(
|
||||
Math.sin(camAngle) * camRadius,
|
||||
camHeight,
|
||||
Math.cos(camAngle) * camRadius
|
||||
);
|
||||
camera.lookAt(0, 0, 0);
|
||||
/* Mappe sur les variables CSS (parallaxe légère du fond) */
|
||||
root.style.setProperty('--mx', ((mouse.x - 0.5) * 2).toFixed(4));
|
||||
root.style.setProperty('--my', ((mouse.y - 0.5) * 2).toFixed(4));
|
||||
|
||||
/* Plane bob + roll */
|
||||
planeHolder.position.y = Math.sin(t * 0.9) * 0.15;
|
||||
planeHolder.rotation.z = Math.sin(t * 0.5) * 0.04 + (p - 0.5) * 0.18;
|
||||
planeHolder.rotation.y = Math.sin(t * 0.4) * 0.025;
|
||||
/* Position de l'avion :
|
||||
- mouse.x = 0 → arrive haut-gauche (hors champ)
|
||||
- mouse.x = 0.5 → centré
|
||||
- mouse.x = 1 → sortie en bas-droite (hors champ)
|
||||
*/
|
||||
const px = -16 + mouse.x * 32; // -16 à +16
|
||||
const py = 5 - mouse.x * 7; // descend de +5 à -2 selon X
|
||||
/* Léger bobbing autonome pour qu'il reste vivant même sans bouger la souris */
|
||||
const bob = Math.sin(t * 0.9) * 0.12;
|
||||
|
||||
/* Clouds slow rotation */
|
||||
clouds.rotation.y = t * 0.04 - p * 0.6;
|
||||
clouds.position.y = Math.sin(t * 0.3) * 0.4;
|
||||
planeHolder.position.set(px, py + bob, 0);
|
||||
|
||||
/* Roulis : penche dans le sens du mouvement (mais tourne le nez vers la droite) */
|
||||
const targetRoll = -0.18 - (mouse.x - 0.5) * 0.25; // léger pivot quand on traverse
|
||||
const targetPitch = -0.18 - mouse.x * 0.10; // légèrement piqué
|
||||
const targetYaw = (mouse.x - 0.5) * 0.10; // soupçon de yaw
|
||||
|
||||
planeHolder.rotation.z += (targetRoll - planeHolder.rotation.z) * 0.08;
|
||||
planeHolder.rotation.x += (targetPitch - planeHolder.rotation.x) * 0.08;
|
||||
planeHolder.rotation.y += (targetYaw - planeHolder.rotation.y) * 0.08;
|
||||
|
||||
renderer.render(scene, camera);
|
||||
requestAnimationFrame(tick);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB |
Loading…
Reference in New Issue
Block a user