[Ai] feat: 花魁短剧 Next.js 站点初始提交
- 原生 Next.js App Router + 严格 TypeScript 实现花魁短剧 PC/H5 页面,按 src/features/{catalog,content,playback} 组织,保留完整 SEO(metadata、JSON-LD、canonical、robots、llms、sitemap、IndexNow)。
- 主域名通过 SITE_URL=https://huakuidj.com 强制覆盖后端 foreverUrlAddr(config/*.env、src/server/config.ts、src/server/seo/shared.ts、src/server/services/shared.ts),支持邮箱改为 support@huakuidj.com。
- 移除登录与账户体系:删除 /account 全部页面、登录/注册/充值弹窗、收藏点赞、评论、金币解锁及对应 BFF 接口;旧账户地址 GET/HEAD 统一 301 到首页(src/lib/routes.ts REMOVED_ACCOUNT_PATHS),保留游客会话、反馈、图片上传、观看上报与搜索联想。
- 播放页:评论位改为同类推荐 + 热播榜单,锁定集改为 App 观看遮罩与底部弹层,新增简介标签、分集图例与独立视觉皮肤(src/features/playback、src/styles/pages/playback.css)。
- 卡片:短剧/视频卡状态与时长移至信息行胶囊标签,封面角标移至左上角(src/components/catalog/Cards.tsx、src/styles/foundations/base.css)。
- 同步更新单元测试、浏览器冒烟脚本、路由清单与 README。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,598 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { Data } from "@/server/services/model";
|
||||
import { useDialog, useScrollLock } from "@/components/ui/Dialog";
|
||||
import { Icon } from "@/components/ui/Icon";
|
||||
import { EncryptedImage } from "@/components/media/EncryptedImage";
|
||||
import { appDownloadURL } from "@/lib/app-download";
|
||||
|
||||
export function AgeGate({ onAccepted }: { onAccepted: () => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
let accepted = false;
|
||||
try {
|
||||
accepted = localStorage.getItem("hg_age_ok") === "1";
|
||||
} catch {}
|
||||
if (accepted) {
|
||||
document.documentElement.classList.add("hk-age-gate-ok");
|
||||
onAccepted();
|
||||
} else setOpen(true);
|
||||
}, [onAccepted]);
|
||||
const ref = useDialog(() => {}, open);
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle("hk-age-gate-locked", open);
|
||||
return () => document.body.classList.remove("hk-age-gate-locked");
|
||||
}, [open]);
|
||||
if (!open) return null;
|
||||
const accept = () => {
|
||||
try {
|
||||
localStorage.setItem("hg_age_ok", "1");
|
||||
} catch {}
|
||||
document.documentElement.classList.add("hk-age-gate-ok");
|
||||
setOpen(false);
|
||||
document.dispatchEvent(new CustomEvent("eighteen:closed"));
|
||||
onAccepted();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="hk-age-gate hk-is-open"
|
||||
data-eighteen=""
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="eighteen-title"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="hk-age-gate__mask" aria-hidden="true" />
|
||||
<div className="hk-age-gate__dialog">
|
||||
<div className="hk-age-gate__badge" aria-hidden="true">
|
||||
18+
|
||||
</div>
|
||||
<h2 className="hk-age-gate__title" id="eighteen-title">
|
||||
年龄确认
|
||||
</h2>
|
||||
<p className="hk-age-gate__desc">
|
||||
本网站部分内容仅适合成年人观看。点击继续即表示您确认您已年满 18
|
||||
岁,或在您访问本网站时所在的司法管辖区已是成年人。
|
||||
</p>
|
||||
<button
|
||||
className="hk-button hk-button--primary hk-age-gate__enter"
|
||||
type="button"
|
||||
data-eighteen-enter=""
|
||||
onClick={accept}
|
||||
>
|
||||
我已年满 18 岁,进入
|
||||
</button>
|
||||
<button
|
||||
className="hk-age-gate__exit"
|
||||
type="button"
|
||||
data-eighteen-exit=""
|
||||
onClick={() => location.assign("https://www.google.com/")}
|
||||
>
|
||||
我未满 18 岁,退出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function DownloadDialog({
|
||||
data,
|
||||
onClose,
|
||||
}: {
|
||||
data: Data;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [mobile, setMobile] = useState(
|
||||
() =>
|
||||
typeof window !== "undefined" && matchMedia("(max-width:768px)").matches,
|
||||
);
|
||||
const [platform, setPlatform] = useState<"ios" | "android" | "other">(
|
||||
"other",
|
||||
),
|
||||
[tab, setTab] = useState<"store" | "profile">("store");
|
||||
const ref = useDialog(onClose),
|
||||
guide = data.installGuide || {},
|
||||
fallback =
|
||||
guide.fallbackURL ||
|
||||
appDownloadURL(data.appConfig) ||
|
||||
appDownloadURL(data.config) ||
|
||||
"/app/download/";
|
||||
useEffect(() => {
|
||||
const media = matchMedia("(max-width:768px)"),
|
||||
resize = () => {
|
||||
if (mobile && !media.matches) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
setMobile(media.matches);
|
||||
};
|
||||
media.addEventListener("change", resize);
|
||||
setPlatform(
|
||||
/android/i.test(navigator.userAgent)
|
||||
? "android"
|
||||
: /iPhone|iPad|iPod/i.test(navigator.userAgent) ||
|
||||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)
|
||||
? "ios"
|
||||
: "other",
|
||||
);
|
||||
return () => media.removeEventListener("change", resize);
|
||||
}, [mobile, onClose]);
|
||||
if (!mobile)
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="hk-app-download-dialog hk-is-open"
|
||||
data-download-pop=""
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="download-pop-title"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div
|
||||
className="hk-app-download-dialog__mask"
|
||||
data-download-close=""
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="hk-app-download-dialog__panel">
|
||||
<button
|
||||
className="hk-app-download-dialog__close"
|
||||
type="button"
|
||||
data-download-close=""
|
||||
onClick={onClose}
|
||||
aria-label="关闭"
|
||||
>
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
<div className="hk-app-download-dialog__copy">
|
||||
<h2 className="hk-app-download-dialog__title" id="download-pop-title">
|
||||
下载花魁短剧 App
|
||||
</h2>
|
||||
<p className="hk-app-download-dialog__desc">
|
||||
点击后前往下载页,手机打开会自动识别设备并推荐安装方式
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
className="hk-app-download-dialog__download"
|
||||
href={fallback}
|
||||
target="_blank"
|
||||
rel="sponsored nofollow noopener"
|
||||
>
|
||||
<Icon name="download" className="hk-app-download-dialog__download-icon" />
|
||||
点击下载
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const panel = platform === "android" ? "android" : tab;
|
||||
const description =
|
||||
platform === "ios"
|
||||
? guide.hasIOSStore || guide.hasIOSProfile
|
||||
? "已识别为 iOS 设备,推荐商店版,也可选轻量版"
|
||||
: "已识别为 iOS 设备,可前往官方下载页查看安装方式"
|
||||
: platform === "android"
|
||||
? guide.hasAndroidAPK
|
||||
? "已识别为 Android 设备,下载 APK 直接安装"
|
||||
: "已识别为 Android 设备,可前往官方下载页查看安装方式"
|
||||
: "请选择适合当前设备的安装方式";
|
||||
const notice = (text: string) => (
|
||||
<p className="hk-installation-guide__notice">
|
||||
<Icon name="install/notice" size={14} />
|
||||
<span>{text}</span>
|
||||
</p>
|
||||
);
|
||||
const action = (
|
||||
kind: "store" | "profile" | "android",
|
||||
href: string,
|
||||
label: string,
|
||||
icon: string,
|
||||
recommended = false,
|
||||
) => (
|
||||
<a
|
||||
className="hk-installation-guide__action"
|
||||
data-install-action={kind}
|
||||
data-navigation-key={"install_" + kind}
|
||||
data-navigation-name={label}
|
||||
href={href || fallback}
|
||||
target="_blank"
|
||||
rel="sponsored nofollow noopener"
|
||||
>
|
||||
<Icon name={icon} size={20} />
|
||||
<span>{label}</span>
|
||||
{recommended && <span className="hk-installation-guide__action-badge">推荐</span>}
|
||||
</a>
|
||||
);
|
||||
const steps = (items: [string, string][]) => (
|
||||
<ol className="hk-installation-guide__steps">
|
||||
{items.map(([title, desc], index) => (
|
||||
<li className="hk-installation-guide__step" key={title}>
|
||||
<span className="hk-installation-guide__badge">{index + 1}</span>
|
||||
<div className="hk-installation-guide__step-copy">
|
||||
<h3>{title}</h3>
|
||||
<p>{desc}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="hk-installation-guide hk-is-open"
|
||||
data-install-guide=""
|
||||
data-platform={platform}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="install-guide-title"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div
|
||||
className="hk-installation-guide__mask"
|
||||
data-install-close=""
|
||||
data-download-close=""
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<section className="hk-installation-guide__sheet">
|
||||
<div className="hk-installation-guide__handle" aria-hidden="true">
|
||||
<span />
|
||||
</div>
|
||||
<header className="hk-installation-guide__head">
|
||||
<div className="hk-installation-guide__heading">
|
||||
<h2 id="install-guide-title">下载花魁短剧 App</h2>
|
||||
<p id="install-guide-description" data-install-description="">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="hk-installation-guide__close"
|
||||
type="button"
|
||||
data-install-close=""
|
||||
onClick={onClose}
|
||||
aria-label="关闭安装引导"
|
||||
>
|
||||
<Icon name="close" size={18} />
|
||||
</button>
|
||||
</header>
|
||||
{platform !== "android" && (
|
||||
<div
|
||||
className="hk-installation-guide__tabs"
|
||||
role="tablist"
|
||||
aria-label="iOS 安装方式"
|
||||
data-install-tabs=""
|
||||
>
|
||||
{(["store", "profile"] as const).map((kind) => (
|
||||
<button
|
||||
key={kind}
|
||||
id={"install-guide-" + kind + "-tab"}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === kind}
|
||||
aria-controls={"install-guide-" + kind}
|
||||
tabIndex={tab === kind ? 0 : -1}
|
||||
data-install-tab={kind}
|
||||
onClick={() => setTab(kind)}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
["ArrowLeft", "ArrowRight", "Home", "End"].includes(
|
||||
event.key,
|
||||
)
|
||||
) {
|
||||
event.preventDefault();
|
||||
const next =
|
||||
event.key === "Home"
|
||||
? "store"
|
||||
: event.key === "End"
|
||||
? "profile"
|
||||
: tab === "store"
|
||||
? "profile"
|
||||
: "store";
|
||||
setTab(next);
|
||||
document
|
||||
.getElementById("install-guide-" + next + "-tab")
|
||||
?.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{kind === "store" ? "商店版" : "轻量版"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="hk-installation-guide__content"
|
||||
id="install-guide-store"
|
||||
role="tabpanel"
|
||||
aria-labelledby="install-guide-store-tab"
|
||||
data-install-panel="store"
|
||||
hidden={panel !== "store"}
|
||||
>
|
||||
<div className="hk-installation-guide__store-card">
|
||||
<span className="hk-installation-guide__store-icon">
|
||||
<Icon name="brand-apple" size={20} />
|
||||
</span>
|
||||
<div className="hk-installation-guide__store-copy">
|
||||
<div className="hk-installation-guide__store-title">
|
||||
花魁短剧 App Store 版
|
||||
{guide.hasIOSStore && (
|
||||
<span className="hk-installation-guide__recommended">推荐</span>
|
||||
)}
|
||||
</div>
|
||||
<p>
|
||||
{guide.hasIOSStore
|
||||
? "通过 App Store 安装,随系统自动更新,与轻量版账号内容互通"
|
||||
: "前往官方下载页,查看商店版是否可用及当前设备的安装方式"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hk-installation-guide__step hk-installation-guide__store-tip">
|
||||
<span className="hk-installation-guide__badge hk-installation-guide__badge--tip">
|
||||
提示
|
||||
</span>
|
||||
<div className="hk-installation-guide__step-copy">
|
||||
<h3>所在地区未上架时</h3>
|
||||
<p>请切换到轻量版,用 Safari 安装描述文件</p>
|
||||
</div>
|
||||
</div>
|
||||
{notice("需登录 App Store 账号;若显示「不可用」,请返回选择轻量版")}
|
||||
{action(
|
||||
"store",
|
||||
guide.iosStoreURL,
|
||||
guide.hasIOSStore ? "前往 App Store" : "前往下载页",
|
||||
"install/external-link",
|
||||
guide.hasIOSStore,
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="hk-installation-guide__content"
|
||||
id="install-guide-profile"
|
||||
role="tabpanel"
|
||||
aria-labelledby="install-guide-profile-tab"
|
||||
data-install-panel="profile"
|
||||
hidden={panel !== "profile"}
|
||||
>
|
||||
{steps([
|
||||
[
|
||||
guide.hasIOSProfile ? "允许下载描述文件" : "前往官方下载页",
|
||||
guide.hasIOSProfile
|
||||
? "用 Safari 打开本页,点击「安装轻量版」,弹窗选择「允许」"
|
||||
: "用 Safari 打开本页,点击「前往下载页」,查看轻量版是否可用及安装说明",
|
||||
],
|
||||
[
|
||||
"安装描述文件",
|
||||
"前往 设置 / 已下载描述文件,选择「安装」并输入密码",
|
||||
],
|
||||
[
|
||||
"返回桌面打开",
|
||||
"桌面出现花魁短剧图标即安装完成,建议保存账号凭证",
|
||||
],
|
||||
])}
|
||||
{notice(
|
||||
"仅支持 Safari;若在微信 / Telegram 内打开,请先点右上角「在浏览器中打开」",
|
||||
)}
|
||||
{action(
|
||||
"profile",
|
||||
guide.iosProfileURL,
|
||||
guide.hasIOSProfile ? "安装轻量版" : "前往下载页",
|
||||
"brand-apple",
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="hk-installation-guide__content"
|
||||
id="install-guide-android"
|
||||
data-install-panel="android"
|
||||
hidden={panel !== "android"}
|
||||
>
|
||||
{steps([
|
||||
[
|
||||
guide.hasAndroidAPK ? "下载 APK 安装包" : "前往官方下载页",
|
||||
guide.hasAndroidAPK
|
||||
? "点击「下载 APK」,下载完成后在通知栏或下载列表点击打开"
|
||||
: "点击「前往下载页」,查看 Android 安装包及适用于当前设备的安装说明",
|
||||
],
|
||||
[
|
||||
"允许安装未知来源",
|
||||
"系统提示「禁止安装来源不明的应用」时,点击「设置」开启允许",
|
||||
],
|
||||
[
|
||||
"打开花魁短剧",
|
||||
"安装完成后点击「打开」,或在桌面找到花魁短剧图标",
|
||||
],
|
||||
])}
|
||||
{notice("如系统提示风险,请核实安装包来源后再继续")}
|
||||
{action(
|
||||
"android",
|
||||
guide.androidAPKURL,
|
||||
guide.hasAndroidAPK ? "下载 APK" : "前往下载页",
|
||||
"brand-android",
|
||||
)}
|
||||
</div>
|
||||
{guide.deepLinkURL && (
|
||||
<button
|
||||
className="hk-installation-guide__launch"
|
||||
type="button"
|
||||
data-install-launch=""
|
||||
onClick={() => location.assign(guide.deepLinkURL)}
|
||||
>
|
||||
已安装?打开花魁短剧 App
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
interface Ad {
|
||||
id?: string;
|
||||
advertiseCode?: string;
|
||||
images?: string;
|
||||
link?: string;
|
||||
title?: string;
|
||||
location?: string;
|
||||
advertiseLocationCode?: string;
|
||||
advertiseLocationName?: string;
|
||||
}
|
||||
function AdItem({ ad, onClose }: { ad: Ad; onClose: () => void }) {
|
||||
const [ready, setReady] = useState(false);
|
||||
const ref = useDialog(onClose, ready);
|
||||
const id = ad.advertiseCode || ad.id || "";
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(onClose, 12000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose]);
|
||||
useEffect(() => {
|
||||
if (ready && ref.current)
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("external-ad:track", {
|
||||
detail: {
|
||||
element: ref.current.querySelector("[data-track-ad]"),
|
||||
eventType: "show",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [ready, ref]);
|
||||
const image = (
|
||||
<EncryptedImage
|
||||
eager
|
||||
className="hk-advertisement-dialog-image"
|
||||
src={ad.images || ""}
|
||||
alt={ad.title || ""}
|
||||
onResolved={() => setReady(true)}
|
||||
onResolveError={onClose}
|
||||
onError={onClose}
|
||||
/>
|
||||
);
|
||||
const click = () =>
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("external-ad:track", {
|
||||
detail: {
|
||||
element: ref.current?.querySelector("[data-track-ad]"),
|
||||
eventType: "click",
|
||||
},
|
||||
}),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={"hk-advertisement-dialog-mask" + (ready ? " hk-is-open" : "")}
|
||||
data-dialog-ad=""
|
||||
style={
|
||||
!ready
|
||||
? { display: "flex", visibility: "hidden", pointerEvents: "none" }
|
||||
: undefined
|
||||
}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="hk-advertisement-dialog-panel"
|
||||
data-dialog-ad-item=""
|
||||
data-dialog-ad-id={id}
|
||||
data-track-ad="external-dialog"
|
||||
data-ad-id={id}
|
||||
data-creative-id=""
|
||||
data-ad-location={ad.location}
|
||||
data-ad-slot-key={ad.advertiseLocationCode || ad.location || ""}
|
||||
data-ad-slot-name={
|
||||
ad.advertiseLocationName || "弹窗广告位" + (ad.location || "")
|
||||
}
|
||||
data-ad-type="interstitial"
|
||||
data-ad-clickable={ad.link ? "1" : undefined}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={ad.title || "广告"}
|
||||
>
|
||||
<div className="hk-advertisement-dialog-card">
|
||||
<div className="hk-advertisement-dialog-cover">
|
||||
{ad.link ? (
|
||||
<a
|
||||
className="hk-advertisement-dialog-media"
|
||||
href={ad.link}
|
||||
target="_blank"
|
||||
rel="sponsored nofollow noopener"
|
||||
data-dialog-ad-link=""
|
||||
onClick={click}
|
||||
>
|
||||
{image}
|
||||
</a>
|
||||
) : (
|
||||
<div className="hk-advertisement-dialog-media">{image}</div>
|
||||
)}
|
||||
<span className="hk-tag hk-tag--dark hk-advertisement-dialog-tag">广告</span>
|
||||
<button
|
||||
className="hk-advertisement-dialog-close"
|
||||
type="button"
|
||||
data-dialog-ad-close=""
|
||||
onClick={onClose}
|
||||
aria-label="关闭广告"
|
||||
>
|
||||
<Icon name="close" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="hk-advertisement-dialog-foot">
|
||||
<div className="hk-advertisement-dialog-copy">
|
||||
<p className="hk-advertisement-dialog-title">{ad.title || "精彩推荐"}</p>
|
||||
<p className="hk-advertisement-dialog-desc">每日展示一次</p>
|
||||
</div>
|
||||
{ad.link && (
|
||||
<a
|
||||
className="hk-advertisement-dialog-cta"
|
||||
href={ad.link}
|
||||
target="_blank"
|
||||
rel="sponsored nofollow noopener"
|
||||
data-dialog-ad-link=""
|
||||
onClick={click}
|
||||
>
|
||||
<Icon
|
||||
name="play_hollow"
|
||||
size={18}
|
||||
className="hk-advertisement-dialog-cta__icon"
|
||||
/>
|
||||
立即观看
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function AdvertisementQueue({
|
||||
data,
|
||||
enabled,
|
||||
}: {
|
||||
data: Data;
|
||||
enabled: boolean;
|
||||
}) {
|
||||
const ads: Ad[] = data.appConfig?.dialogList || [];
|
||||
const signature = "2|" + ads.map((ad) => ad.advertiseCode || ad.id).join("|");
|
||||
const [index, setIndex] = useState(-1);
|
||||
useEffect(() => {
|
||||
if (!enabled || !ads.length) return;
|
||||
try {
|
||||
if (sessionStorage.getItem("dialogAdDismissed") === signature) return;
|
||||
const saved = JSON.parse(
|
||||
sessionStorage.getItem("dialogAdProgress") || "null",
|
||||
);
|
||||
setIndex(saved?.signature === signature ? saved.index : 0);
|
||||
} catch {
|
||||
setIndex(0);
|
||||
}
|
||||
}, [enabled, signature, ads.length]);
|
||||
const close = useRef(() => {});
|
||||
close.current = () => {
|
||||
const next = index + 1;
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
"dialogAdProgress",
|
||||
JSON.stringify({ signature, index: next }),
|
||||
);
|
||||
if (next >= ads.length)
|
||||
sessionStorage.setItem("dialogAdDismissed", signature);
|
||||
} catch {}
|
||||
setIndex(next);
|
||||
};
|
||||
const stableClose = useRef(() => close.current()).current;
|
||||
if (!enabled || index < 0 || index >= ads.length) return null;
|
||||
return <AdItem key={index} ad={ads[index]} onClose={stableClose} />;
|
||||
}
|
||||
Reference in New Issue
Block a user