Merge branch 'lineage-landing-page-migration'
This commit is contained in:
144
src/routes/lineage/index.tsx
Normal file
144
src/routes/lineage/index.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* Life and Lineage — lineage subdomain landing page (task 08).
|
||||||
|
*
|
||||||
|
* Migrates the legacy `src/routes/marketing/life-and-lineage.tsx` page to the
|
||||||
|
* `lineage.freno.me` root (vercel.json rewrites `lineage.freno.me/*` →
|
||||||
|
* `/lineage/*`), and enriches it with feature highlights + a game-art strip.
|
||||||
|
*
|
||||||
|
* Site-awareness:
|
||||||
|
* - `<PageHead>` reads `useSite()` → the lineage `titleSuffix`
|
||||||
|
* (` | Life and Lineage`) + canonical `https://lineage.freno.me/` are
|
||||||
|
* derived automatically (task 02); we pass only the base title here.
|
||||||
|
* - The Google Play badge links to the **public browser path** `/downloads`
|
||||||
|
* (subdomain-relative), which vercel rewrites to `/lineage/downloads`
|
||||||
|
* (task 11). The App Store link is an absolute external URL, unchanged.
|
||||||
|
* - `SimpleParallax` reuses the existing Cave parallax background — a
|
||||||
|
* fitting "dark fantasy" ambience — and continues to work under the
|
||||||
|
* subdomain because assets are served from the shared `/public` root.
|
||||||
|
*
|
||||||
|
* Auth: this page performs NO auth — Lineage's mobile JWT (`LINEAGE_JWT_SECRET`)
|
||||||
|
* is for the mobile app's API calls, not the web landing page.
|
||||||
|
*/
|
||||||
|
import { A } from "@solidjs/router";
|
||||||
|
import { PageHead } from "~/components/PageHead";
|
||||||
|
import SimpleParallax from "~/components/SimpleParallax";
|
||||||
|
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
|
||||||
|
import { For } from "solid-js";
|
||||||
|
import {
|
||||||
|
APP_STORE_URL,
|
||||||
|
DOWNLOADS_HREF,
|
||||||
|
APP_ICON_SRC,
|
||||||
|
GOOGLE_PLAY_BADGE_SRC,
|
||||||
|
SCREENSHOT_ASSETS,
|
||||||
|
PAGE_META,
|
||||||
|
FEATURES
|
||||||
|
} from "~/routes/lineage/landing-content";
|
||||||
|
|
||||||
|
export default function LineageLandingPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={PAGE_META.title} description={PAGE_META.description} />
|
||||||
|
<SimpleParallax>
|
||||||
|
<div class="flex h-full flex-col items-center justify-center px-4 text-white">
|
||||||
|
{/* Hero */}
|
||||||
|
<img
|
||||||
|
src={APP_ICON_SRC}
|
||||||
|
alt="Life and Lineage App Icon"
|
||||||
|
height={128}
|
||||||
|
width={128}
|
||||||
|
class="object-cover object-center"
|
||||||
|
/>
|
||||||
|
<h1 class="mb-4 mt-4 text-center text-5xl font-bold">
|
||||||
|
Life and Lineage
|
||||||
|
</h1>
|
||||||
|
<p class="mb-8 text-xl">A dark fantasy adventure</p>
|
||||||
|
|
||||||
|
{/* Store badges — App Store (external) + Google Play → /downloads */}
|
||||||
|
<div class="flex flex-wrap items-center justify-center gap-4">
|
||||||
|
<a
|
||||||
|
class="my-auto transition-all duration-200 ease-out active:scale-95"
|
||||||
|
href={APP_STORE_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<DownloadOnAppStoreDark size={50} />
|
||||||
|
</a>
|
||||||
|
<A
|
||||||
|
href={DOWNLOADS_HREF}
|
||||||
|
class="transition-all duration-200 ease-out active:scale-95"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={GOOGLE_PLAY_BADGE_SRC}
|
||||||
|
alt="Get it on Google Play"
|
||||||
|
width={180}
|
||||||
|
height={60}
|
||||||
|
/>
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature highlights */}
|
||||||
|
<section class="mt-16 w-full max-w-4xl">
|
||||||
|
<h2 class="mb-6 text-center text-2xl font-semibold">
|
||||||
|
Why players return
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<For each={FEATURES}>
|
||||||
|
{(feature) => (
|
||||||
|
<div class="bg-base/70 rounded-lg p-4 backdrop-blur-sm">
|
||||||
|
<h3 class="mb-1 text-lg font-bold">{feature.title}</h3>
|
||||||
|
<p class="text-sm text-subtext0">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Game art / screenshots strip */}
|
||||||
|
<section class="mt-16 w-full max-w-4xl">
|
||||||
|
<h2 class="mb-6 text-center text-2xl font-semibold">
|
||||||
|
glimpses of the world
|
||||||
|
</h2>
|
||||||
|
<div class="flex flex-col items-center gap-6">
|
||||||
|
<div class="flex flex-wrap items-center justify-center gap-6">
|
||||||
|
<img
|
||||||
|
src={SCREENSHOT_ASSETS.home}
|
||||||
|
alt="Life and Lineage home"
|
||||||
|
class="h-auto w-full max-w-sm rounded-lg shadow-lg"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={SCREENSHOT_ASSETS.shops}
|
||||||
|
alt="Life and Lineage shops"
|
||||||
|
class="h-auto w-full max-w-sm rounded-lg shadow-lg"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<video
|
||||||
|
src={SCREENSHOT_ASSETS.preview}
|
||||||
|
class="w-full max-w-2xl rounded-lg shadow-lg"
|
||||||
|
controls
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
preload="metadata"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Secondary CTA → per-subdomain downloads page */}
|
||||||
|
<p class="mt-16 text-sm text-subtext0">
|
||||||
|
Prefer a direct download?{" "}
|
||||||
|
<A
|
||||||
|
href={DOWNLOADS_HREF}
|
||||||
|
class="underline transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105"
|
||||||
|
>
|
||||||
|
Visit the downloads page
|
||||||
|
</A>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SimpleParallax>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
src/routes/lineage/landing-content.test.ts
Normal file
133
src/routes/lineage/landing-content.test.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for the Lineage landing page content (task 08).
|
||||||
|
*
|
||||||
|
* Mirrors the `page-head-meta.ts` / `nav-config.ts` pattern: assert against
|
||||||
|
* pure constants exported from `landing-content.ts` (no solid-js / router /
|
||||||
|
* DOM). This covers the task-08 acceptance matrix that's structurally
|
||||||
|
* verifiable without rendering:
|
||||||
|
* - App Store link is present and correct
|
||||||
|
* - Google Play / downloads link targets the subdomain `/downloads` path
|
||||||
|
* (public browser path, NOT the vercel-rewritten `/lineage/downloads`)
|
||||||
|
* - Feature highlights cover: dark fantasy, mobile, remote saves, PvP
|
||||||
|
* - PageHead base title + description (suffix is added by PageHead, task 02)
|
||||||
|
* - Legacy `/marketing/life-and-lineage` redirect target points at the
|
||||||
|
* Lineage subdomain.
|
||||||
|
*
|
||||||
|
* The PageHead title-suffix + canonical derivation for the lineage site is
|
||||||
|
* already covered by `src/components/PageHead.test.ts`; these tests assert
|
||||||
|
* the *inputs* the page passes to PageHead.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import {
|
||||||
|
APP_STORE_URL,
|
||||||
|
DOWNLOADS_HREF,
|
||||||
|
APP_ICON_SRC,
|
||||||
|
GOOGLE_PLAY_BADGE_SRC,
|
||||||
|
SCREENSHOT_ASSETS,
|
||||||
|
PAGE_META,
|
||||||
|
FEATURES,
|
||||||
|
LEGACY_REDIRECT_TARGET,
|
||||||
|
type LineageFeature
|
||||||
|
} from "~/routes/lineage/landing-content";
|
||||||
|
|
||||||
|
describe("Lineage landing — App Store link", () => {
|
||||||
|
it("matches the canonical Life and Lineage App Store URL", () => {
|
||||||
|
expect(APP_STORE_URL).toBe(
|
||||||
|
"https://apps.apple.com/us/app/life-and-lineage/id6737252442"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is an absolute https URL", () => {
|
||||||
|
expect(APP_STORE_URL.startsWith("https://")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Lineage landing — downloads link", () => {
|
||||||
|
it("targets the subdomain-relative public browser path", () => {
|
||||||
|
// NOT `/lineage/downloads` (the internal vercel-rewrite prefix) — vercel
|
||||||
|
// rewrites `lineage.freno.me/downloads` → `/lineage/downloads` while
|
||||||
|
// leaving the browser URL clean, matching the canonical rule from task 02.
|
||||||
|
expect(DOWNLOADS_HREF).toBe("/downloads");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not leak the internal route prefix", () => {
|
||||||
|
expect(DOWNLOADS_HREF).not.toContain("/lineage");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Lineage landing — asset paths", () => {
|
||||||
|
it("points at the shared app icon", () => {
|
||||||
|
expect(APP_ICON_SRC).toBe("/LineageIcon.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("points at the shared Google Play badge", () => {
|
||||||
|
expect(GOOGLE_PLAY_BADGE_SRC).toBe("/google-play-badge.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes screenshot + preview assets", () => {
|
||||||
|
expect(SCREENSHOT_ASSETS.home).toBe("/lineage-home.png");
|
||||||
|
expect(SCREENSHOT_ASSETS.shops).toBe("/lineage-shops.png");
|
||||||
|
expect(SCREENSHOT_ASSETS.preview).toBe("/lineage-preview.mp4");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Lineage landing — PageHead inputs", () => {
|
||||||
|
it("passes the base title (suffix is appended by PageHead)", () => {
|
||||||
|
expect(PAGE_META.title).toBe("Life and Lineage");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not pre-bake the site suffix into the title", () => {
|
||||||
|
// PageHead via resolvePageHeadMeta appends ` | Life and Lineage`.
|
||||||
|
expect(PAGE_META.title).not.toContain("|");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("carries a non-empty description", () => {
|
||||||
|
expect(PAGE_META.description.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("description mentions both store fronts", () => {
|
||||||
|
expect(PAGE_META.description.toLowerCase()).toContain("app store");
|
||||||
|
expect(PAGE_META.description.toLowerCase()).toContain("google play");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Lineage landing — feature highlights", () => {
|
||||||
|
it("exposes exactly the four required pillars", () => {
|
||||||
|
expect(FEATURES.length).toBe(4);
|
||||||
|
const titles = FEATURES.map((f) => f.title);
|
||||||
|
expect(titles).toContain("Dark Fantasy Adventure");
|
||||||
|
expect(titles).toContain("Built for Mobile");
|
||||||
|
expect(titles).toContain("Remote Saves");
|
||||||
|
expect(titles).toContain("PvP Combat");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("every feature has a non-empty title + description", () => {
|
||||||
|
for (const f of FEATURES as LineageFeature[]) {
|
||||||
|
expect(f.title.length).toBeGreaterThan(0);
|
||||||
|
expect(f.description.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("covers the dark-fantasy / mobile / saves / PvP themes", () => {
|
||||||
|
const blob = FEATURES.map(
|
||||||
|
(f) => `${f.title} ${f.description}`
|
||||||
|
)
|
||||||
|
.join(" ")
|
||||||
|
.toLowerCase();
|
||||||
|
expect(blob).toContain("dark fantasy");
|
||||||
|
expect(blob).toContain("mobile");
|
||||||
|
expect(blob).toContain("sav");
|
||||||
|
expect(blob).toContain("pvp");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Lineage landing — legacy redirect target", () => {
|
||||||
|
it("points at the lineage subdomain apex", () => {
|
||||||
|
expect(LEGACY_REDIRECT_TARGET).toBe("https://lineage.freno.me");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is an https absolute URL with no trailing path", () => {
|
||||||
|
expect(LEGACY_REDIRECT_TARGET.startsWith("https://")).toBe(true);
|
||||||
|
expect(LEGACY_REDIRECT_TARGET.endsWith("/")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
102
src/routes/lineage/landing-content.ts
Normal file
102
src/routes/lineage/landing-content.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Pure content + metadata for the Lineage subdomain landing page (task 08).
|
||||||
|
*
|
||||||
|
* Intentionally imports NOTHING from solid-js / @solidjs/router / @solidjs/meta
|
||||||
|
* so the constant set here can be unit-tested in `bun:test` without spinning
|
||||||
|
* up the SolidJS router / MetaProvider — mirroring the pattern established by
|
||||||
|
* `page-head-meta.ts` and `nav-config.ts`.
|
||||||
|
*
|
||||||
|
* The render layer (`./index.tsx`) is a thin JSX consumer of these values:
|
||||||
|
* keeping them externalized means the acceptance matrix (App Store URL,
|
||||||
|
* Google Play → subdomain `/downloads` link, feature highlight copy, PageHead
|
||||||
|
* title/description) is asserted against in `landing-content.test.ts` without
|
||||||
|
* a DOM render.
|
||||||
|
*
|
||||||
|
* Cross-task contracts encoded here:
|
||||||
|
* - `APP_STORE_URL` is the canonical App Store link the marketing page has
|
||||||
|
* always surfaced (kept stable across the migration).
|
||||||
|
* - `DOWNLOADS_HREF` is the **public browser path** on the lineage subdomain
|
||||||
|
* (`/downloads`), NOT the internal vercel-rewritten prefix `/lineage/downloads`.
|
||||||
|
* This matches the canonical-URL rule from task 02 and the nav-config rule
|
||||||
|
* from task 04: vercel.json maps `lineage.freno.me/downloads` →
|
||||||
|
* `/lineage/downloads` server-side while the browser sees `/downloads`.
|
||||||
|
* Task 11 will create the matching `src/routes/lineage/downloads.tsx`.
|
||||||
|
* - `PAGE_META` is consumed verbatim by `<PageHead>`; the per-site title
|
||||||
|
* suffix (` | Life and Lineage`) is appended automatically by
|
||||||
|
* `resolvePageHeadMeta` (task 02), so the `title` here is the BASE title
|
||||||
|
* only — do NOT include the suffix.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Apple App Store link — surfaced unchanged from the legacy marketing page. */
|
||||||
|
export const APP_STORE_URL =
|
||||||
|
"https://apps.apple.com/us/app/life-and-lineage/id6737252442";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public browser path to the per-subdomain downloads page (task 11).
|
||||||
|
* Subdomain-relative: renders `lineage.freno.me/downloads` in the browser.
|
||||||
|
*/
|
||||||
|
export const DOWNLOADS_HREF = "/downloads";
|
||||||
|
|
||||||
|
/** App icon asset, served from the site root (shared with the main site). */
|
||||||
|
export const APP_ICON_SRC = "/LineageIcon.png";
|
||||||
|
|
||||||
|
/** Google Play badge asset (shared with the main site's downloads page). */
|
||||||
|
export const GOOGLE_PLAY_BADGE_SRC = "/google-play-badge.png";
|
||||||
|
|
||||||
|
/** Screenshot / game-art assets used in the enhanced marketing content. */
|
||||||
|
export const SCREENSHOT_ASSETS = {
|
||||||
|
home: "/lineage-home.png",
|
||||||
|
shops: "/lineage-shops.png",
|
||||||
|
preview: "/lineage-preview.mp4"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Base page title (site suffix appended by PageHead). */
|
||||||
|
export const PAGE_META = {
|
||||||
|
title: "Life and Lineage",
|
||||||
|
description:
|
||||||
|
"A dark fantasy adventure mobile game. Download Life and Lineage on the App Store and Google Play."
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature highlights surfaced in the enhanced marketing section.
|
||||||
|
*
|
||||||
|
* Each entry is `{ title, description }` so the renderer can present them
|
||||||
|
* in a uniform grid; the test asserts the full set is present so the
|
||||||
|
* acceptance criteria ("dark fantasy adventure, mobile game, remote saves,
|
||||||
|
* PvP") is verified structurally.
|
||||||
|
*/
|
||||||
|
export interface LineageFeature {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FEATURES: readonly LineageFeature[] = [
|
||||||
|
{
|
||||||
|
title: "Dark Fantasy Adventure",
|
||||||
|
description:
|
||||||
|
"Carve your legend through a grim, atmospheric world steeped in dark fantasy."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Built for Mobile",
|
||||||
|
description:
|
||||||
|
"Jump in anywhere — designed ground-up for quick sessions on iOS and Android."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Remote Saves",
|
||||||
|
description:
|
||||||
|
"Your lineage follows you across devices with cloud-backed character saves."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "PvP Combat",
|
||||||
|
description:
|
||||||
|
"Test your build against other lineages in head-to-head PvP showdowns."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical absolute URL the legacy `/marketing/life-and-lineage` route
|
||||||
|
* 308-redirects to (see `src/routes/marketing/life-and-lineage.ts`). Kept here
|
||||||
|
* so tests can assert the redirect target without importing the route module
|
||||||
|
* (which would pull in the server runtime).
|
||||||
|
*/
|
||||||
|
export const LEGACY_REDIRECT_TARGET = "https://lineage.freno.me";
|
||||||
@@ -1,51 +1,27 @@
|
|||||||
import { A } from "@solidjs/router";
|
/**
|
||||||
import { PageHead } from "~/components/PageHead";
|
* Legacy Life and Lineage marketing route — now a 308 permanent redirect to
|
||||||
import SimpleParallax from "~/components/SimpleParallax";
|
* the Lineage subdomain (task 08).
|
||||||
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
|
*
|
||||||
|
* The marketing content has been migrated to `src/routes/lineage/index.tsx`
|
||||||
|
* served at `lineage.freno.me` (vercel.json host rewrites map the subdomain to
|
||||||
|
* the `/lineage/*` internal prefix). Keeping this route as a permanent (308)
|
||||||
|
* server-side redirect — rather than a client `<Navigate>` — preserves SEO
|
||||||
|
* equity and gives installed / linked URLs a stable resolution path.
|
||||||
|
*
|
||||||
|
* Implemented as a SolidStart API route (`GET` handler returning a Response)
|
||||||
|
* so the redirect happens before any rendering; the route no longer ships a
|
||||||
|
* page component. The redirect target is centralized in
|
||||||
|
* `~/routes/lineage/landing-content.ts` (`LEGACY_REDIRECT_TARGET`) so the
|
||||||
|
* unit test can assert the destination without importing this server module.
|
||||||
|
*/
|
||||||
|
import { LEGACY_REDIRECT_TARGET } from "~/routes/lineage/landing-content";
|
||||||
|
|
||||||
export default function LifeAndLineageMarketing() {
|
export function GET() {
|
||||||
return (
|
return new Response(null, {
|
||||||
<>
|
status: 308,
|
||||||
<PageHead
|
headers: {
|
||||||
title="Life and Lineage"
|
Location: LEGACY_REDIRECT_TARGET,
|
||||||
description="A dark fantasy adventure mobile game. Download Life and Lineage on the App Store and Google Play."
|
"Cache-Control": "public, max-age=0, must-revalidate"
|
||||||
/>
|
}
|
||||||
<SimpleParallax>
|
});
|
||||||
<div class="flex h-full flex-col items-center justify-center text-white">
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
src="/LineageIcon.png"
|
|
||||||
alt="Lineage App Icon"
|
|
||||||
height={128}
|
|
||||||
width={128}
|
|
||||||
class="object-cover object-center"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h1 class="mb-4 text-center text-5xl font-bold">Life and Lineage</h1>
|
|
||||||
<p class="mb-8 text-xl">A dark fantasy adventure</p>
|
|
||||||
<div class="flex space-x-4">
|
|
||||||
<a
|
|
||||||
class="my-auto transition-all duration-200 ease-out active:scale-95"
|
|
||||||
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<DownloadOnAppStoreDark size={50} />
|
|
||||||
</a>
|
|
||||||
<A
|
|
||||||
href="/downloads"
|
|
||||||
class="transition-all duration-200 ease-out active:scale-95"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/google-play-badge.png"
|
|
||||||
alt="google-play"
|
|
||||||
width={180}
|
|
||||||
height={60}
|
|
||||||
/>
|
|
||||||
</A>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SimpleParallax>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user