mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-06-13 05:03:08 +08:00
feat: mobile style
This commit is contained in:
@@ -111,7 +111,7 @@ function DetailPageClient() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout activePath='/detail'>
|
<PageLayout activePath='/detail'>
|
||||||
<div className='px-10 py-8 overflow-visible'>
|
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible'>
|
||||||
{/* 顶部返回按钮已移入右侧信息容器 */}
|
{/* 顶部返回按钮已移入右侧信息容器 */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className='flex items-center justify-center min-h-[60vh]'>
|
<div className='flex items-center justify-center min-h-[60vh]'>
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ function DoubanPageClient() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout activePath={getActivePath()}>
|
<PageLayout activePath={getActivePath()}>
|
||||||
<div className='px-10 py-8 overflow-visible'>
|
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible'>
|
||||||
{/* 页面标题 */}
|
{/* 页面标题 */}
|
||||||
<div className='mb-8'>
|
<div className='mb-8'>
|
||||||
<h1 className='text-3xl font-bold text-gray-800 mb-2'>
|
<h1 className='text-3xl font-bold text-gray-800 mb-2'>
|
||||||
@@ -200,7 +200,7 @@ function DoubanPageClient() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* 内容网格 */}
|
{/* 内容网格 */}
|
||||||
<div className='grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-x-8 gap-y-20 px-4'>
|
<div className='grid grid-cols-2 gap-x-2 gap-y-12 px-2 sm:grid-cols-[repeat(auto-fit,minmax(180px,1fr))] sm:gap-x-8 sm:gap-y-20 sm:px-4'>
|
||||||
{loading
|
{loading
|
||||||
? // 显示骨架屏
|
? // 显示骨架屏
|
||||||
skeletonData.map((index) => (
|
skeletonData.map((index) => (
|
||||||
@@ -208,7 +208,7 @@ function DoubanPageClient() {
|
|||||||
))
|
))
|
||||||
: // 显示实际数据
|
: // 显示实际数据
|
||||||
doubanData.map((item, index) => (
|
doubanData.map((item, index) => (
|
||||||
<div key={`${item.title}-${index}`} className='w-44'>
|
<div key={`${item.title}-${index}`} className='w-full'>
|
||||||
<DemoCard title={item.title} poster={item.poster} />
|
<DemoCard title={item.title} poster={item.poster} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -73,3 +73,17 @@ body {
|
|||||||
rgba(0, 0, 0, 0.8) 100%
|
rgba(0, 0, 0, 0.8) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 隐藏移动端(<768px)垂直滚动条 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
-ms-overflow-style: none; /* IE & Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar,
|
||||||
|
body::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome Safari */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ function HomeClient() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className='px-10 py-8'>
|
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible'>
|
||||||
{/* 顶部 Tab 切换 */}
|
{/* 顶部 Tab 切换 */}
|
||||||
<div className='mb-8 flex justify-center'>
|
<div className='mb-8 flex justify-center'>
|
||||||
<CapsuleSwitch
|
<CapsuleSwitch
|
||||||
@@ -150,7 +150,7 @@ function HomeClient() {
|
|||||||
<h2 className='mb-4 text-xl font-bold text-gray-800 text-left'>
|
<h2 className='mb-4 text-xl font-bold text-gray-800 text-left'>
|
||||||
我的收藏
|
我的收藏
|
||||||
</h2>
|
</h2>
|
||||||
<div className='justify-start grid grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] gap-x-8 gap-y-20 px-4'>
|
<div className='justify-start grid grid-cols-2 gap-x-2 gap-y-6 px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:gap-y-20 sm:px-4'>
|
||||||
{favoriteItems.map((item) => (
|
{favoriteItems.map((item) => (
|
||||||
<div key={item.id + item.source} className='w-full'>
|
<div key={item.id + item.source} className='w-full'>
|
||||||
<VideoCard {...item} from='favorites' />
|
<VideoCard {...item} from='favorites' />
|
||||||
@@ -173,7 +173,10 @@ function HomeClient() {
|
|||||||
</h2>
|
</h2>
|
||||||
<ScrollableRow scrollDistance={800}>
|
<ScrollableRow scrollDistance={800}>
|
||||||
{collections.map((collection) => (
|
{collections.map((collection) => (
|
||||||
<div key={collection.title} className='min-w-[280px] w-72'>
|
<div
|
||||||
|
key={collection.title}
|
||||||
|
className='min-w-[180px] w-44 sm:min-w-[280px] sm:w-72'
|
||||||
|
>
|
||||||
<CollectionCard
|
<CollectionCard
|
||||||
title={collection.title}
|
title={collection.title}
|
||||||
icon={collection.icon}
|
icon={collection.icon}
|
||||||
@@ -196,7 +199,10 @@ function HomeClient() {
|
|||||||
{loading
|
{loading
|
||||||
? // 加载状态显示灰色占位数据
|
? // 加载状态显示灰色占位数据
|
||||||
Array.from({ length: 8 }).map((_, index) => (
|
Array.from({ length: 8 }).map((_, index) => (
|
||||||
<div key={index} className='min-w-[180px] w-44'>
|
<div
|
||||||
|
key={index}
|
||||||
|
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||||
|
>
|
||||||
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
|
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
|
||||||
<div className='absolute inset-0 bg-gray-300'></div>
|
<div className='absolute inset-0 bg-gray-300'></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,7 +211,10 @@ function HomeClient() {
|
|||||||
))
|
))
|
||||||
: // 显示真实数据
|
: // 显示真实数据
|
||||||
hotMovies.map((movie, index) => (
|
hotMovies.map((movie, index) => (
|
||||||
<div key={index} className='min-w-[180px] w-44'>
|
<div
|
||||||
|
key={index}
|
||||||
|
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||||
|
>
|
||||||
<DemoCard title={movie.title} poster={movie.poster} />
|
<DemoCard title={movie.title} poster={movie.poster} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -221,7 +230,10 @@ function HomeClient() {
|
|||||||
{loading
|
{loading
|
||||||
? // 加载状态显示灰色占位数据
|
? // 加载状态显示灰色占位数据
|
||||||
Array.from({ length: 8 }).map((_, index) => (
|
Array.from({ length: 8 }).map((_, index) => (
|
||||||
<div key={index} className='min-w-[180px] w-44'>
|
<div
|
||||||
|
key={index}
|
||||||
|
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||||
|
>
|
||||||
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
|
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
|
||||||
<div className='absolute inset-0 bg-gray-300'></div>
|
<div className='absolute inset-0 bg-gray-300'></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,7 +242,10 @@ function HomeClient() {
|
|||||||
))
|
))
|
||||||
: // 显示真实数据
|
: // 显示真实数据
|
||||||
hotTvShows.map((show, index) => (
|
hotTvShows.map((show, index) => (
|
||||||
<div key={index} className='min-w-[180px] w-44'>
|
<div
|
||||||
|
key={index}
|
||||||
|
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||||
|
>
|
||||||
<DemoCard title={show.title} poster={show.poster} />
|
<DemoCard title={show.title} poster={show.poster} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ function SearchPageClient() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout activePath='/search'>
|
<PageLayout activePath='/search'>
|
||||||
<div className='px-10 py-8 overflow-visible mb-10'>
|
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible mb-10'>
|
||||||
{/* 搜索框 */}
|
{/* 搜索框 */}
|
||||||
<div className='mb-8'>
|
<div className='mb-8'>
|
||||||
<form onSubmit={handleSearch} className='max-w-2xl mx-auto'>
|
<form onSubmit={handleSearch} className='max-w-2xl mx-auto'>
|
||||||
@@ -125,7 +125,7 @@ function SearchPageClient() {
|
|||||||
</div>
|
</div>
|
||||||
) : showResults ? (
|
) : showResults ? (
|
||||||
// 搜索结果
|
// 搜索结果
|
||||||
<div className='justify-start grid grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] gap-x-8 gap-y-20 px-4'>
|
<div className='justify-start grid grid-cols-2 gap-x-2 gap-y-20 px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:gap-y-20 sm:px-4'>
|
||||||
{searchResults.map((item) => (
|
{searchResults.map((item) => (
|
||||||
<div key={item.id} className='w-full'>
|
<div key={item.id} className='w-full'>
|
||||||
<VideoCard {...item} from='search' />
|
<VideoCard {...item} from='search' />
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const CapsuleSwitch: React.FC<CapsuleSwitchProps> = ({
|
|||||||
<button
|
<button
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
onClick={() => onChange(opt.value)}
|
onClick={() => onChange(opt.value)}
|
||||||
className={`w-20 px-3 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
|
className={`w-16 px-3 py-1 text-xs sm:w-20 sm:py-2 sm:text-sm rounded-full font-medium transition-all duration-200 ${
|
||||||
active === opt.value
|
active === opt.value
|
||||||
? 'bg-white text-gray-900 shadow-sm'
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
: 'text-gray-700 hover:text-gray-900'
|
: 'text-gray-700 hover:text-gray-900'
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function CollectionCard({
|
|||||||
<div className='relative aspect-[5/3] w-full overflow-hidden rounded-xl bg-gray-200 border border-gray-300/50'>
|
<div className='relative aspect-[5/3] w-full overflow-hidden rounded-xl bg-gray-200 border border-gray-300/50'>
|
||||||
{/* 图标容器 */}
|
{/* 图标容器 */}
|
||||||
<div className='absolute inset-0 flex items-center justify-center'>
|
<div className='absolute inset-0 flex items-center justify-center'>
|
||||||
<Icon className='h-14 w-14 text-gray-600' />
|
<Icon className='h-10 w-10 sm:h-12 sm:w-12 text-gray-600' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hover 蒙版效果 - 参考 DemoCard */}
|
{/* Hover 蒙版效果 - 参考 DemoCard */}
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) {
|
|||||||
{loading
|
{loading
|
||||||
? // 加载状态显示灰色占位数据
|
? // 加载状态显示灰色占位数据
|
||||||
Array.from({ length: 6 }).map((_, index) => (
|
Array.from({ length: 6 }).map((_, index) => (
|
||||||
<div key={index} className='min-w-[180px] w-44'>
|
<div
|
||||||
|
key={index}
|
||||||
|
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||||
|
>
|
||||||
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
|
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
|
||||||
<div className='absolute inset-0 bg-gray-300'></div>
|
<div className='absolute inset-0 bg-gray-300'></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,7 +93,10 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) {
|
|||||||
playRecords.map((record) => {
|
playRecords.map((record) => {
|
||||||
const { source, id } = parseKey(record.key);
|
const { source, id } = parseKey(record.key);
|
||||||
return (
|
return (
|
||||||
<div key={record.key} className='min-w-[180px] w-44'>
|
<div
|
||||||
|
key={record.key}
|
||||||
|
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||||
|
>
|
||||||
<VideoCard
|
<VideoCard
|
||||||
id={id}
|
id={id}
|
||||||
title={record.title}
|
title={record.title}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const DoubanCardSkeleton = () => {
|
const DoubanCardSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
<div className='w-44'>
|
<div className='w-full'>
|
||||||
<div className='group relative w-full rounded-lg bg-transparent shadow-none flex flex-col'>
|
<div className='group relative w-full rounded-lg bg-transparent shadow-none flex flex-col'>
|
||||||
{/* 海报骨架 - 2:3 比例 */}
|
{/* 海报骨架 - 2:3 比例 */}
|
||||||
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
|
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
|
||||||
|
|||||||
99
src/components/MobileBottomNav.tsx
Normal file
99
src/components/MobileBottomNav.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Film,
|
||||||
|
Home,
|
||||||
|
MessageCircleHeart,
|
||||||
|
MountainSnow,
|
||||||
|
Search,
|
||||||
|
Star,
|
||||||
|
Swords,
|
||||||
|
Tv,
|
||||||
|
VenetianMask,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
const MobileBottomNav = () => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ icon: Home, label: '首页', href: '/' },
|
||||||
|
{ icon: Search, label: '搜索', href: '/search' },
|
||||||
|
{
|
||||||
|
icon: Film,
|
||||||
|
label: '热门电影',
|
||||||
|
href: '/douban?type=movie&tag=热门&title=热门电影',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Tv,
|
||||||
|
label: '热门剧集',
|
||||||
|
href: '/douban?type=tv&tag=热门&title=热门剧集',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Star,
|
||||||
|
label: '豆瓣 Top250',
|
||||||
|
href: '/douban?type=movie&tag=top250&title=豆瓣 Top250',
|
||||||
|
},
|
||||||
|
{ icon: Swords, label: '美剧', href: '/douban?type=tv&tag=美剧' },
|
||||||
|
{
|
||||||
|
icon: MessageCircleHeart,
|
||||||
|
label: '韩剧',
|
||||||
|
href: '/douban?type=tv&tag=韩剧',
|
||||||
|
},
|
||||||
|
{ icon: MountainSnow, label: '日剧', href: '/douban?type=tv&tag=日剧' },
|
||||||
|
{ icon: VenetianMask, label: '日漫', href: '/douban?type=tv&tag=日本动画' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isActive = (href: string) => {
|
||||||
|
const typeMatch = href.match(/type=([^&]+)/)?.[1];
|
||||||
|
const tagMatch = href.match(/tag=([^&]+)/)?.[1];
|
||||||
|
|
||||||
|
// 解码URL以进行正确的比较
|
||||||
|
const decodedActive = decodeURIComponent(pathname);
|
||||||
|
const decodedItemHref = decodeURIComponent(href);
|
||||||
|
|
||||||
|
return (
|
||||||
|
decodedActive === decodedItemHref ||
|
||||||
|
(decodedActive.startsWith('/douban') &&
|
||||||
|
decodedActive.includes(`type=${typeMatch}`) &&
|
||||||
|
decodedActive.includes(`tag=${tagMatch}`))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
className='md:hidden fixed left-0 right-0 z-20 bg-white/90 backdrop-blur-xl border-t border-gray-200/50 overflow-x-auto overscroll-x-contain whitespace-nowrap scrollbar-hide'
|
||||||
|
style={{
|
||||||
|
/* 紧贴视口底部,同时在内部留出安全区高度 */
|
||||||
|
bottom: 0,
|
||||||
|
paddingBottom: 'env(safe-area-inset-bottom)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ul className='flex items-center'>
|
||||||
|
{navItems.map((item) => {
|
||||||
|
const active = isActive(item.href);
|
||||||
|
return (
|
||||||
|
<li key={item.href} className='flex-shrink-0 w-1/4'>
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className='flex flex-col items-center justify-center w-full h-14 gap-1 text-xs'
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
className={`h-6 w-6 ${
|
||||||
|
active ? 'text-green-600' : 'text-gray-500'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className={active ? 'text-green-600' : 'text-gray-600'}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileBottomNav;
|
||||||
18
src/components/MobileHeader.tsx
Normal file
18
src/components/MobileHeader.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
const MobileHeader = () => {
|
||||||
|
return (
|
||||||
|
<header className='md:hidden w-full bg-white/70 backdrop-blur-xl border-b border-gray-200/50 shadow-sm'>
|
||||||
|
<div className='h-12 flex items-center justify-center'>
|
||||||
|
<Link
|
||||||
|
href='/'
|
||||||
|
className='text-2xl font-bold text-green-600 tracking-tight hover:opacity-80 transition-opacity'
|
||||||
|
>
|
||||||
|
MoonTV
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileHeader;
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import MobileBottomNav from './MobileBottomNav';
|
||||||
|
import MobileHeader from './MobileHeader';
|
||||||
import { useSidebar } from './Sidebar';
|
import { useSidebar } from './Sidebar';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
|
|
||||||
@@ -10,16 +12,33 @@ const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => {
|
|||||||
const { isCollapsed } = useSidebar();
|
const { isCollapsed } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grid grid-cols-[auto_1fr] w-full'>
|
<>
|
||||||
<Sidebar activePath={activePath} />
|
{/* 桌面端布局 */}
|
||||||
<div
|
<div className='hidden md:grid md:grid-cols-[auto_1fr] w-full'>
|
||||||
className={`min-w-0 transition-all duration-300 ${
|
<Sidebar activePath={activePath} />
|
||||||
isCollapsed ? 'col-start-2' : 'col-start-2'
|
<div
|
||||||
}`}
|
className={`min-w-0 transition-all duration-300 ${
|
||||||
>
|
isCollapsed ? 'col-start-2' : 'col-start-2'
|
||||||
{children}
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* 移动端布局 */}
|
||||||
|
<div className='md:hidden flex flex-col min-h-screen w-full'>
|
||||||
|
<MobileHeader />
|
||||||
|
<main
|
||||||
|
className='flex-1 pb-14'
|
||||||
|
style={{
|
||||||
|
paddingBottom: 'calc(3.5rem + env(safe-area-inset-bottom))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<MobileBottomNav />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContext.Provider value={contextValue}>
|
<SidebarContext.Provider value={contextValue}>
|
||||||
<div className='flex'>
|
{/* 在移动端隐藏侧边栏 */}
|
||||||
|
<div className='hidden md:flex'>
|
||||||
<aside
|
<aside
|
||||||
data-sidebar
|
data-sidebar
|
||||||
className={`fixed top-0 left-0 h-screen bg-white/40 backdrop-blur-xl transition-all duration-300 border-r border-gray-200/50 z-10 shadow-lg ${
|
className={`fixed top-0 left-0 h-screen bg-white/40 backdrop-blur-xl transition-all duration-300 border-r border-gray-200/50 z-10 shadow-lg ${
|
||||||
|
|||||||
Reference in New Issue
Block a user