Added titles, some buttons, made submitter display usernames instead of userids, quick links
Closes #162 Closes #170
This commit is contained in:
@@ -7,10 +7,7 @@ const nextConfig: NextConfig = {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "tr.rbxcdn.com",
|
||||
pathname: "/**",
|
||||
port: "",
|
||||
search: "",
|
||||
hostname: "**.rbxcdn.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
|
||||
interface HeaderButton {
|
||||
name: string;
|
||||
@@ -28,6 +29,7 @@ interface HeaderButton {
|
||||
}
|
||||
|
||||
const navItems: HeaderButton[] = [
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Submissions", href: "/submissions" },
|
||||
{ name: "Mapfixes", href: "/mapfixes" },
|
||||
{ name: "Maps", href: "/maps" },
|
||||
@@ -54,6 +56,7 @@ export default function Header() {
|
||||
const [valid, setValid] = useState<boolean>(false);
|
||||
const [user, setUser] = useState<UserInfo | null>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [quickLinksAnchor, setQuickLinksAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -67,6 +70,13 @@ export default function Header() {
|
||||
setMobileOpen(!mobileOpen);
|
||||
};
|
||||
|
||||
const handleQuickLinksOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setQuickLinksAnchor(event.currentTarget);
|
||||
};
|
||||
const handleQuickLinksClose = () => {
|
||||
setQuickLinksAnchor(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function getLoginInfo() {
|
||||
try {
|
||||
@@ -129,6 +139,15 @@ export default function Header() {
|
||||
</Box>
|
||||
);
|
||||
|
||||
const quickLinks = [
|
||||
{ name: "Bhop", href: "https://www.roblox.com/games/5315046213" },
|
||||
{ name: "Bhop Maptest", href: "https://www.roblox.com/games/517201717" },
|
||||
{ name: "Surf", href: "https://www.roblox.com/games/5315066937" },
|
||||
{ name: "Surf Maptest", href: "https://www.roblox.com/games/517206177" },
|
||||
{ name: "Fly Trials", href: "https://www.roblox.com/games/12591611759" },
|
||||
{ name: "Fly Trials Maptest", href: "https://www.roblox.com/games/12724901535" },
|
||||
];
|
||||
|
||||
return (
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
@@ -146,10 +165,43 @@ export default function Header() {
|
||||
|
||||
{/* Desktop navigation */}
|
||||
{!isMobile && (
|
||||
<Box display="flex" flexGrow={1} gap={2}>
|
||||
<Box display="flex" flexGrow={1} gap={2} alignItems="center">
|
||||
{navItems.map((item) => (
|
||||
<HeaderButton key={item.name} name={item.name} href={item.href} />
|
||||
))}
|
||||
<Box sx={{ flexGrow: 1 }} /> {/* Push quick links to the right */}
|
||||
{/* Quick Links Dropdown */}
|
||||
<Box>
|
||||
<Button
|
||||
color="inherit"
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
onClick={handleQuickLinksOpen}
|
||||
sx={{ textTransform: 'none', fontSize: '0.95rem', px: 1 }}
|
||||
>
|
||||
QUICK LINKS
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={quickLinksAnchor}
|
||||
open={Boolean(quickLinksAnchor)}
|
||||
onClose={handleQuickLinksClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
{quickLinks.map(link => (
|
||||
<MenuItem
|
||||
key={link.name}
|
||||
onClick={handleQuickLinksClose}
|
||||
sx={{ minWidth: 180 }}
|
||||
component="a"
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{link.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@ interface CopyableFieldProps {
|
||||
value: string | number | null | undefined;
|
||||
onCopy: (value: string) => void;
|
||||
placeholderText?: string;
|
||||
link?: string; // Optional link prop
|
||||
}
|
||||
|
||||
export const CopyableField = ({
|
||||
label,
|
||||
value,
|
||||
onCopy,
|
||||
placeholderText = "Not assigned"
|
||||
placeholderText = "Not assigned",
|
||||
link
|
||||
}: CopyableFieldProps) => {
|
||||
const displayValue = value?.toString() || placeholderText;
|
||||
|
||||
@@ -20,7 +22,18 @@ export const CopyableField = ({
|
||||
<>
|
||||
<Typography variant="body2" color="text.secondary">{label}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Typography variant="body1">{displayValue}</Typography>
|
||||
{link ? (
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'inherit', textDecoration: 'none', cursor: 'pointer' }}
|
||||
>
|
||||
<Typography variant="body1" sx={{ '&:hover': { textDecoration: 'underline' } }}>{displayValue}</Typography>
|
||||
</a>
|
||||
) : (
|
||||
<Typography variant="body1">{displayValue}</Typography>
|
||||
)}
|
||||
{value && (
|
||||
<Tooltip title="Copy ID">
|
||||
<IconButton
|
||||
|
||||
@@ -1,8 +1,56 @@
|
||||
import { Paper, Grid, Typography } from "@mui/material";
|
||||
import { Paper, Grid, Typography, Box } from "@mui/material";
|
||||
import { ReviewItemHeader } from "./ReviewItemHeader";
|
||||
import { CopyableField } from "@/app/_components/review/CopyableField";
|
||||
import { SubmissionInfo } from "@/app/ts/Submission";
|
||||
import { MapfixInfo } from "@/app/ts/Mapfix";
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import LaunchIcon from '@mui/icons-material/Launch'; // Import the icon
|
||||
|
||||
// New component to fetch and display submitter info
|
||||
function SubmitterInfo({ submitterId }: { submitterId: string | number }) {
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!submitterId) return;
|
||||
|
||||
const fetchUserName = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Use the internal proxy API route
|
||||
const response = await fetch(`/proxy/users/${submitterId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch user');
|
||||
}
|
||||
const data = await response.json();
|
||||
setName(`@${data.name}`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user name:", error);
|
||||
setName(String(submitterId)); // Fallback to ID on error
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserName();
|
||||
}, [submitterId]);
|
||||
|
||||
if (loading) {
|
||||
return <Typography variant="body1">Loading...</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={`https://www.roblox.com/users/${submitterId}/profile`} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, '&:hover': { textDecoration: 'underline' } }}>
|
||||
<Typography>
|
||||
{name || submitterId}
|
||||
</Typography>
|
||||
<LaunchIcon sx={{ fontSize: '1rem', color: 'text.secondary' }} />
|
||||
</Box>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// Define a field configuration for specific types
|
||||
interface FieldConfig {
|
||||
@@ -34,14 +82,14 @@ export function ReviewItem({
|
||||
if (isSubmission) {
|
||||
// Fields for Submission
|
||||
fields = [
|
||||
{ key: 'Submitter', label: 'Submitter ID' },
|
||||
{ key: 'Submitter', label: 'Submitter' },
|
||||
{ key: 'AssetID', label: 'Asset ID' },
|
||||
{ key: 'UploadedAssetID', label: 'Uploaded Asset ID' },
|
||||
];
|
||||
} else if (isMapfix) {
|
||||
// Fields for Mapfix
|
||||
fields = [
|
||||
{ key: 'Submitter', label: 'Submitter ID' },
|
||||
{ key: 'Submitter', label: 'Submitter' },
|
||||
{ key: 'AssetID', label: 'Asset ID' },
|
||||
{ key: 'TargetAssetID', label: 'Target Asset ID' },
|
||||
];
|
||||
@@ -58,16 +106,33 @@ export function ReviewItem({
|
||||
|
||||
{/* Item Details */}
|
||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||
{fields.map((field) => (
|
||||
<Grid item xs={12} sm={6} key={field.key}>
|
||||
<CopyableField
|
||||
label={field.label}
|
||||
value={(item as never)[field.key]}
|
||||
onCopy={handleCopyValue}
|
||||
placeholderText={field.placeholder}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
{fields.map((field) => {
|
||||
const fieldValue = (item as never)[field.key];
|
||||
|
||||
if (field.key === 'Submitter') {
|
||||
return (
|
||||
<Grid item xs={12} sm={6} key={field.key}>
|
||||
<Typography variant="subtitle2" color="text.secondary">{field.label}</Typography>
|
||||
<SubmitterInfo submitterId={fieldValue} />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
const displayValue = fieldValue === 0 || fieldValue == null ? 'N/A' : fieldValue;
|
||||
const isAssetId = field.key.includes('AssetID') && fieldValue !== 0 && fieldValue != null;
|
||||
|
||||
return (
|
||||
<Grid item xs={12} sm={6} key={field.key}>
|
||||
<CopyableField
|
||||
label={field.label}
|
||||
value={String(displayValue)}
|
||||
onCopy={handleCopyValue}
|
||||
placeholderText={field.placeholder}
|
||||
link={isAssetId ? `https://create.roblox.com/store/asset/${fieldValue}` : undefined}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
{/* Description Section */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Typography, Box, Avatar, keyframes} from "@mui/material";
|
||||
import {Typography, Box, keyframes} from "@mui/material";
|
||||
import { StatusChip } from "@/app/_components/statusChip";
|
||||
import { SubmissionStatus } from "@/app/ts/Submission";
|
||||
import { MapfixStatus } from "@/app/ts/Mapfix";
|
||||
@@ -13,7 +13,7 @@ interface ReviewItemHeaderProps {
|
||||
submitterId: number;
|
||||
}
|
||||
|
||||
export const ReviewItemHeader = ({ displayName, statusId, creator, submitterId }: ReviewItemHeaderProps) => {
|
||||
export const ReviewItemHeader = ({ displayName, statusId, creator }: ReviewItemHeaderProps) => {
|
||||
const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting]);
|
||||
|
||||
const pulse = keyframes`
|
||||
@@ -56,10 +56,6 @@ export const ReviewItemHeader = ({ displayName, statusId, creator, submitterId }
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<Avatar
|
||||
src={`/thumbnails/user/${submitterId}`}
|
||||
sx={{ mr: 1, width: 24, height: 24 }}
|
||||
/>
|
||||
<Typography variant="body1">
|
||||
by {creator || "Unknown Creator"}
|
||||
</Typography>
|
||||
|
||||
@@ -6,6 +6,7 @@ import GameSelection from "./_game";
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import Webpage from "@/app/_components/webpage"
|
||||
import React, { useState } from "react";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
import "./(styles)/page.scss"
|
||||
|
||||
@@ -20,6 +21,8 @@ interface IdResponse {
|
||||
}
|
||||
|
||||
export default function SubmissionInfoPage() {
|
||||
useTitle("Admin Submit");
|
||||
|
||||
const [game, setGame] = useState(1);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import "./globals.scss";
|
||||
import {theme} from "@/app/lib/theme";
|
||||
import {ThemeProvider} from "@mui/material";
|
||||
|
||||
@@ -26,6 +26,7 @@ import {ErrorDisplay} from "@/app/_components/ErrorDisplay";
|
||||
import ReviewButtons from "@/app/_components/review/ReviewButtons";
|
||||
import {useReviewData} from "@/app/hooks/useReviewData";
|
||||
import {MapfixInfo} from "@/app/ts/Mapfix";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
interface SnackbarState {
|
||||
open: boolean;
|
||||
@@ -50,7 +51,6 @@ export default function MapfixDetailsPage() {
|
||||
severity
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseSnackbar = () => {
|
||||
setSnackbar({
|
||||
...snackbar,
|
||||
@@ -74,6 +74,8 @@ export default function MapfixDetailsPage() {
|
||||
});
|
||||
const mapfix = mapfixData as MapfixInfo;
|
||||
|
||||
useTitle(mapfix ? `${mapfix.DisplayName} Mapfix` : 'Loading Mapfix...');
|
||||
|
||||
// Handle review button actions
|
||||
async function handleReviewAction(action: string, mapfixId: number) {
|
||||
try {
|
||||
@@ -179,6 +181,7 @@ export default function MapfixDetailsPage() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Webpage>
|
||||
<Container maxWidth="lg" sx={{ py: { xs: 3, md: 5 } }}>
|
||||
|
||||
@@ -15,8 +15,11 @@ import {
|
||||
} from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
export default function MapfixInfoPage() {
|
||||
useTitle("Map Fixes");
|
||||
|
||||
const [mapfixes, setMapfixes] = useState<MapfixList | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -19,6 +19,7 @@ import Webpage from "@/app/_components/webpage";
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import {MapInfo} from "@/app/ts/Map";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
interface MapfixPayload {
|
||||
AssetID: number;
|
||||
@@ -41,6 +42,8 @@ export default function MapfixInfoPage() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
|
||||
useTitle("Submit Mapfix");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMapDetails = async () => {
|
||||
try {
|
||||
|
||||
@@ -32,7 +32,8 @@ import BugReportIcon from "@mui/icons-material/BugReport";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import { hasRole, RolesConstants } from "@/app/ts/Roles";
|
||||
import {hasRole, RolesConstants} from "@/app/ts/Roles";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
export default function MapDetails() {
|
||||
const { mapId } = useParams();
|
||||
@@ -44,6 +45,8 @@ export default function MapDetails() {
|
||||
const [roles, setRoles] = useState(RolesConstants.Empty);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
useTitle(map ? `${map.DisplayName}` : 'Loading Map...');
|
||||
|
||||
useEffect(() => {
|
||||
async function getMap() {
|
||||
try {
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
import {Search as SearchIcon} from "@mui/icons-material";
|
||||
import Link from "next/link";
|
||||
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
import {thumbnailLoader} from '@/app/lib/thumbnailLoader';
|
||||
|
||||
interface Map {
|
||||
ID: number;
|
||||
@@ -36,6 +38,8 @@ interface Map {
|
||||
}
|
||||
|
||||
export default function MapsPage() {
|
||||
useTitle("Map Collection");
|
||||
|
||||
const router = useRouter();
|
||||
const [maps, setMaps] = useState<Map[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -259,6 +263,7 @@ export default function MapsPage() {
|
||||
{getGameName(map.GameID)}
|
||||
</Box>
|
||||
<Image
|
||||
loader={thumbnailLoader}
|
||||
src={`/thumbnails/asset/${map.ID}`}
|
||||
alt={map.DisplayName}
|
||||
fill
|
||||
|
||||
@@ -21,6 +21,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import PendingIcon from '@mui/icons-material/Pending';
|
||||
import Webpage from "@/app/_components/webpage";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
interface Operation {
|
||||
OperationID: number;
|
||||
@@ -41,7 +42,8 @@ export default function OperationStatusPage() {
|
||||
const [expandStatusMessage, setExpandStatusMessage] = useState(false);
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
|
||||
useTitle(`Operation ${operationId}`);
|
||||
useEffect(() => {
|
||||
if (!operationId) return;
|
||||
|
||||
|
||||
@@ -15,8 +15,11 @@ import {
|
||||
import Link from "next/link";
|
||||
import {SubmissionInfo, SubmissionList} from "@/app/ts/Submission";
|
||||
import {Carousel} from "@/app/_components/carousel";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
export default function Home() {
|
||||
useTitle("Home");
|
||||
|
||||
const [mapfixes, setMapfixes] = useState<MapfixList | null>(null);
|
||||
const [submissions, setSubmissions] = useState<SubmissionList | null>(null);
|
||||
const [isLoadingMapfixes, setIsLoadingMapfixes] = useState<boolean>(false);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {ErrorDisplay} from "@/app/_components/ErrorDisplay";
|
||||
import ReviewButtons from "@/app/_components/review/ReviewButtons";
|
||||
import {useReviewData} from "@/app/hooks/useReviewData";
|
||||
import {SubmissionInfo} from "@/app/ts/Submission";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
interface SnackbarState {
|
||||
open: boolean;
|
||||
@@ -73,6 +74,8 @@ export default function SubmissionDetailsPage() {
|
||||
});
|
||||
const submission = submissionData as SubmissionInfo;
|
||||
|
||||
useTitle(submission ? `${submission.DisplayName} Submission` : 'Loading Submission...');
|
||||
|
||||
// Handle review button actions
|
||||
async function handleReviewAction(action: string, submissionId: number) {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
'use client'
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { SubmissionList } from "../ts/Submission";
|
||||
@@ -15,8 +15,11 @@ import {
|
||||
} from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
export default function SubmissionInfoPage() {
|
||||
useTitle("Submissions");
|
||||
|
||||
const [submissions, setSubmissions] = useState<SubmissionList | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -18,6 +18,7 @@ import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
||||
import Webpage from "@/app/_components/webpage";
|
||||
import GameSelection from "./_game";
|
||||
import Link from "next/link";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
interface SubmissionPayload {
|
||||
AssetID: number;
|
||||
@@ -27,6 +28,8 @@ interface SubmissionPayload {
|
||||
}
|
||||
|
||||
export default function SubmitPage() {
|
||||
useTitle("Submit");
|
||||
|
||||
const [game, setGame] = useState(1);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function GET(
|
||||
|
||||
try {
|
||||
const mediaResponse = await fetch(
|
||||
`https://publish.roblox.com/v1/assets/${assetId}/media`
|
||||
`https://publish.roblox.com/v1/assets/${assetId}/media` // NOTE: This allows users to add custom images(their own thumbnail if they'd like) to their maps
|
||||
);
|
||||
if (mediaResponse.ok) {
|
||||
const mediaData = await mediaResponse.json();
|
||||
@@ -47,7 +47,6 @@ export async function GET(
|
||||
|
||||
// Redirect to the actual image URL instead of proxying
|
||||
return NextResponse.redirect(imageUrl);
|
||||
|
||||
} catch (err) {
|
||||
return errorImageResponse(500, {
|
||||
message: `Failed to fetch thumbnail URL: ${err}`,
|
||||
|
||||
Reference in New Issue
Block a user