refactor: remove "Services" pages and migrate content to focused alternatives
- Removed `/services` and `/zh/services` pages. Migrated content to updated pages: `/uses`, `/about#contact-card`, and `/hire`. - Removed Framer Motion for better performance and simpler animations in `LanguageSwitcher`, `GlassHeader`, and other components. - Updated font sources to WOFF2 for better compression and added preload links for critical fonts. - Optimized Vite configuration with manual chunking for React libraries. - Replaced `client:load` with `client:idle` for non-critical client-side components like `GlassHeader`, `Footer`, and `BackToTop`.
This commit is contained in:
BIN
.playwright-mcp/page-2026-03-17T05-13-50-415Z.png
Normal file
BIN
.playwright-mcp/page-2026-03-17T05-13-50-415Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 463 KiB |
@@ -17,7 +17,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
integrations: [
|
||||
react(),
|
||||
react(),
|
||||
sitemap({
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
@@ -31,6 +31,16 @@ export default defineConfig({
|
||||
],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
build: {
|
||||
cssCodeSplit: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vendor-react': ['react', 'react-dom'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
i18n: {
|
||||
// The default locale to fall back to if a page isn't available in the active locale
|
||||
|
||||
BIN
public/fonts/archivo-400.ttf
Normal file
BIN
public/fonts/archivo-400.ttf
Normal file
Binary file not shown.
BIN
public/fonts/archivo-400.woff2
Normal file
BIN
public/fonts/archivo-400.woff2
Normal file
Binary file not shown.
BIN
public/fonts/archivo-700.ttf
Normal file
BIN
public/fonts/archivo-700.ttf
Normal file
Binary file not shown.
BIN
public/fonts/archivo-700.woff2
Normal file
BIN
public/fonts/archivo-700.woff2
Normal file
Binary file not shown.
BIN
public/fonts/space-grotesk-400.ttf
Normal file
BIN
public/fonts/space-grotesk-400.ttf
Normal file
Binary file not shown.
BIN
public/fonts/space-grotesk-400.woff2
Normal file
BIN
public/fonts/space-grotesk-400.woff2
Normal file
Binary file not shown.
BIN
public/fonts/space-grotesk-700.ttf
Normal file
BIN
public/fonts/space-grotesk-700.ttf
Normal file
Binary file not shown.
BIN
public/fonts/space-grotesk-700.woff2
Normal file
BIN
public/fonts/space-grotesk-700.woff2
Normal file
Binary file not shown.
@@ -1,7 +1,6 @@
|
||||
import { useTranslations } from "@/i18n/utils";
|
||||
import type { Lang } from "@/types/i18n";
|
||||
import { personalInfo } from "@/lib/data/index";
|
||||
import { motion } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import { defaultLang } from "@/i18n/ui";
|
||||
import Container from "./ui/Container.tsx";
|
||||
@@ -9,7 +8,7 @@ import { type FooterProps } from "@/types";
|
||||
|
||||
export default function Footer({ lang: propLang }: FooterProps) {
|
||||
const [lang, setLang] = useState<Lang>(propLang || defaultLang);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const htmlLang = document.documentElement.lang as Lang;
|
||||
if (htmlLang && (!propLang || htmlLang !== lang)) {
|
||||
@@ -21,60 +20,38 @@ export default function Footer({ lang: propLang }: FooterProps) {
|
||||
return (
|
||||
<footer className="border-t border-primary/10 py-6 bg-gradient-to-b from-background to-muted/20 backdrop-blur-sm">
|
||||
<Container>
|
||||
<motion.div
|
||||
<div
|
||||
className="flex flex-col md:flex-row justify-between items-center"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<motion.div className="flex flex-col gap-2">
|
||||
<motion.p
|
||||
className="text-sm text-muted-foreground text-center md:text-left"
|
||||
whileHover={{ scale: 1.01 }}
|
||||
<div className="flex flex-col gap-2">
|
||||
<p
|
||||
className="text-sm text-muted-foreground text-center md:text-left hover:scale-[1.01] transition-transform"
|
||||
>
|
||||
© {new Date().getFullYear()} { personalInfo.name }. {t('footer.rights')} ✨
|
||||
</motion.p>
|
||||
<motion.p
|
||||
className="text-sm text-primary font-medium text-center md:text-left"
|
||||
whileHover={{ scale: 1.01 }}
|
||||
</p>
|
||||
<p
|
||||
className="text-sm text-primary font-medium text-center md:text-left hover:scale-[1.01] transition-transform"
|
||||
>
|
||||
{lang === 'zh' ? '正在招聘远程工程师或寻求项目合作?欢迎联系我。' : 'Hiring for a remote engineering role or looking for project collaboration? Let’s connect.'}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
<motion.p
|
||||
className="text-sm text-muted-foreground mt-2 md:mt-0 text-center md:text-left"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className="text-sm text-muted-foreground mt-2 md:mt-0 text-center md:text-left hover:scale-[1.01] transition-transform"
|
||||
>
|
||||
Built with{" "}
|
||||
<motion.span
|
||||
className="inline-block"
|
||||
initial={{ rotate: 0 }}
|
||||
whileHover={{ rotate: 360 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
<span
|
||||
className="inline-block hover:rotate-360 transition-transform duration-500"
|
||||
>
|
||||
💻
|
||||
</motion.span>{" "}
|
||||
</span>{" "}
|
||||
and{" "}
|
||||
<motion.span
|
||||
className="inline-block"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
}}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
duration: 1.5,
|
||||
}}
|
||||
<span
|
||||
className="inline-block animate-pulse"
|
||||
>
|
||||
❤️
|
||||
</motion.span>
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useState, useEffect } from "react";
|
||||
import { Menu, X, Home, Rocket, PenTool, User, Briefcase, Clock3 } from "lucide-react";
|
||||
import { defaultLang } from "@/i18n/ui";
|
||||
import { type GlassHeaderProps } from "@/types";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
||||
@@ -98,7 +97,7 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.header
|
||||
<header
|
||||
className={`fixed top-0 z-50 w-full transition-all duration-300 ${
|
||||
isScrolled
|
||||
? 'backdrop-blur-xl backdrop-saturate-150 bg-white/70 dark:bg-black/70 border-b border-border/50 shadow-lg shadow-black/5 dark:shadow-primary/5'
|
||||
@@ -106,10 +105,8 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
||||
}`}
|
||||
>
|
||||
<Container className="p-4 flex justify-between items-center">
|
||||
<motion.a
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="flex items-center text-lg font-medium transition-colors duration-150 hover:text-foreground/80"
|
||||
<a
|
||||
className="flex items-center text-lg font-medium transition-colors duration-150 hover:text-foreground/80 hover:scale-105 active:scale-95"
|
||||
href={getLocalizedPath('/', lang)}
|
||||
>
|
||||
<div className="w-6 h-6 mr-2 flex items-center justify-center">
|
||||
@@ -119,50 +116,48 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
||||
</svg>
|
||||
</div>
|
||||
<span className="gradient-text font-bold">{personalInfo.name}</span>
|
||||
</motion.a>
|
||||
</a>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
|
||||
{navItems.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<motion.a
|
||||
<a
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 transition-colors duration-150 px-3 py-2 rounded-md hover:bg-primary/5",
|
||||
active
|
||||
? "text-primary bg-primary/5 font-semibold"
|
||||
active
|
||||
? "text-primary bg-primary/5 font-semibold"
|
||||
: "text-foreground/70 hover:text-primary"
|
||||
)}
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<item.icon size={16} />
|
||||
{t(item.key as any)}
|
||||
</motion.a>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Language Switcher added here */}
|
||||
<motion.div>
|
||||
<div>
|
||||
<LanguageSwitcher lang={lang} />
|
||||
</motion.div>
|
||||
|
||||
<motion.div>
|
||||
<ThemeToggle />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<motion.button
|
||||
className="md:hidden p-2 text-foreground transition-all duration-150 hover:bg-foreground/10 active:bg-foreground/20 rounded-md"
|
||||
<div>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
className="md:hidden p-2 text-foreground transition-all duration-150 hover:bg-foreground/10 active:bg-foreground/20 rounded-md active:scale-90"
|
||||
onClick={toggleMenu}
|
||||
aria-label="Toggle menu"
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</motion.button>
|
||||
</button>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -171,15 +166,15 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
||||
isMenuOpen ? 'max-h-80 opacity-100' : 'max-h-0 opacity-0'
|
||||
}`}>
|
||||
<div className={`py-4 px-4 border-t border-border/10 glass-effect shadow-xl ${
|
||||
isScrolled
|
||||
? 'bg-white/90 dark:bg-black/80'
|
||||
isScrolled
|
||||
? 'bg-white/90 dark:bg-black/80'
|
||||
: 'bg-white/95 dark:bg-black/85'
|
||||
}`}>
|
||||
<nav className="flex flex-col space-y-3 text-sm font-medium">
|
||||
{navItems.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<motion.a
|
||||
<a
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
@@ -192,12 +187,12 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
||||
>
|
||||
<item.icon size={18} />
|
||||
{t(item.key as any)}
|
||||
</motion.a>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</motion.header>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,19 +3,18 @@ import { getLocalizedPath } from "@/i18n/utils";
|
||||
import type { Lang } from "@/types/i18n";
|
||||
import { languages as i18nLanguages, defaultLang } from "@/i18n/ui";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { type LanguageSwitcherProps } from "@/types";
|
||||
|
||||
const availableLanguages = Object.entries(i18nLanguages).map(([code, name]) => ({
|
||||
code: code as Lang,
|
||||
name,
|
||||
icon: code === 'en' ? '🇬🇧' : code === 'zh' ? '🇨🇳' : '🌐'
|
||||
icon: code === 'en' ? '🇬🇧' : code === 'zh' ? '🇨🇳' : '🌐'
|
||||
}));
|
||||
|
||||
export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentLang, setCurrentLang] = useState<Lang>(propLang || defaultLang);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const pathLang = window.location.pathname.startsWith("/zh") ? "zh" : "en";
|
||||
@@ -32,10 +31,10 @@ export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherPro
|
||||
}
|
||||
}
|
||||
}, [propLang, currentLang]);
|
||||
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(() => {
|
||||
return availableLanguages.find(l => l.code === currentLang) ||
|
||||
availableLanguages.find(l => l.code === defaultLang) ||
|
||||
return availableLanguages.find(l => l.code === currentLang) ||
|
||||
availableLanguages.find(l => l.code === defaultLang) ||
|
||||
availableLanguages[0];
|
||||
});
|
||||
|
||||
@@ -65,45 +64,38 @@ export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherPro
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<motion.button
|
||||
<button
|
||||
onClick={toggleOpen}
|
||||
className="flex items-center p-2 rounded-md hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-sm"
|
||||
className="flex items-center p-2 rounded-md hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-sm active:scale-95"
|
||||
aria-label={`Change language, current language is ${selectedLanguage.name}`}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<span className="mr-1.5">{selectedLanguage.icon}</span>
|
||||
<span className="hidden sm:inline">{selectedLanguage.name}</span>
|
||||
<span className="sm:hidden">{selectedLanguage.code.toUpperCase()}</span>
|
||||
<ChevronDown size={16} className={`ml-1.5 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} />
|
||||
</motion.button>
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="absolute right-0 mt-2 w-40 origin-top-right rounded-md bg-popover text-popover-foreground shadow-md focus:outline-none z-50 border border-border/20"
|
||||
>
|
||||
<div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||
{availableLanguages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => handleSelectLanguage(lang)}
|
||||
className="w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground flex items-center justify-between cursor-pointer focus:bg-accent focus:outline-none"
|
||||
role="menuitem"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-2">{lang.icon}</span>
|
||||
{lang.name}
|
||||
</span>
|
||||
{selectedLanguage.code === lang.code && <Check size={16} className="text-primary" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute right-0 mt-2 w-40 origin-top-right rounded-md bg-popover text-popover-foreground shadow-md focus:outline-none z-50 border border-border/20 animate-in fade-in slide-in-from-top-2"
|
||||
>
|
||||
<div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||
{availableLanguages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => handleSelectLanguage(lang)}
|
||||
className="w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground flex items-center justify-between cursor-pointer focus:bg-accent focus:outline-none"
|
||||
role="menuitem"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-2">{lang.icon}</span>
|
||||
{lang.name}
|
||||
</span>
|
||||
{selectedLanguage.code === lang.code && <Check size={16} className="text-primary" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const title = lang === 'zh' ? '目录' : 'Table of Contents';
|
||||
</svg>
|
||||
{title}
|
||||
</h3>
|
||||
<ScrollArea className="w-full" client:load>
|
||||
<ScrollArea className="w-full" client:visible>
|
||||
<div class="text-neutral-500 dark:text-neutral-300 pr-4">
|
||||
<ul id="toc-list" class="leading-relaxed text-sm sm:text-base border-l dark:border-neutral-500/20 border-blacktext/20 mt-4">
|
||||
</ul>
|
||||
|
||||
@@ -23,6 +23,9 @@ const t = useTranslations(lang);
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<!-- Preload critical fonts - using WOFF2 for better compression -->
|
||||
<link rel="preload" href="/fonts/archivo-700.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/space-grotesk-400.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<meta name="description" content={description} />
|
||||
<title>{title} | {t("site.title")}</title>
|
||||
@@ -46,7 +49,7 @@ const t = useTranslations(lang);
|
||||
<div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(249,115,22,0.05),transparent_40%)]"></div>
|
||||
</div>
|
||||
<slot />
|
||||
<BackToTop client:load />
|
||||
<BackToTop client:idle />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const latestPosts = sortPostsByDate(
|
||||
---
|
||||
|
||||
<Layout title={isZh ? '首页' : 'Home'}>
|
||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||
<GlassHeader lang={lang} client:idle transition:persist="header" />
|
||||
|
||||
<main class="min-h-screen pt-24 pb-20">
|
||||
<Container>
|
||||
@@ -180,5 +180,5 @@ const latestPosts = sortPostsByDate(
|
||||
</Container>
|
||||
</main>
|
||||
|
||||
<Footer lang={lang} client:load />
|
||||
<Footer lang={lang} client:idle />
|
||||
</Layout>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
title: "Moved to Uses"
|
||||
description: "This page has moved."
|
||||
layout: "../layouts/AboutLayout.astro"
|
||||
---
|
||||
|
||||
# This page moved
|
||||
|
||||
The old **Services** page has been replaced by focused pages:
|
||||
|
||||
- [Uses](/uses)
|
||||
- [About (Contact Card)](/about#contact-card)
|
||||
|
||||
If you are hiring for a remote role, please start from [Hire](/hire) and use the contact card on that page.
|
||||
@@ -33,7 +33,7 @@ const latestPosts = sortPostsByDate(
|
||||
---
|
||||
|
||||
<Layout title={isZh ? '首页' : 'Home'}>
|
||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||
<GlassHeader lang={lang} client:idle transition:persist="header" />
|
||||
|
||||
<main class="min-h-screen pt-24 pb-20">
|
||||
<Container>
|
||||
@@ -180,5 +180,5 @@ const latestPosts = sortPostsByDate(
|
||||
</Container>
|
||||
</main>
|
||||
|
||||
<Footer lang={lang} client:load />
|
||||
<Footer lang={lang} client:idle />
|
||||
</Layout>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
title: "页面已迁移"
|
||||
description: "该页面已迁移到新的结构。"
|
||||
layout: "../../layouts/AboutLayout.astro"
|
||||
---
|
||||
|
||||
# 页面已迁移
|
||||
|
||||
原 **服务** 页面已拆分为更清晰的页面:
|
||||
|
||||
- [工具](/zh/uses)
|
||||
- [关于(联系卡片)](/zh/about#contact-card)
|
||||
|
||||
如需沟通远程岗位,请优先查看 [合作](/zh/hire),并使用页面内的联系卡片。
|
||||
@@ -5,7 +5,7 @@
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url('./fonts/archivo-400.ttf') format('truetype');
|
||||
src: url('/fonts/archivo-400.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Archivo';
|
||||
@@ -13,7 +13,7 @@
|
||||
font-weight: 700;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url('./fonts/archivo-700.ttf') format('truetype');
|
||||
src: url('/fonts/archivo-700.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* Local Fonts - Space Grotesk */
|
||||
@@ -22,12 +22,12 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./fonts/space-grotesk-400.ttf') format('truetype');
|
||||
src: url('/fonts/space-grotesk-400.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('./fonts/space-grotesk-700.ttf') format('truetype');
|
||||
src: url('/fonts/space-grotesk-700.woff2') format('woff2');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user