├── .env.example ├── .eslintrc.json ├── src ├── lib │ ├── language │ │ ├── types │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── quantity.ts │ │ │ └── parse.ts │ │ ├── actions │ │ │ ├── change-lang.ts │ │ │ └── get-lang.ts │ │ ├── interfaces │ │ │ └── index.ts │ │ ├── dictionaries.ts │ │ └── components │ │ │ ├── language-changer.tsx │ │ │ └── language-provider.tsx │ ├── utils.ts │ ├── db.ts │ ├── date.ts │ └── punishment │ │ ├── player.ts │ │ ├── kick.ts │ │ ├── warn.ts │ │ ├── ban.ts │ │ ├── mute.ts │ │ └── punishment.ts ├── app │ ├── not-found.tsx │ ├── (app) │ │ ├── bans │ │ │ ├── page.tsx │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── kicks │ │ │ ├── page.tsx │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── mutes │ │ │ └── page.tsx │ │ ├── warns │ │ │ ├── page.tsx │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── history │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── [player] │ │ │ ├── bans │ │ │ └── page.tsx │ │ │ ├── kicks │ │ │ └── page.tsx │ │ │ ├── mutes │ │ │ └── page.tsx │ │ │ ├── warns │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── globals.css │ └── layout.tsx ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── tooltip.tsx │ │ ├── badge.tsx │ │ ├── scroll-area.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── table.tsx │ │ ├── pagination.tsx │ │ └── sheet.tsx │ ├── punishments │ │ ├── bans │ │ │ ├── bans-body-skeleton.tsx │ │ │ ├── bans-row-skeleton.tsx │ │ │ ├── bans-body-data.tsx │ │ │ └── bans-table.tsx │ │ ├── kicks │ │ │ ├── kicks-body-skeleton.tsx │ │ │ ├── kicks-row-skeleton.tsx │ │ │ ├── kicks-body-data.tsx │ │ │ └── kicks-table.tsx │ │ ├── mutes │ │ │ ├── mutes-body-skeleton.tsx │ │ │ ├── mutes-row-skeleton.tsx │ │ │ ├── mutes-body-data.tsx │ │ │ └── mutes-table.tsx │ │ ├── warns │ │ │ ├── warns-body-skeleton.tsx │ │ │ ├── warns-row-skeleton.tsx │ │ │ ├── warns-body-data.tsx │ │ │ └── warns-table.tsx │ │ ├── history │ │ │ ├── history-body-skeleton.tsx │ │ │ ├── history-row-skeleton.tsx │ │ │ ├── history-table.tsx │ │ │ └── history-body-data.tsx │ │ ├── relative-time-tooltip.tsx │ │ ├── punishment-status-dot.tsx │ │ └── punishment-type-card.tsx │ ├── theme │ │ ├── theme-provider.tsx │ │ ├── navbar.tsx │ │ └── theme-toggle.tsx │ ├── images │ │ └── website-logo.tsx │ ├── avatar │ │ ├── console-avatar.tsx │ │ ├── player-avatar.tsx │ │ ├── avatar-bust.tsx │ │ └── avatar-body.tsx │ ├── buttons │ │ ├── punishment-info-button.tsx │ │ └── github-button.tsx │ ├── layout │ │ ├── not-found.tsx │ │ ├── default-page.tsx │ │ ├── header │ │ │ ├── site-header.tsx │ │ │ ├── main-nav.tsx │ │ │ └── mobile-nav.tsx │ │ └── icons.tsx │ ├── table │ │ ├── avatar-name.tsx │ │ ├── filters.tsx │ │ ├── player-filter.tsx │ │ └── pagination.tsx │ ├── input │ │ └── player-lookup.tsx │ └── info │ │ └── punishment-info-card.tsx ├── utils │ ├── bedrock.ts │ ├── searchParams.ts │ └── date.ts ├── actions │ └── check-player.ts ├── types │ └── index.ts └── middleware.ts ├── public ├── logo.webp ├── console.webp ├── console-body.webp └── console-bust.webp ├── postcss.config.mjs ├── next.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json ├── tailwind.config.ts ├── config └── site.ts ├── language ├── en.js └── es.js ├── README.md └── prisma └── schema.prisma /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | SITE_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/language/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Dictionary = Record -------------------------------------------------------------------------------- /public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoSoyVillaa/next-litebans/HEAD/public/logo.webp -------------------------------------------------------------------------------- /public/console.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoSoyVillaa/next-litebans/HEAD/public/console.webp -------------------------------------------------------------------------------- /public/console-body.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoSoyVillaa/next-litebans/HEAD/public/console-body.webp -------------------------------------------------------------------------------- /public/console-bust.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoSoyVillaa/next-litebans/HEAD/public/console-bust.webp -------------------------------------------------------------------------------- /src/lib/language/utils/quantity.ts: -------------------------------------------------------------------------------- 1 | const quantity = (key: any, value: number) => { 2 | return value === 1 ? key.singular : key.plural; 3 | } 4 | 5 | export default quantity; -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { NotFound as NotFoundComponent } from "@/components/layout/not-found" 2 | 3 | export default function NotFound() { 4 | return ( 5 | 6 | ) 7 | } -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined; 5 | } 6 | 7 | export const db = globalThis.prisma || new PrismaClient(); 8 | 9 | if (process.env.NODE_ENV !== "production") globalThis.prisma = db; -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'minotar.net' 8 | } 9 | ] 10 | } 11 | }; 12 | 13 | export default nextConfig; 14 | -------------------------------------------------------------------------------- /src/lib/language/actions/change-lang.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { cookies } from "next/headers" 4 | 5 | export const changeLang = async (lang: string) => { 6 | const cookieStore = cookies() 7 | cookieStore.set({ 8 | name: "lang", 9 | value: lang, 10 | httpOnly: true, 11 | path: "/", 12 | }) 13 | } -------------------------------------------------------------------------------- /src/lib/language/actions/get-lang.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import {cookies} from "next/headers" 4 | 5 | import {siteConfig} from "@config/site" 6 | 7 | export const getLang = async (): Promise => { 8 | const cookieStore = cookies() 9 | return cookieStore.get("lang")?.value ?? siteConfig.languages.default 10 | } -------------------------------------------------------------------------------- /src/lib/language/utils/parse.ts: -------------------------------------------------------------------------------- 1 | const parse = (text: string, placeholders: Record = {}) => { 2 | let parsedText = text; 3 | Object.entries(placeholders).forEach(([key, value]) => { 4 | parsedText = parsedText.replace(new RegExp(`{${key}}`, 'g'), value); 5 | }); 6 | return parsedText; 7 | } 8 | 9 | export default parse; -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/components/punishments/bans/bans-body-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { TableBody } from "@/components/ui/table"; 2 | import { BansRowSkeleton } from "@/components/punishments/bans/bans-row-skeleton"; 3 | 4 | export const BansBodySkeleton = () => ( 5 | 6 | {Array.from({ length: 10 }).map((_, index) => ( 7 | 8 | ))} 9 | 10 | ) -------------------------------------------------------------------------------- /src/components/theme/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => { 8 | return {children} 9 | } -------------------------------------------------------------------------------- /src/lib/language/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | import { Dictionary } from "../types" 2 | 3 | export interface LanguageProviderProps extends React.PropsWithChildren { 4 | lang: string 5 | defaultLang: string 6 | dictionary: Dictionary 7 | } 8 | 9 | export interface UseLangProps { 10 | lang: string 11 | defaultLang: string 12 | dictionary: Dictionary 13 | setLang: React.Dispatch 14 | } -------------------------------------------------------------------------------- /src/components/punishments/kicks/kicks-body-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { TableBody } from "@/components/ui/table"; 2 | import { KicksRowSkeleton } from "@/components/punishments/kicks/kicks-row-skeleton"; 3 | 4 | export const KicksBodySkeleton = () => ( 5 | 6 | {Array.from({ length: 10 }).map((_, index) => ( 7 | 8 | ))} 9 | 10 | ) -------------------------------------------------------------------------------- /src/components/punishments/mutes/mutes-body-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { TableBody } from "@/components/ui/table"; 2 | import { MutesRowSkeleton } from "@/components/punishments/mutes/mutes-row-skeleton"; 3 | 4 | export const MutesBodySkeleton = () => ( 5 | 6 | {Array.from({ length: 10 }).map((_, index) => ( 7 | 8 | ))} 9 | 10 | ) -------------------------------------------------------------------------------- /src/components/punishments/warns/warns-body-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { TableBody } from "@/components/ui/table"; 2 | import { WarnsRowSkeleton } from "@/components/punishments/warns/warns-row-skeleton"; 3 | 4 | export const WarnsBodySkeleton = () => ( 5 | 6 | {Array.from({ length: 10 }).map((_, index) => ( 7 | 8 | ))} 9 | 10 | ) -------------------------------------------------------------------------------- /src/components/punishments/history/history-body-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { TableBody } from "@/components/ui/table"; 2 | import { HistoryRowSkeleton } from "@/components/punishments/history/history-row-skeleton"; 3 | 4 | export const HistoryBodySkeleton = () => ( 5 | 6 | {Array.from({ length: 10 }).map((_, index) => ( 7 | 8 | ))} 9 | 10 | ) -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/theme/navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { siteConfig } from "@config/site" 4 | 5 | export const NavBar = () => { 6 | return ( 7 |
8 | 9 | 10 | {siteConfig.title} 11 | 12 | 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /src/utils/bedrock.ts: -------------------------------------------------------------------------------- 1 | import { siteConfig } from "@config/site" 2 | 3 | const getSkinUUID = (playername: string, uuid: string) => { 4 | if (!siteConfig.bedrock.enabled) return uuid; 5 | 6 | if (playername.startsWith(siteConfig.bedrock.prefix)) { 7 | return "2bc90ee6-7b81-3a66-a268-f89d4472e8ec"; 8 | } 9 | 10 | return uuid; 11 | } 12 | 13 | const bedrockPrefixRegex = new RegExp(`^${siteConfig.bedrock.prefix}`); 14 | 15 | export { getSkinUUID, bedrockPrefixRegex } -------------------------------------------------------------------------------- /src/lib/date.ts: -------------------------------------------------------------------------------- 1 | import { siteConfig } from "@config/site"; 2 | import { format, formatDistance } from "date-fns"; 3 | import * as locales from "date-fns/locale"; 4 | 5 | type Locales = typeof locales; 6 | 7 | const formatDate = (date: Date) => { 8 | return format(date, siteConfig.openGraph.dateFormat); 9 | } 10 | 11 | const formatDuration = (start: Date, end: Date, lang: string) => { 12 | return formatDistance(start, end, { 13 | locale: locales[lang as keyof Locales] 14 | }); 15 | } 16 | 17 | export { formatDate, formatDuration } -------------------------------------------------------------------------------- /src/actions/check-player.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { db } from "@/lib/db"; 4 | 5 | export const checkPlayer = async (name: string) => { 6 | const player = await db.litebans_history.findFirst({ 7 | where: { 8 | name 9 | }, 10 | orderBy: { 11 | date: 'desc' 12 | }, 13 | select: { 14 | name: true, 15 | uuid: true 16 | } 17 | }); 18 | 19 | if (player && player.uuid === "CONSOLE") return { exists: false } 20 | 21 | return { 22 | exists: player ? true : false, 23 | name: player?.name 24 | }; 25 | } -------------------------------------------------------------------------------- /src/components/images/website-logo.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from "next/image"; 4 | 5 | import { siteConfig } from "@config/site"; 6 | 7 | interface WebsiteLogoProps { 8 | height: number; 9 | width: number; 10 | className?: string; 11 | } 12 | 13 | export const WebsiteLogo = ({ height, width, className }: WebsiteLogoProps) => { 14 | return ( 15 | {`${siteConfig.title} 22 | ) 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /src/components/avatar/console-avatar.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { siteConfig } from "@config/site"; 5 | 6 | interface ConsoleAvatarProps { 7 | name: string; 8 | size?: number; 9 | className?: string; 10 | } 11 | 12 | export const ConsoleAvatar = ({ 13 | name, 14 | size, 15 | className 16 | }: ConsoleAvatarProps) => ( 17 | {`${name}'s 24 | ) -------------------------------------------------------------------------------- /src/components/avatar/player-avatar.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface PlayerAvatarProps { 6 | name: string; 7 | uuid?: string; 8 | size?: number; 9 | className?: string; 10 | } 11 | 12 | export const PlayerAvatar = ({ 13 | name, 14 | uuid, 15 | size, 16 | className 17 | }: PlayerAvatarProps) => ( 18 | {`${name}'s 25 | ) -------------------------------------------------------------------------------- /src/components/buttons/punishment-info-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { LuExternalLink } from "react-icons/lu"; 3 | 4 | import { PunishmentType } from "@/types"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | 8 | interface PunishmentInfoButtonProps { 9 | type: PunishmentType, 10 | id: string 11 | } 12 | 13 | export const PunishmentInfoButton = ({ 14 | type, 15 | id 16 | }: PunishmentInfoButtonProps) => ( 17 | 18 | 21 | 22 | ) -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type SearchParams = { 2 | searchParams: { [key: string]: string | string[] | undefined } 3 | } 4 | 5 | const PUNISMENT_TYPES = { 6 | BAN: "ban", 7 | MUTE: "mute", 8 | WARN: "warn", 9 | KICK: "kick" 10 | } as const 11 | 12 | export type PunishmentType = typeof PUNISMENT_TYPES[keyof typeof PUNISMENT_TYPES] 13 | 14 | export type PunishmentListItem = { 15 | id: bigint | number 16 | uuid: string | null 17 | reason: string | null 18 | banned_by_uuid: string 19 | banned_by_name: string | null 20 | time: bigint | number 21 | until: bigint | number 22 | active: boolean | string 23 | type?: PunishmentType 24 | } -------------------------------------------------------------------------------- /src/components/buttons/github-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | import { Icons } from "@/components/layout/icons"; 6 | import { buttonVariants } from "@/components/ui/button"; 7 | 8 | export const GithubButton = () => ( 9 | 14 |
22 | 23 | GitHub 24 |
25 | 26 | ) -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { siteConfig } from "@config/site"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export function middleware(request: NextRequest) { 5 | let lang = request.cookies.get("lang")?.value 6 | 7 | if (!lang || !siteConfig.languages.available.includes(lang)) { 8 | lang = siteConfig.languages.default 9 | const response = NextResponse.rewrite(request.url) 10 | response.cookies.set({ 11 | name: "lang", 12 | value: lang, 13 | httpOnly: true, 14 | path: "/", 15 | }) 16 | return response 17 | } 18 | 19 | return 20 | } 21 | 22 | export const config = { 23 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 24 | } -------------------------------------------------------------------------------- /src/components/avatar/avatar-bust.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { siteConfig } from "@config/site"; 3 | import { getSkinUUID } from "@/utils/bedrock"; 4 | 5 | interface AvatarBustProps { 6 | name: string; 7 | uuid: string; 8 | console?: boolean; 9 | className?: string; 10 | } 11 | 12 | export const AvatarBust = ({ name, uuid, console, className }: AvatarBustProps) => ( 13 | // eslint-disable-next-line @next/next/no-img-element 14 | {name} 21 | ) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"], 22 | "@config/*": ["./config/*"], 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/components/avatar/avatar-body.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { siteConfig } from "@config/site"; 3 | import { getSkinUUID } from "@/utils/bedrock"; 4 | 5 | interface AvatarBodyProps { 6 | name: string; 7 | uuid: string; 8 | console?: boolean; 9 | className?: string; 10 | } 11 | 12 | export const AvatarBody = ({ name, uuid, console, className }: AvatarBodyProps) => ( 13 | // eslint-disable-next-line @next/next/no-img-element 14 | {name} 21 | ) -------------------------------------------------------------------------------- /src/lib/language/dictionaries.ts: -------------------------------------------------------------------------------- 1 | import "server-only" 2 | 3 | import { getLang } from "./actions/get-lang" 4 | import { siteConfig } from "@config/site" 5 | import { Dictionary } from "./types" 6 | 7 | const dictionaries: Record = {} 8 | 9 | siteConfig.languages.available.forEach(async (lang) => { 10 | const dictionary = await import(`../../../language/${lang}.js`) 11 | dictionaries[lang] = dictionary.default 12 | }) 13 | 14 | const getDictionary = (lang: string) => dictionaries[lang] 15 | 16 | const getDictionaries = () => dictionaries 17 | 18 | const language = async () => { 19 | const lang = await getLang() 20 | const dictionary = getDictionary(lang) 21 | 22 | return { 23 | lang, 24 | dictionary 25 | } 26 | } 27 | 28 | export { getDictionary, getDictionaries, language } -------------------------------------------------------------------------------- /src/components/layout/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | import { useLang } from "@/lib/language/components/language-provider"; 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { DefaultPage } from "@/components/layout/default-page"; 9 | 10 | export const NotFound = () => { 11 | const router = useRouter(); 12 | const { dictionary } = useLang(); 13 | 14 | const handleBack = () => { 15 | router.push("/"); 16 | } 17 | 18 | return ( 19 | 23 | 26 | 27 | ) 28 | } -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/punishments/relative-time-tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { getRelativeDifference, getRelativeDifferenceText } from "@/utils/date"; 2 | 3 | import { 4 | Tooltip, 5 | TooltipContent, 6 | TooltipProvider, 7 | TooltipTrigger 8 | } from "@/components/ui/tooltip"; 9 | 10 | interface RelativeTimeTooltipProps { 11 | lang: string 12 | time: Date | string 13 | } 14 | 15 | export const RelativeTimeTooltip = ({ 16 | lang, 17 | time 18 | }: RelativeTimeTooltipProps) => ( 19 | 20 | 21 | 22 | {time.toLocaleString(lang)} 23 | 24 | 25 | {time instanceof Date && ( 26 |

{getRelativeDifferenceText(lang, getRelativeDifference(time))}

27 | )} 28 |
29 |
30 |
31 | ) -------------------------------------------------------------------------------- /src/utils/searchParams.ts: -------------------------------------------------------------------------------- 1 | import { SearchParams } from "@/types"; 2 | import { siteConfig } from "@config/site"; 3 | 4 | const getPage = ({ searchParams }: SearchParams) => { 5 | let page = searchParams.page ? parseInt(searchParams.page as string) : 1; 6 | 7 | if (isNaN(page) || page < 1) { 8 | page = 1; 9 | } 10 | 11 | return page; 12 | } 13 | 14 | const getPlayer = ({ searchParams }: SearchParams) => { 15 | const player = searchParams.player as string; 16 | if (!/^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i.test(player)) { 17 | return undefined; 18 | } 19 | 20 | return player; 21 | } 22 | 23 | const getStaff = ({ searchParams }: SearchParams) => { 24 | const staff = searchParams.staff as string; 25 | if (!/^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i.test(staff) && staff !== siteConfig.console.uuid) { 26 | return undefined; 27 | } 28 | 29 | return staff; 30 | } 31 | 32 | export { getPage, getPlayer, getStaff } -------------------------------------------------------------------------------- /src/components/punishments/kicks/kicks-row-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { TableCell, TableRow } from "@/components/ui/table"; 3 | 4 | export const KicksRowSkeleton = () => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner" 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 28 | ) 29 | } 30 | 31 | export { Toaster } 32 | -------------------------------------------------------------------------------- /src/components/layout/default-page.tsx: -------------------------------------------------------------------------------- 1 | import Balance from "react-wrap-balancer"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface DefaultPageProps { 6 | title: string; 7 | description: string; 8 | padding?: string; 9 | className?: string; 10 | children: React.ReactNode; 11 | } 12 | 13 | export const DefaultPage = ({ 14 | title, 15 | description, 16 | padding, 17 | className, 18 | children, 19 | }: DefaultPageProps) => { 20 | return ( 21 | <> 22 |
23 |

24 | {title} 25 |

26 | 27 | 28 | {description} 29 | 30 | 31 |
32 | {children} 33 |
34 |
35 | 36 | ); 37 | } -------------------------------------------------------------------------------- /src/components/punishments/warns/warns-row-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { TableCell, TableRow } from "@/components/ui/table"; 3 | 4 | export const WarnsRowSkeleton = () => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | const getRelativeDifference = (d: Date) => { 2 | const diff = -((new Date().getTime() - d.getTime())/1000)|0; 3 | const absDiff = Math.abs(diff); 4 | if (absDiff > 86400*30*10) { 5 | return { duration: Math.round(diff/(86400*365)), unit: 'years' }; 6 | } 7 | if (absDiff > 86400*25) { 8 | return { duration: Math.round(diff/(86400*30)), unit: 'months' }; 9 | } 10 | if (absDiff > 3600*21) { 11 | return { duration: Math.round(diff/86400), unit: 'days' }; 12 | } 13 | if (absDiff > 60*44) { 14 | return { duration: Math.round(diff/3600), unit: 'hours' }; 15 | } 16 | if (absDiff > 30) { 17 | return { duration: Math.round(diff/60), unit: 'minutes' }; 18 | } 19 | return { duration: diff, unit: 'seconds' }; 20 | } 21 | 22 | const getRelativeDifferenceText = (lang: string, difference: { duration: number, unit: string }) => { 23 | const formatter = new Intl.RelativeTimeFormat(lang, { numeric: 'auto', style: 'long'}); 24 | return formatter.format(difference.duration, difference.unit as Intl.RelativeTimeFormatUnit); 25 | } 26 | 27 | export { getRelativeDifference, getRelativeDifferenceText }; -------------------------------------------------------------------------------- /src/components/punishments/punishment-status-dot.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { Dictionary } from "@/lib/language/types" 3 | 4 | import { 5 | Tooltip, 6 | TooltipContent, 7 | TooltipProvider, 8 | TooltipTrigger 9 | } from "@/components/ui/tooltip"; 10 | 11 | interface PunishmentStatusDotProps { 12 | dictionary: Dictionary 13 | status: boolean | undefined 14 | } 15 | 16 | export const PunishmentStatusDot = ({ 17 | dictionary, 18 | status 19 | }: PunishmentStatusDotProps) => ( 20 | 21 | 22 | 23 | 31 | 32 | 33 | {status === undefined ? dictionary.table.active.temporal : (status ? dictionary.table.active.true : dictionary.table.active.false)} 34 | 35 | 36 | 37 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 David Villarreal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/punishments/bans/bans-row-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { TableCell, TableRow } from "@/components/ui/table"; 3 | 4 | export const BansRowSkeleton = () => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 | ) -------------------------------------------------------------------------------- /src/components/punishments/mutes/mutes-row-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { TableCell, TableRow } from "@/components/ui/table"; 3 | 4 | export const MutesRowSkeleton = () => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 | ) -------------------------------------------------------------------------------- /src/components/table/avatar-name.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link"; 4 | import { useCallback } from "react"; 5 | import { usePathname, useSearchParams } from "next/navigation"; 6 | 7 | import { PlayerAvatar } from "@/components/avatar/player-avatar"; 8 | import { ConsoleAvatar } from "@/components/avatar/console-avatar"; 9 | 10 | interface AvatarName { 11 | query: "player" | "staff"; 12 | name: string; 13 | uuid: string; 14 | console?: boolean; 15 | } 16 | 17 | export const AvatarName = ({ query, name, uuid, console }: AvatarName) => { 18 | 19 | const searchParams = useSearchParams(); 20 | const pathname = usePathname(); 21 | 22 | const createQueryString = useCallback( 23 | (name: string, value: string) => { 24 | const params = new URLSearchParams(searchParams.toString()) 25 | params.set(name, value) 26 | params.delete("page") 27 | 28 | return params.toString() 29 | }, 30 | [searchParams] 31 | ) 32 | 33 | return ( 34 | 35 | {console ? 36 | 37 | : 38 | 39 | } 40 |

{name}

41 | 42 | ); 43 | } -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )) 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 31 | -------------------------------------------------------------------------------- /src/components/table/filters.tsx: -------------------------------------------------------------------------------- 1 | import { siteConfig } from "@config/site"; 2 | import { getPlayerName } from "@/lib/punishment/punishment"; 3 | 4 | import { PlayerFilter } from "./player-filter"; 5 | 6 | interface FiltersProps { 7 | player?: string; 8 | staff?: string; 9 | } 10 | 11 | export const Filters = async ({ player, staff }: FiltersProps) => { 12 | 13 | let playerName; 14 | if (player) { 15 | playerName = await getPlayerName(player); 16 | } 17 | 18 | let staffName; 19 | if (staff) { 20 | if (staff === siteConfig.console.uuid) staffName = siteConfig.console.name; 21 | else staffName = await getPlayerName(staff); 22 | } 23 | 24 | return ( 25 | <> 26 | {(player || staff) && ( 27 |
28 | {player && ( 29 | 34 | )} 35 | {staff && ( 36 | 42 | )} 43 |
44 | )} 45 | 46 | ) 47 | } -------------------------------------------------------------------------------- /src/lib/punishment/player.ts: -------------------------------------------------------------------------------- 1 | import { db } from "../db"; 2 | 3 | const getPlayerByName = async (name: string) => { 4 | const player = await db.litebans_history.findFirst({ 5 | where: { 6 | name 7 | }, 8 | orderBy: { 9 | date: 'desc' 10 | }, 11 | select: { 12 | uuid: true, 13 | name: true 14 | } 15 | }); 16 | 17 | return player; 18 | } 19 | 20 | const getPlayerBanCount = async (uuid: string) => { 21 | const count = await db.litebans_bans.count({ 22 | where: { 23 | uuid 24 | } 25 | }); 26 | 27 | return count; 28 | } 29 | 30 | const getPlayerMuteCount = async (uuid: string) => { 31 | const count = await db.litebans_mutes.count({ 32 | where: { 33 | uuid 34 | } 35 | }); 36 | 37 | return count; 38 | } 39 | 40 | const getPlayerWarnCount = async (uuid: string) => { 41 | const count = await db.litebans_warnings.count({ 42 | where: { 43 | uuid 44 | } 45 | }); 46 | 47 | return count; 48 | } 49 | 50 | const getPlayerKickCount = async (uuid: string) => { 51 | const count = await db.litebans_kicks.count({ 52 | where: { 53 | uuid 54 | } 55 | }); 56 | 57 | return count; 58 | } 59 | 60 | export { getPlayerByName, getPlayerBanCount, getPlayerMuteCount, getPlayerWarnCount, getPlayerKickCount} -------------------------------------------------------------------------------- /src/components/punishments/history/history-row-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { TableCell, TableRow } from "@/components/ui/table"; 3 | 4 | export const HistoryRowSkeleton = () => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 | ) -------------------------------------------------------------------------------- /src/components/theme/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Moon, Sun } from "lucide-react" 5 | import { useTheme } from "next-themes" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu" 14 | 15 | export const ThemeToggle = () => { 16 | const { setTheme } = useTheme() 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ) 40 | } -------------------------------------------------------------------------------- /src/components/layout/header/site-header.tsx: -------------------------------------------------------------------------------- 1 | import { LanguageChanger } from "@/lib/language/components/language-changer"; 2 | import { getDictionaries } from "@/lib/language/dictionaries"; 3 | import { getPunishmentCount } from "@/lib/punishment/punishment"; 4 | 5 | import { ThemeToggle } from "@/components/theme/theme-toggle"; 6 | import { PlayerInput } from "@/components/input/player-lookup"; 7 | 8 | import { MainNav } from "./main-nav"; 9 | import { MobileNav } from "./mobile-nav"; 10 | import { GithubButton } from "@/components/buttons/github-button"; 11 | 12 | export const SiteHeader = async () => { 13 | const dictionaries = getDictionaries(); 14 | const { bans, mutes, warns, kicks } = await getPunishmentCount(); 15 | 16 | return( 17 |
18 |
19 | 20 | 21 |
22 | 28 |
29 |
30 |
31 | ) 32 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-litebans", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "setup:db:generate": "prisma generate", 11 | "setup:db:pull": "prisma db pull" 12 | }, 13 | "dependencies": { 14 | "@prisma/client": "^5.13.0", 15 | "@radix-ui/react-dialog": "^1.0.5", 16 | "@radix-ui/react-dropdown-menu": "^2.0.6", 17 | "@radix-ui/react-icons": "^1.3.0", 18 | "@radix-ui/react-scroll-area": "^1.0.5", 19 | "@radix-ui/react-separator": "^1.0.3", 20 | "@radix-ui/react-slot": "^1.0.2", 21 | "@radix-ui/react-tooltip": "^1.0.7", 22 | "class-variance-authority": "^0.7.0", 23 | "clsx": "^2.1.1", 24 | "date-fns": "^3.6.0", 25 | "lucide-react": "^0.378.0", 26 | "motion-number": "^0.1.6", 27 | "next": "14.2.25", 28 | "next-themes": "^0.3.0", 29 | "react": "^18", 30 | "react-country-flag": "^3.1.0", 31 | "react-dom": "^18", 32 | "react-icons": "^5.2.1", 33 | "react-wrap-balancer": "^1.1.0", 34 | "sharp": "^0.33.3", 35 | "sonner": "^1.4.41", 36 | "tailwind-merge": "^2.3.0", 37 | "tailwindcss-animate": "^1.0.7" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "^20", 41 | "@types/react": "^18", 42 | "@types/react-dom": "^18", 43 | "eslint": "^8", 44 | "eslint-config-next": "14.2.3", 45 | "postcss": "^8", 46 | "prisma": "^5.13.0", 47 | "tailwindcss": "^3.4.1", 48 | "typescript": "^5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/(app)/bans/page.tsx: -------------------------------------------------------------------------------- 1 | import { SearchParams } from "@/types"; 2 | import { siteConfig } from "@config/site"; 3 | import p from "@/lib/language/utils/parse"; 4 | import { getBanCount } from "@/lib/punishment/ban"; 5 | import { language } from "@/lib/language/dictionaries"; 6 | 7 | import { DefaultPage } from "@/components/layout/default-page"; 8 | import { getPage, getPlayer, getStaff } from "@/utils/searchParams"; 9 | import { BansTable } from "@/components/punishments/bans/bans-table"; 10 | 11 | export async function generateMetadata() { 12 | 13 | const { dictionary } = await language(); 14 | 15 | const banCount = await getBanCount(); 16 | 17 | return { 18 | title: dictionary.pages.bans.title, 19 | openGraph: { 20 | images: process.env.SITE_URL + siteConfig.logo, 21 | description: p(siteConfig.openGraph.pages.bans.description, { 22 | total: banCount 23 | }) 24 | } 25 | } 26 | } 27 | 28 | export default async function Bans(searchParams: SearchParams) { 29 | const dictionary = await (await language()).dictionary.pages.bans; 30 | 31 | const page = getPage(searchParams); 32 | const player = getPlayer(searchParams); 33 | const staff = getStaff(searchParams); 34 | 35 | const banCount = await getBanCount(player, staff); 36 | 37 | return ( 38 | 45 | 46 | 47 | ); 48 | } -------------------------------------------------------------------------------- /src/app/(app)/kicks/page.tsx: -------------------------------------------------------------------------------- 1 | import { SearchParams } from "@/types"; 2 | import { siteConfig } from "@config/site"; 3 | import p from "@/lib/language/utils/parse"; 4 | import { getKickCount } from "@/lib/punishment/kick"; 5 | import { language } from "@/lib/language/dictionaries"; 6 | 7 | import { DefaultPage } from "@/components/layout/default-page"; 8 | import { getPage, getPlayer, getStaff } from "@/utils/searchParams"; 9 | import { KicksTable } from "@/components/punishments/kicks/kicks-table"; 10 | 11 | export async function generateMetadata() { 12 | 13 | const { dictionary } = await language(); 14 | 15 | const kickCount = await getKickCount(); 16 | 17 | return { 18 | title: dictionary.pages.kicks.title, 19 | openGraph: { 20 | images: process.env.SITE_URL + siteConfig.logo, 21 | description: p(siteConfig.openGraph.pages.kicks.description, { 22 | total: kickCount 23 | }) 24 | } 25 | } 26 | } 27 | 28 | export default async function Kicks(searchParams: SearchParams) { 29 | const dictionary = (await language()).dictionary.pages.kicks; 30 | 31 | const page = getPage(searchParams); 32 | const player = getPlayer(searchParams); 33 | const staff = getStaff(searchParams); 34 | 35 | const kickCount = await getKickCount(player, staff); 36 | 37 | return ( 38 | 45 | 46 | 47 | ); 48 | } -------------------------------------------------------------------------------- /src/app/(app)/mutes/page.tsx: -------------------------------------------------------------------------------- 1 | import { SearchParams } from "@/types"; 2 | import { siteConfig } from "@config/site"; 3 | import p from "@/lib/language/utils/parse"; 4 | import { getMuteCount } from "@/lib/punishment/mute"; 5 | import { language } from "@/lib/language/dictionaries"; 6 | import { getPage, getPlayer, getStaff } from "@/utils/searchParams"; 7 | 8 | import { DefaultPage } from "@/components/layout/default-page"; 9 | import { MutesTable } from "@/components/punishments/mutes/mutes-table"; 10 | 11 | export async function generateMetadata() { 12 | 13 | const { dictionary } = await language(); 14 | 15 | const muteCount = await getMuteCount(); 16 | 17 | return { 18 | title: dictionary.pages.mutes.title, 19 | openGraph: { 20 | images: process.env.SITE_URL + siteConfig.logo, 21 | description: p(siteConfig.openGraph.pages.mutes.description, { 22 | total: muteCount 23 | }) 24 | } 25 | } 26 | } 27 | 28 | export default async function Mutes(searchParams: SearchParams) { 29 | const dictionary = (await language()).dictionary.pages.mutes; 30 | 31 | const page = getPage(searchParams); 32 | const player = getPlayer(searchParams); 33 | const staff = getStaff(searchParams); 34 | 35 | const muteCount = await getMuteCount(player, staff); 36 | 37 | return ( 38 | 45 | 46 | 47 | ); 48 | } -------------------------------------------------------------------------------- /src/app/(app)/warns/page.tsx: -------------------------------------------------------------------------------- 1 | import { SearchParams } from "@/types"; 2 | import { siteConfig } from "@config/site"; 3 | import p from "@/lib/language/utils/parse"; 4 | import { getWarnCount } from "@/lib/punishment/warn"; 5 | import { language } from "@/lib/language/dictionaries"; 6 | import { getPage, getPlayer, getStaff } from "@/utils/searchParams"; 7 | 8 | 9 | import { DefaultPage } from "@/components/layout/default-page"; 10 | import { WarnsTable } from "@/components/punishments/warns/warns-table"; 11 | 12 | export async function generateMetadata() { 13 | 14 | const { dictionary } = await language(); 15 | 16 | const warnCount = await getWarnCount(); 17 | 18 | return { 19 | title: dictionary.pages.warns.title, 20 | openGraph: { 21 | images: process.env.SITE_URL + siteConfig.logo, 22 | description: p(siteConfig.openGraph.pages.warns.description, { 23 | total: warnCount 24 | }) 25 | } 26 | } 27 | } 28 | 29 | export default async function Warns(searchParams: SearchParams) { 30 | const dictionary = (await language()).dictionary.pages.warns; 31 | 32 | const page = getPage(searchParams); 33 | const player = getPlayer(searchParams); 34 | const staff = getStaff(searchParams); 35 | 36 | const warnCount = await getWarnCount(player, staff); 37 | 38 | return ( 39 | 46 | 47 | 48 | ); 49 | } -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | ban: "border-transparent bg-red-500/75 shadow text-primary-foreground dark:text-primary", 19 | mute: "border-transparent bg-gray-500/75 shadow text-primary-foreground dark:text-primary", 20 | warn: "border-transparent bg-yellow-500/75 shadow text-primary-foreground dark:text-primary", 21 | kick: "border-transparent bg-sky-600/75 shadow text-primary-foreground dark:text-primary", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | }, 27 | } 28 | ) 29 | 30 | export interface BadgeProps 31 | extends React.HTMLAttributes, 32 | VariantProps {} 33 | 34 | function Badge({ className, variant, ...props }: BadgeProps) { 35 | return ( 36 |
37 | ) 38 | } 39 | 40 | export { Badge, badgeVariants } 41 | -------------------------------------------------------------------------------- /src/components/punishments/punishment-type-card.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link"; 4 | import { useEffect, useState } from "react"; 5 | import MotionNumber, { DEFAULT_TRANSITION } from 'motion-number' 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | import { 10 | Card, 11 | CardContent, 12 | CardTitle 13 | } from "@/components/ui/card"; 14 | 15 | interface PunishmentTypeCardProps { 16 | title: string; 17 | fromGradient: string; 18 | count: number; 19 | href: string; 20 | punishmentIcon: JSX.Element; 21 | } 22 | 23 | export const PunishmentTypeCard = ({ 24 | title, 25 | fromGradient, 26 | count, 27 | href, 28 | punishmentIcon, 29 | }: PunishmentTypeCardProps) => { 30 | const [value, setValue] = useState(0); 31 | 32 | useEffect(() => { 33 | setValue(count); 34 | }, [count]); 35 | 36 | return ( 37 | 40 | 43 | {punishmentIcon} 44 | 45 | 51 | {title} 52 | 53 |
54 |
55 | 56 | ) 57 | }; -------------------------------------------------------------------------------- /src/components/punishments/kicks/kicks-body-data.tsx: -------------------------------------------------------------------------------- 1 | import { getKicks, sanitizeKicks } from "@/lib/punishment/kick"; 2 | 3 | import { AvatarName } from "@/components/table/avatar-name"; 4 | import { PunishmentInfoButton } from "@/components/buttons/punishment-info-button"; 5 | import { RelativeTimeTooltip } from "@/components/punishments/relative-time-tooltip"; 6 | import { 7 | TableBody, 8 | TableCell, 9 | TableRow, 10 | } from "@/components/ui/table" 11 | 12 | interface KicksBodyDataProps { 13 | language: string; 14 | page: number; 15 | player?: string; 16 | staff?: string; 17 | } 18 | 19 | export const KicksBodyData = async ({ 20 | language, 21 | page, 22 | player, 23 | staff 24 | }: KicksBodyDataProps) => { 25 | 26 | const dbKicks = await getKicks(page, player, staff); 27 | const kicks = await sanitizeKicks(dbKicks); 28 | 29 | return ( 30 | 31 | {kicks.map((kick) => ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {kick.reason} 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ))} 50 | 51 | ) 52 | } -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --primary: 240 5.9% 10%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 5.9% 90%; 32 | --input: 240 5.9% 90%; 33 | --ring: 240 10% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 240 10% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 240 10% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 240 10% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | 57 | --accent: 240 3.7% 15.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 240 3.7% 15.9%; 64 | --input: 240 3.7% 15.9%; 65 | --ring: 240 4.9% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | td, th { 77 | @apply !px-2; 78 | } 79 | } -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /src/lib/language/components/language-changer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; 4 | import { useLang } from "./language-provider"; 5 | import { Button } from "@/components/ui/button"; 6 | import ReactCountryFlag from "react-country-flag"; 7 | import { siteConfig } from "@config/site"; 8 | import { Dictionary } from "../types"; 9 | 10 | export const LanguageChanger = ({dictionaries} : {dictionaries: Record}) => { 11 | const { lang, dictionary, setLang } = useLang(); 12 | 13 | return ( 14 | 15 | 16 | 29 | 30 | 31 | {siteConfig.languages.available.map((newLang) => ( 32 | { 33 | if (lang != newLang) setLang(newLang); 34 | }}> 35 | 44 | {dictionaries[newLang].info.lang_name} 45 | 46 | ))} 47 | 48 | 49 | ) 50 | } -------------------------------------------------------------------------------- /src/app/(app)/history/page.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { SearchParams } from "@/types"; 3 | import p from "@/lib/language/utils/parse"; 4 | import { language } from "@/lib/language/dictionaries"; 5 | import { getPunishmentCount } from "@/lib/punishment/punishment"; 6 | import { getPage, getPlayer, getStaff } from "@/utils/searchParams"; 7 | 8 | import { DefaultPage } from "@/components/layout/default-page"; 9 | import { HistoryTable } from "@/components/punishments/history/history-table"; 10 | import { siteConfig } from "@config/site"; 11 | 12 | export async function generateMetadata() { 13 | 14 | const { dictionary } = await language(); 15 | 16 | const banCount = await db.litebans_bans.count(); 17 | const muteCount = await db.litebans_mutes.count(); 18 | const warnCount = await db.litebans_warnings.count(); 19 | const kickCount = await db.litebans_kicks.count(); 20 | 21 | return { 22 | title: dictionary.pages.history.title, 23 | openGraph: { 24 | images: process.env.SITE_URL + siteConfig.logo, 25 | description: p(siteConfig.openGraph.pages.history.description, { 26 | bans: banCount, 27 | mutes: muteCount, 28 | warns: warnCount, 29 | kicks: kickCount, 30 | total: banCount + muteCount + warnCount + kickCount 31 | }) 32 | } 33 | } 34 | } 35 | 36 | export default async function History(searchParams: SearchParams) { 37 | const dictionary = (await language()).dictionary.pages.history; 38 | 39 | 40 | const page = getPage(searchParams); 41 | const player = getPlayer(searchParams); 42 | const staff = getStaff(searchParams); 43 | 44 | const punishmentCount = await getPunishmentCount(player, staff).then(({ bans, mutes, warns, kicks }) => bans + mutes + warns + kicks); 45 | 46 | return ( 47 | 54 | 55 | 56 | ); 57 | } -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | icon_sm: "h-8 w-8", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | export interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ({ className, variant, size, asChild = false, ...props }, ref) => { 46 | const Comp = asChild ? Slot : "button" 47 | return ( 48 | 53 | ) 54 | } 55 | ) 56 | Button.displayName = "Button" 57 | 58 | export { Button, buttonVariants } 59 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | 3 | import { Inter } from "next/font/google"; 4 | 5 | import { siteConfig } from "@config/site"; 6 | import { language } from "@/lib/language/dictionaries"; 7 | import { LanguageProvider } from "@/lib/language/components/language-provider"; 8 | 9 | import { Toaster } from "@/components/ui/sonner"; 10 | import { ThemeProvider } from "@/components/theme/theme-provider"; 11 | import { SiteHeader } from "@/components/layout/header/site-header"; 12 | 13 | const inter = Inter({ subsets: ["latin"] }); 14 | 15 | export async function generateMetadata() { 16 | 17 | const { dictionary } = await language(); 18 | 19 | return { 20 | title: { 21 | template: `%s | ${siteConfig.title}`, 22 | default: siteConfig.title, 23 | }, 24 | description: dictionary.site.description, 25 | openGraph: { 26 | images: process.env.SITE_URL + siteConfig.logo, 27 | }, 28 | twitter: { 29 | card: "summary", 30 | creator: "@yosoyvillaa" 31 | } 32 | } 33 | } 34 | 35 | export default async function RootLayout({ 36 | children 37 | }: Readonly<{ 38 | children: React.ReactNode; 39 | }>) { 40 | 41 | const { lang, dictionary} = await language(); 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | 56 |
57 |
58 | 59 |
{children}
60 |
61 |
62 | 63 |
64 | 65 | 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/components/table/player-filter.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useCallback } from "react"; 4 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 5 | 6 | import { CrossCircledIcon } from "@radix-ui/react-icons"; 7 | 8 | import { useLang } from "@/lib/language/components/language-provider"; 9 | 10 | import { Badge } from "@/components/ui/badge"; 11 | import { Button } from "@/components/ui/button"; 12 | import { Separator } from "@/components/ui/separator"; 13 | import { PlayerAvatar } from "@/components/avatar/player-avatar"; 14 | import { ConsoleAvatar } from "@/components/avatar/console-avatar"; 15 | 16 | interface PlayerFilter { 17 | type: "player" | "staff"; 18 | name: string; 19 | uuid?: string; 20 | console?: boolean; 21 | } 22 | 23 | export const PlayerFilter = ({ type, name, uuid, console }: PlayerFilter) => { 24 | const { dictionary } = useLang(); 25 | const searchParams = useSearchParams(); 26 | const pathname = usePathname(); 27 | const router = useRouter(); 28 | 29 | const createRemovedQueryString = useCallback( 30 | (name: string) => { 31 | const params = new URLSearchParams(searchParams.toString()) 32 | params.delete(name) 33 | params.delete("page") 34 | 35 | return params.toString() 36 | }, 37 | [searchParams]) 38 | 39 | const handleClick = () => { 40 | const query = createRemovedQueryString(type); 41 | if (query === "") { 42 | router.push(pathname); 43 | router.refresh(); 44 | } else { 45 | router.push(`${pathname}?${query}`); 46 | } 47 | } 48 | 49 | return ( 50 | 59 | ) 60 | } -------------------------------------------------------------------------------- /src/components/punishments/warns/warns-body-data.tsx: -------------------------------------------------------------------------------- 1 | import { FaCheck } from "react-icons/fa6"; 2 | import { FaTimes } from "react-icons/fa"; 3 | 4 | import { getWarns, sanitizeWarns } from "@/lib/punishment/warn"; 5 | 6 | import { AvatarName } from "@/components/table/avatar-name"; 7 | import { RelativeTimeTooltip } from "@/components/punishments/relative-time-tooltip"; 8 | import { PunishmentInfoButton } from "@/components/buttons/punishment-info-button"; 9 | import { 10 | TableBody, 11 | TableCell, 12 | TableRow, 13 | } from "@/components/ui/table" 14 | 15 | interface WarnsBodyDataProps { 16 | language: string; 17 | page: number; 18 | player?: string; 19 | staff?: string; 20 | } 21 | 22 | export const WarnsBodyData = async ({ 23 | language, 24 | page, 25 | player, 26 | staff 27 | }: WarnsBodyDataProps) => { 28 | 29 | const dbWarns = await getWarns(page, player, staff); 30 | const warns = await sanitizeWarns(dbWarns); 31 | 32 | return ( 33 | 34 | {warns.map((warn) => ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {warn.reason} 44 | 45 | 46 | 47 | 48 | 49 | {warn.warned ? 50 | 51 | : 52 | 53 | } 54 | 55 | 56 | 57 | 58 | 59 | ))} 60 | 61 | ) 62 | } -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /src/components/punishments/kicks/kicks-table.tsx: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { Suspense } from "react"; 4 | 5 | import { getKickCount } from "@/lib/punishment/kick"; 6 | import { language } from "@/lib/language/dictionaries"; 7 | 8 | import { Filters } from "@/components/table/filters"; 9 | import { TablePagination } from "@/components/table/pagination"; 10 | import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; 11 | import { KicksBodySkeleton } from "@/components/punishments/kicks/kicks-body-skeleton"; 12 | import { KicksBodyData } from "@/components/punishments/kicks/kicks-body-data"; 13 | import { 14 | Table, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from "@/components/ui/table" 19 | 20 | interface KicksTableProps { 21 | page: number; 22 | player?: string; 23 | staff?: string; 24 | } 25 | 26 | export const KicksTable = async ({ 27 | page, 28 | player, 29 | staff 30 | }: KicksTableProps) => { 31 | 32 | const { lang, dictionary } = await language(); 33 | const localDictionary = dictionary.pages.kicks; 34 | 35 | const kickCount = await getKickCount(player, staff); 36 | const totalPages = Math.ceil(kickCount / 10); 37 | 38 | return ( 39 |
40 | 41 | 42 | 43 | 44 | 45 | {localDictionary.table.heads.player} 46 | {localDictionary.table.heads.by} 47 | {localDictionary.table.heads.reason} 48 | {localDictionary.table.heads.date} 49 | 50 | 51 | 52 | }> 53 | 54 | 55 |
56 | 57 |
58 | 59 |
60 | ); 61 | }; -------------------------------------------------------------------------------- /src/components/punishments/bans/bans-body-data.tsx: -------------------------------------------------------------------------------- 1 | import { Dictionary } from "@/lib/language/types"; 2 | import { getBans, sanitizeBans } from "@/lib/punishment/ban"; 3 | 4 | import { AvatarName } from "@/components/table/avatar-name"; 5 | import { PunishmentInfoButton } from "@/components/buttons/punishment-info-button"; 6 | import { PunishmentStatusDot } from "@/components/punishments/punishment-status-dot"; 7 | import { RelativeTimeTooltip } from "@/components/punishments/relative-time-tooltip"; 8 | import { 9 | TableBody, 10 | TableCell, 11 | TableRow, 12 | } from "@/components/ui/table" 13 | 14 | interface BansBodyDataProps { 15 | language: string; 16 | dictionary: Dictionary; 17 | page: number; 18 | player?: string; 19 | staff?: string; 20 | } 21 | 22 | export const BansBodyData = async ({ 23 | language, 24 | dictionary, 25 | page, 26 | player, 27 | staff 28 | }: BansBodyDataProps) => { 29 | 30 | const localDictionary = dictionary.pages.bans; 31 | const dbBans = await getBans(page, player, staff); 32 | const bans = await sanitizeBans(localDictionary, dbBans); 33 | 34 | return ( 35 | 36 | {bans.map((ban) => ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {ban.reason} 46 | 47 | 48 | 49 | 50 | 51 |

52 | 53 | 54 |

55 |
56 | 57 | 58 | 59 |
60 | ))} 61 |
62 | ) 63 | } -------------------------------------------------------------------------------- /src/components/punishments/bans/bans-table.tsx: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { Suspense } from "react"; 4 | 5 | import { getBanCount } from "@/lib/punishment/ban"; 6 | import { language } from "@/lib/language/dictionaries"; 7 | 8 | import { Filters } from "@/components/table/filters"; 9 | import { TablePagination } from "@/components/table/pagination"; 10 | import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; 11 | import { BansBodyData } from "@/components/punishments/bans/bans-body-data"; 12 | import { BansBodySkeleton } from "@/components/punishments/bans/bans-body-skeleton"; 13 | import { 14 | Table, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from "@/components/ui/table" 19 | 20 | interface BansTableProps { 21 | page: number; 22 | player?: string; 23 | staff?: string; 24 | } 25 | 26 | export const BansTable = async ({ 27 | page, 28 | player, 29 | staff 30 | }: BansTableProps) => { 31 | 32 | const { lang, dictionary } = await language(); 33 | const localDictionary = dictionary.pages.bans; 34 | 35 | const banCount = await getBanCount(player, staff); 36 | const totalPages = Math.ceil(banCount / 10); 37 | 38 | return ( 39 |
40 | 41 | 42 | 43 | 44 | 45 | {localDictionary.table.heads.player} 46 | {localDictionary.table.heads.by} 47 | {localDictionary.table.heads.reason} 48 | {localDictionary.table.heads.date} 49 | {localDictionary.table.heads.expires} 50 | 51 | 52 | 53 | }> 54 | 55 | 56 |
57 | 58 |
59 | 60 |
61 | ); 62 | }; -------------------------------------------------------------------------------- /src/components/punishments/mutes/mutes-body-data.tsx: -------------------------------------------------------------------------------- 1 | import { Dictionary } from "@/lib/language/types"; 2 | import { getMutes, sanitizeMutes } from "@/lib/punishment/mute"; 3 | 4 | import { AvatarName } from "@/components/table/avatar-name"; 5 | import { PunishmentInfoButton } from "@/components/buttons/punishment-info-button"; 6 | import { PunishmentStatusDot } from "@/components/punishments/punishment-status-dot"; 7 | import { RelativeTimeTooltip } from "@/components/punishments/relative-time-tooltip"; 8 | import { 9 | TableBody, 10 | TableCell, 11 | TableRow, 12 | } from "@/components/ui/table" 13 | 14 | interface MutesBodyDataProps { 15 | language: string; 16 | dictionary: Dictionary; 17 | page: number; 18 | player?: string; 19 | staff?: string; 20 | } 21 | 22 | export const MutesBodyData = async ({ 23 | language, 24 | dictionary, 25 | page, 26 | player, 27 | staff 28 | }: MutesBodyDataProps) => { 29 | 30 | const localDictionary = dictionary.pages.mutes; 31 | const dbMutes = await getMutes(page, player, staff); 32 | const mutes = await sanitizeMutes(localDictionary, dbMutes); 33 | 34 | return ( 35 | 36 | {mutes.map((mute) => ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {mute.reason} 46 | 47 | 48 | 49 | 50 | 51 |

52 | 53 | 54 |

55 |
56 | 57 | 58 | 59 |
60 | ))} 61 |
62 | ) 63 | } -------------------------------------------------------------------------------- /src/components/punishments/mutes/mutes-table.tsx: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { Suspense } from "react"; 4 | 5 | import { getMuteCount } from "@/lib/punishment/mute"; 6 | import { language } from "@/lib/language/dictionaries"; 7 | 8 | import { Filters } from "@/components/table/filters"; 9 | import { TablePagination } from "@/components/table/pagination"; 10 | import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; 11 | import { MutesBodySkeleton } from "@/components/punishments/mutes/mutes-body-skeleton"; 12 | import { MutesBodyData } from "@/components/punishments/mutes/mutes-body-data"; 13 | import { 14 | Table, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from "@/components/ui/table" 19 | 20 | interface MutesTableProps { 21 | page: number; 22 | player?: string; 23 | staff?: string; 24 | } 25 | 26 | export const MutesTable = async ({ 27 | page, 28 | player, 29 | staff 30 | }: MutesTableProps) => { 31 | 32 | const { lang, dictionary } = await language(); 33 | const localDictionary = dictionary.pages.mutes; 34 | 35 | const muteCount = await getMuteCount(player, staff); 36 | const totalPages = Math.ceil(muteCount / 10); 37 | 38 | return ( 39 |
40 | 41 | 42 | 43 | 44 | 45 | {localDictionary.table.heads.player} 46 | {localDictionary.table.heads.by} 47 | {localDictionary.table.heads.reason} 48 | {localDictionary.table.heads.date} 49 | {localDictionary.table.heads.expires} 50 | 51 | 52 | 53 | }> 54 | 55 | 56 |
57 | 58 |
59 | 60 |
61 | ); 62 | }; -------------------------------------------------------------------------------- /src/components/punishments/warns/warns-table.tsx: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { Suspense } from "react"; 4 | 5 | import { language } from "@/lib/language/dictionaries"; 6 | import { getWarnCount } from "@/lib/punishment/warn"; 7 | 8 | import { Filters } from "@/components/table/filters"; 9 | import { TablePagination } from "@/components/table/pagination"; 10 | import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; 11 | import { WarnsBodySkeleton } from "@/components/punishments/warns/warns-body-skeleton"; 12 | import { WarnsBodyData } from "@/components/punishments/warns/warns-body-data"; 13 | import { 14 | Table, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from "@/components/ui/table" 19 | 20 | interface WarnsTableProps { 21 | page: number; 22 | player?: string; 23 | staff?: string; 24 | } 25 | 26 | export const WarnsTable = async ({ 27 | page, 28 | player, 29 | staff 30 | }: WarnsTableProps) => { 31 | 32 | const { lang, dictionary } = await language(); 33 | const localDictionary = dictionary.pages.warns; 34 | 35 | const warnCount = await getWarnCount(player, staff); 36 | const totalPages = Math.ceil(warnCount / 10); 37 | 38 | return ( 39 |
40 | 41 | 42 | 43 | 44 | 45 | {localDictionary.table.heads.player} 46 | {localDictionary.table.heads.by} 47 | {localDictionary.table.heads.reason} 48 | {localDictionary.table.heads.date} 49 | {localDictionary.table.heads.notified} 50 | 51 | 52 | 53 | }> 54 | 55 | 56 |
57 | 58 |
59 | 60 |
61 | ); 62 | }; -------------------------------------------------------------------------------- /src/lib/punishment/kick.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | 3 | import { siteConfig } from "@config/site"; 4 | import { PunishmentListItem } from "@/types"; 5 | 6 | import { db } from "../db"; 7 | import { getPlayerName } from "./punishment"; 8 | 9 | const getKickCount = async (player?: string, staff?: string) => { 10 | const count = await db.litebans_kicks.count({ 11 | where: { 12 | uuid: player, 13 | banned_by_uuid: staff 14 | } 15 | }); 16 | return count; 17 | } 18 | 19 | const getKicks = async (page: number, player?: string, staff?: string) => { 20 | const kicks = await db.litebans_kicks.findMany({ 21 | where: { 22 | uuid: player, 23 | banned_by_uuid: staff 24 | }, 25 | take: 10, 26 | skip: (page - 1) * 10, 27 | select: { 28 | id: true, 29 | uuid: true, 30 | banned_by_name: true, 31 | banned_by_uuid: true, 32 | reason: true, 33 | time: true, 34 | until: true, 35 | active: true 36 | }, 37 | orderBy: { 38 | time: "desc" 39 | } 40 | }); 41 | 42 | return kicks; 43 | } 44 | 45 | const sanitizeKicks = async (kicks: PunishmentListItem[]) => { 46 | 47 | const sanitized = await Promise.all(kicks.map(async (kick) => { 48 | const name = await getPlayerName(kick.uuid!); 49 | return { 50 | ...kick, 51 | id: kick.id.toString(), 52 | time: new Date(parseInt(kick.time.toString())), 53 | console: kick.banned_by_uuid === siteConfig.console.uuid, 54 | active: typeof kick.active === "boolean" ? kick.active : kick.active === "1", 55 | name 56 | } 57 | })); 58 | 59 | return sanitized; 60 | } 61 | 62 | const getKick = async (id: number) => { 63 | const kick = await db.litebans_kicks.findUnique({ 64 | where: { 65 | id 66 | }, 67 | select: { 68 | id: true, 69 | uuid: true, 70 | banned_by_name: true, 71 | banned_by_uuid: true, 72 | reason: true, 73 | time: true, 74 | until: true, 75 | active: true, 76 | server_origin: true, 77 | } 78 | }); 79 | 80 | if (!kick) { 81 | return null; 82 | } 83 | 84 | const sanitized = (await sanitizeKicks([kick]))[0]; 85 | 86 | return { 87 | ...sanitized, 88 | server: kick.server_origin 89 | } 90 | } 91 | 92 | const getCachedKick = cache( 93 | async (id: number) => getKick(id) 94 | ); 95 | 96 | export { getKickCount, getKicks, sanitizeKicks, getKick, getCachedKick } -------------------------------------------------------------------------------- /src/components/punishments/history/history-table.tsx: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { Suspense } from "react"; 4 | 5 | import { language } from "@/lib/language/dictionaries"; 6 | import { getPunishmentCount } from "@/lib/punishment/punishment"; 7 | 8 | import { Filters } from "@/components/table/filters"; 9 | import { TablePagination } from "@/components/table/pagination"; 10 | import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; 11 | import { HistoryBodyData } from "@/components/punishments/history/history-body-data"; 12 | import { HistoryBodySkeleton } from "@/components/punishments/history/history-body-skeleton"; 13 | import { 14 | Table, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from "@/components/ui/table" 19 | 20 | interface HistoryTableProps { 21 | page: number; 22 | player?: string; 23 | staff?: string; 24 | } 25 | 26 | export const HistoryTable = async ({ 27 | page, 28 | player, 29 | staff 30 | }: HistoryTableProps) => { 31 | 32 | const { lang, dictionary } = await language(); 33 | const localDictionary = dictionary.pages.history; 34 | 35 | const punishmentCount = await getPunishmentCount(player, staff).then(({ bans, mutes, warns, kicks }) => bans + mutes + warns + kicks); 36 | const totalPages = Math.ceil(punishmentCount / 10); 37 | 38 | return ( 39 |
40 | 41 | 42 | 43 | 44 | 45 | {localDictionary.table.heads.type} 46 | {localDictionary.table.heads.player} 47 | {localDictionary.table.heads.by} 48 | {localDictionary.table.heads.reason} 49 | {localDictionary.table.heads.date} 50 | {localDictionary.table.heads.expires} 51 | 52 | 53 | 54 | }> 55 | 56 | 57 |
58 | 59 |
60 | 61 |
62 | ); 63 | }; -------------------------------------------------------------------------------- /src/lib/punishment/warn.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | 3 | import { siteConfig } from "@config/site"; 4 | import { PunishmentListItem } from "@/types"; 5 | 6 | import { db } from "../db"; 7 | import { getPlayerName } from "./punishment"; 8 | 9 | const getWarnCount = async (player?: string, staff?: string) => { 10 | const count = await db.litebans_warnings.count({ 11 | where: { 12 | uuid: player, 13 | banned_by_uuid: staff 14 | } 15 | }); 16 | return count; 17 | } 18 | 19 | const getWarns = async (page: number, player?: string, staff?: string) => { 20 | const warns = await db.litebans_warnings.findMany({ 21 | where: { 22 | uuid: player, 23 | banned_by_uuid: staff 24 | }, 25 | take: 10, 26 | skip: (page - 1) * 10, 27 | select: { 28 | id: true, 29 | uuid: true, 30 | banned_by_name: true, 31 | banned_by_uuid: true, 32 | reason: true, 33 | time: true, 34 | until: true, 35 | active: true, 36 | warned: true 37 | }, 38 | orderBy: { 39 | time: "desc" 40 | } 41 | }); 42 | 43 | return warns; 44 | } 45 | 46 | const sanitizeWarns = async (warns: (PunishmentListItem & { warned: boolean | string})[]) => { 47 | 48 | const sanitized = await Promise.all(warns.map(async (warn) => { 49 | const name = await getPlayerName(warn.uuid!); 50 | return { 51 | ...warn, 52 | id: warn.id.toString(), 53 | time: new Date(parseInt(warn.time.toString())), 54 | console: warn.banned_by_uuid === siteConfig.console.uuid, 55 | active: typeof warn.active === "boolean" ? warn.active : warn.active === "1", 56 | warned: typeof warn.warned === "boolean" ? warn.warned : warn.warned === "1", 57 | name 58 | } 59 | })); 60 | 61 | return sanitized; 62 | } 63 | 64 | const getWarn = async (id: number) => { 65 | const warn = await db.litebans_warnings.findUnique({ 66 | where: { 67 | id 68 | }, 69 | select: { 70 | id: true, 71 | uuid: true, 72 | banned_by_name: true, 73 | banned_by_uuid: true, 74 | reason: true, 75 | time: true, 76 | until: true, 77 | active: true, 78 | server_origin: true, 79 | warned: true 80 | } 81 | }); 82 | 83 | if (!warn) { 84 | return null; 85 | } 86 | 87 | const sanitized = (await sanitizeWarns([warn]))[0]; 88 | 89 | return { 90 | ...sanitized, 91 | server: warn.server_origin 92 | } 93 | } 94 | 95 | const getCachedWarn = cache( 96 | async (id: number) => getWarn(id) 97 | ); 98 | 99 | export { getWarnCount, getWarns, sanitizeWarns, getWarn, getCachedWarn } -------------------------------------------------------------------------------- /src/components/input/player-lookup.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { toast } from "sonner" 4 | import { useRouter } from "next/navigation" 5 | import { useState, useTransition } from "react" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { siteConfig } from "@config/site" 9 | import { checkPlayer } from "@/actions/check-player" 10 | import { useLang } from "@/lib/language/components/language-provider" 11 | 12 | import { Input } from "@/components/ui/input" 13 | import { PlayerAvatar } from "@/components/avatar/player-avatar" 14 | import { bedrockPrefixRegex } from "@/utils/bedrock" 15 | 16 | export const PlayerInput = () => { 17 | const [playerName, setPlayerName] = useState("") 18 | const [iconPlayer, setIconPlayer] = useState("") 19 | const [isPending, startTransition] = useTransition() 20 | const [isError, setError] = useState(false) 21 | const router = useRouter() 22 | 23 | const { dictionary } = useLang() 24 | 25 | const handlePlayerNameChange = (event: React.ChangeEvent) => { 26 | setError(false) 27 | setPlayerName(event.target.value) 28 | 29 | if (siteConfig.bedrock.enabled) { 30 | if (event.target.value.startsWith(siteConfig.bedrock.prefix)) { 31 | setIconPlayer(event.target.value.replace(bedrockPrefixRegex, "")) 32 | } else { 33 | setIconPlayer(event.target.value) 34 | } 35 | } else { 36 | setIconPlayer(event.target.value) 37 | } 38 | } 39 | 40 | const handleEnter = (event: React.KeyboardEvent) => { 41 | if (playerName === "") return 42 | 43 | if (event.key === "Enter") { 44 | startTransition(() => { 45 | checkPlayer(playerName).then(({ exists, name }) => { 46 | setPlayerName("") 47 | if (exists) { 48 | router.push(`/@${name}`) 49 | } else { 50 | setError(true) 51 | toast.error(dictionary.notifications.playerNotFound.title, { 52 | description: dictionary.notifications.playerNotFound.description 53 | }) 54 | } 55 | }) 56 | }) 57 | } 58 | } 59 | 60 | return ( 61 |
62 | 63 | 71 |
72 | ) 73 | } -------------------------------------------------------------------------------- /src/components/layout/header/main-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { usePathname } from "next/navigation" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { siteConfig } from "@config/site" 8 | 9 | import { Badge } from "@/components/ui/badge" 10 | import { WebsiteLogo } from "@/components/images/website-logo" 11 | import { useLang } from "@/lib/language/components/language-provider" 12 | 13 | interface MainNavProps { 14 | bans: number; 15 | mutes: number; 16 | warns: number; 17 | kicks: number; 18 | } 19 | 20 | export const MainNav = ({ bans, mutes, warns, kicks}: MainNavProps) => { 21 | const pathname = usePathname() 22 | const { dictionary } = useLang() 23 | 24 | return ( 25 |
26 | 27 | 31 | 32 | {siteConfig.title} 33 | 34 | 35 | 64 |
65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/language/components/language-provider.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | "use client" 3 | 4 | import { siteConfig } from "@config/site"; 5 | import { changeLang } from "../actions/change-lang"; 6 | 7 | import React from "react"; 8 | import { useRouter } from "next/navigation"; 9 | import { LanguageProviderProps, UseLangProps } from "../interfaces"; 10 | 11 | const LanguageContext = React.createContext(undefined); 12 | const defaultContext = { lang: siteConfig.languages.default, defaultLang: siteConfig.languages.default, dictionary: {} as Record, setLang: () => {} }; 13 | 14 | export const useLang = () => React.useContext(LanguageContext) ?? defaultContext; 15 | 16 | export const LanguageProvider = (props: LanguageProviderProps) => { 17 | const context = React.useContext(LanguageContext); 18 | 19 | if (context) return props.children; 20 | return 21 | } 22 | 23 | const Lang = (props: LanguageProviderProps) => { 24 | 25 | const [lang, setLangState] = React.useState(props.lang); 26 | 27 | const router = useRouter(); 28 | 29 | React.useEffect(() => { 30 | try { 31 | const localLang = localStorage.getItem("lang") 32 | 33 | if (!localLang) { 34 | localStorage.setItem("lang", props.lang) 35 | } 36 | 37 | if (localLang && localLang != props.lang) { 38 | setLocalLang(props.lang) 39 | setLangState(props.lang) 40 | } 41 | } catch (e) { 42 | // Unsupported 43 | } 44 | }, []) 45 | 46 | const setLocalLang = React.useCallback((newLang: string) => { 47 | try { 48 | localStorage.setItem("lang", newLang) 49 | } catch (e) { 50 | // Unsupported 51 | } 52 | }, []) 53 | 54 | const setLang = React.useCallback((newLang: string) => { 55 | React.startTransition(() => { 56 | changeLang(newLang).then(() => { 57 | setLangState(newLang); 58 | setLocalLang(newLang); 59 | router.refresh(); 60 | }) 61 | }) 62 | }, [lang]); 63 | 64 | // localStorage event handling 65 | React.useEffect(() => { 66 | const handleStorage = (e: StorageEvent) => { 67 | if (e.key !== "lang") { 68 | return 69 | } 70 | 71 | if (e.newValue === lang) { 72 | return 73 | } 74 | 75 | const newLang = e.newValue || props.lang 76 | setLang(newLang) 77 | } 78 | 79 | window.addEventListener('storage', handleStorage) 80 | return () => window.removeEventListener('storage', handleStorage) 81 | }, [setLang, props.lang]) 82 | 83 | const providerValue = React.useMemo(() => ({ 84 | lang, 85 | defaultLang: props.defaultLang, 86 | dictionary: props.dictionary, 87 | setLang 88 | }), [lang, props.defaultLang, props.dictionary, setLang]) 89 | 90 | return ( 91 | 92 | {props.children} 93 | 94 | ) 95 | } -------------------------------------------------------------------------------- /src/components/punishments/history/history-body-data.tsx: -------------------------------------------------------------------------------- 1 | import { Dictionary } from "@/lib/language/types"; 2 | import { getPunishments, sanitizePunishments } from "@/lib/punishment/punishment"; 3 | 4 | import { Badge } from "@/components/ui/badge"; 5 | import { AvatarName } from "@/components/table/avatar-name"; 6 | import { PunishmentInfoButton } from "@/components/buttons/punishment-info-button"; 7 | import { PunishmentStatusDot } from "@/components/punishments/punishment-status-dot"; 8 | import { RelativeTimeTooltip } from "@/components/punishments/relative-time-tooltip"; 9 | import { 10 | TableBody, 11 | TableCell, 12 | TableRow, 13 | } from "@/components/ui/table" 14 | 15 | interface HistoryBodyDataProps { 16 | language: string; 17 | dictionary: Dictionary; 18 | page: number; 19 | player?: string; 20 | staff?: string; 21 | } 22 | 23 | export const HistoryBodyData = async ({ 24 | language, 25 | dictionary, 26 | page, 27 | player, 28 | staff 29 | }: HistoryBodyDataProps) => { 30 | 31 | const localDictionary = dictionary.pages.history; 32 | const dBPunishments = await getPunishments(page, player, staff); 33 | const punishments = await sanitizePunishments(localDictionary, dBPunishments); 34 | 35 | return ( 36 | 37 | {punishments.map((punishment) => ( 38 | 39 | 40 | 41 | {dictionary.words[`${punishment.type!}s`].singular.toUpperCase()} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {punishment.reason} 52 | 53 | 54 | 55 | 56 | 57 | { (punishment.type == "ban" || punishment.type == "mute") ? 58 |

59 | 60 | 61 |

62 | :

{localDictionary.table.expire_not_applicable}

63 | } 64 |
65 | 66 | 67 | 68 |
69 | ))} 70 |
71 | ) 72 | } -------------------------------------------------------------------------------- /src/lib/punishment/ban.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | 3 | import { siteConfig } from "@config/site"; 4 | import { PunishmentListItem } from "@/types"; 5 | 6 | import { db } from "../db"; 7 | import { getPlayerName } from "./punishment"; 8 | import { Dictionary } from "../language/types"; 9 | 10 | const getBanCount = async (player?: string, staff?: string) => { 11 | const count = await db.litebans_bans.count({ 12 | where: { 13 | uuid: player, 14 | banned_by_uuid: staff 15 | } 16 | }); 17 | return count; 18 | } 19 | 20 | const getBans = async (page: number, player?: string, staff?: string) => { 21 | const bans = (await db.litebans_bans.findMany({ 22 | where: { 23 | uuid: player, 24 | banned_by_uuid: staff 25 | }, 26 | take: 10, 27 | skip: (page - 1) * 10, 28 | select: { 29 | id: true, 30 | uuid: true, 31 | banned_by_name: true, 32 | banned_by_uuid: true, 33 | reason: true, 34 | time: true, 35 | until: true, 36 | active: true 37 | }, 38 | orderBy: { 39 | time: "desc" 40 | } 41 | })); 42 | 43 | return bans; 44 | } 45 | 46 | const sanitizeBans = async (dictionary: Dictionary, bans: PunishmentListItem[]) => { 47 | 48 | const sanitized = await Promise.all(bans.map(async (ban) => { 49 | const name = await getPlayerName(ban.uuid!); 50 | const until = ban.until.toString() === "0" ? dictionary.table.permanent : new Date(parseInt(ban.until.toString())); 51 | const active = typeof ban.active === "boolean" ? ban.active : ban.active === "1"; 52 | return { 53 | ...ban, 54 | id: ban.id.toString(), 55 | time: new Date(parseInt(ban.time.toString())), 56 | status: until == dictionary.table.permanent ? 57 | active : 58 | until < new Date() ? false : undefined, 59 | console: ban.banned_by_uuid === siteConfig.console.uuid, 60 | permanent: until == dictionary.table.permanent, 61 | active, 62 | until, 63 | name, 64 | } 65 | })); 66 | 67 | return sanitized; 68 | } 69 | 70 | const getBan = async (id: number, dictionary: Dictionary) => { 71 | const ban = await db.litebans_bans.findUnique({ 72 | where: { 73 | id 74 | }, 75 | select: { 76 | id: true, 77 | uuid: true, 78 | banned_by_name: true, 79 | banned_by_uuid: true, 80 | reason: true, 81 | time: true, 82 | until: true, 83 | ipban: true, 84 | active: true, 85 | server_origin: true 86 | } 87 | }); 88 | 89 | if (!ban) { 90 | return null; 91 | } 92 | 93 | const sanitized = (await sanitizeBans(dictionary, [ban]))[0]; 94 | 95 | return { 96 | ...sanitized, 97 | ipban: ban.ipban, 98 | server: ban.server_origin 99 | } 100 | } 101 | 102 | const getCachedBan = cache( 103 | async (id: number, dictionary: Dictionary) => getBan(id, dictionary) 104 | ); 105 | 106 | export { getBanCount, getBans, sanitizeBans, getBan, getCachedBan } -------------------------------------------------------------------------------- /src/lib/punishment/mute.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | 3 | import { siteConfig } from "@config/site"; 4 | import { PunishmentListItem } from "@/types"; 5 | 6 | import { db } from "../db"; 7 | import { getPlayerName } from "./punishment"; 8 | import { Dictionary } from "../language/types"; 9 | 10 | const getMuteCount = async (player?: string, staff?: string) => { 11 | const count = await db.litebans_mutes.count({ 12 | where: { 13 | uuid: player, 14 | banned_by_uuid: staff 15 | } 16 | }); 17 | return count; 18 | } 19 | 20 | const getMutes = async (page: number, player?: string, staff?: string) => { 21 | const mutes = await db.litebans_mutes.findMany({ 22 | where: { 23 | uuid: player, 24 | banned_by_uuid: staff 25 | }, 26 | take: 10, 27 | skip: (page - 1) * 10, 28 | select: { 29 | id: true, 30 | uuid: true, 31 | banned_by_name: true, 32 | banned_by_uuid: true, 33 | reason: true, 34 | time: true, 35 | until: true, 36 | active: true 37 | }, 38 | orderBy: { 39 | time: "desc" 40 | } 41 | }); 42 | 43 | return mutes; 44 | } 45 | 46 | const sanitizeMutes = async (dictionary: Dictionary, mutes: PunishmentListItem[]) => { 47 | 48 | const sanitized = await Promise.all(mutes.map(async (mute) => { 49 | const name = await getPlayerName(mute.uuid!); 50 | const until = mute.until.toString() === "0" ? dictionary.table.permanent : new Date(parseInt(mute.until.toString())); 51 | const active = typeof mute.active === "boolean" ? mute.active : mute.active === "1"; 52 | return { 53 | ...mute, 54 | id: mute.id.toString(), 55 | time: new Date(parseInt(mute.time.toString())), 56 | status: until == dictionary.table.permanent ? 57 | (active ? true : false) : 58 | (until < new Date() ? false : undefined), 59 | console: mute.banned_by_uuid === siteConfig.console.uuid, 60 | permanent: until == dictionary.table.permanent, 61 | active, 62 | until, 63 | name 64 | } 65 | })); 66 | 67 | return sanitized; 68 | } 69 | 70 | const getMute = async (id: number, dictionary: Dictionary) => { 71 | const mute = await db.litebans_mutes.findUnique({ 72 | where: { 73 | id 74 | }, 75 | select: { 76 | id: true, 77 | uuid: true, 78 | banned_by_name: true, 79 | banned_by_uuid: true, 80 | reason: true, 81 | time: true, 82 | until: true, 83 | ipban: true, 84 | active: true, 85 | server_origin: true 86 | } 87 | }); 88 | 89 | if (!mute) { 90 | return null; 91 | } 92 | 93 | const sanitized = (await sanitizeMutes(dictionary, [mute]))[0]; 94 | 95 | return { 96 | ...sanitized, 97 | ipban: mute.ipban, 98 | server: mute.server_origin 99 | } 100 | } 101 | 102 | const getCachedMute = cache( 103 | async (id: number, dictionary: Dictionary) => getMute(id, dictionary) 104 | ); 105 | 106 | export { getMuteCount, getMutes, sanitizeMutes, getMute, getCachedMute } -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | shake: { 71 | '0%': { transform: 'translateX(0)' }, 72 | '25%': { transform: 'translateX(-5px)' }, 73 | '50%': { transform: 'translateX(5px)' }, 74 | '75%': { transform: 'translateX(-5px)' }, 75 | '100%': { transform: 'translateX(0)' }, 76 | '0%, 100%': { transform: 'translateX(0)' }, 77 | '10%, 30%, 50%, 70%, 90%': { transform: 'translateX(-3px)' }, 78 | '20%, 40%, 60%, 80%': { transform: 'translateX(3px)' }, 79 | '80%': { transform: 'translateX(1px)' }, 80 | '90%': { transform: 'translateX(-1px)' }, 81 | }, 82 | }, 83 | animation: { 84 | "accordion-down": "accordion-down 0.2s ease-out", 85 | "accordion-up": "accordion-up 0.2s ease-out", 86 | "shake": "shake 0.5s ease-in-out both", 87 | }, 88 | }, 89 | }, 90 | plugins: [require("tailwindcss-animate")], 91 | } satisfies Config 92 | 93 | export default config -------------------------------------------------------------------------------- /src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | 93 | )) 94 | TableCell.displayName = "TableCell" 95 | 96 | const TableCaption = React.forwardRef< 97 | HTMLTableCaptionElement, 98 | React.HTMLAttributes 99 | >(({ className, ...props }, ref) => ( 100 |
105 | )) 106 | TableCaption.displayName = "TableCaption" 107 | 108 | export { 109 | Table, 110 | TableHeader, 111 | TableBody, 112 | TableFooter, 113 | TableHead, 114 | TableRow, 115 | TableCell, 116 | TableCaption, 117 | } 118 | -------------------------------------------------------------------------------- /src/components/info/punishment-info-card.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | "use server" 3 | 4 | import Link from "next/link"; 5 | 6 | import { getSkinUUID } from "@/utils/bedrock"; 7 | import { language } from "@/lib/language/dictionaries"; 8 | 9 | import { AvatarBody } from "@/components/avatar/avatar-body"; 10 | import { AvatarBust } from "@/components/avatar/avatar-bust"; 11 | import { ConsoleAvatar } from "@/components/avatar/console-avatar"; 12 | 13 | interface PunishmentInfoCardProps { 14 | punishment: { 15 | uuid: string | null; 16 | name: string | null | undefined; 17 | banned_by_uuid: string | null; 18 | banned_by_name: string | null; 19 | console: boolean; 20 | }; 21 | children: React.ReactNode; 22 | } 23 | 24 | export const PunishmentInfoCard = async ({ punishment, children }: PunishmentInfoCardProps) => { 25 | const { dictionary } = await language(); 26 | 27 | return ( 28 |
29 |
30 |

{dictionary.words.player}

31 | 32 | 37 | 42 | 43 |
44 | {`${punishment.name}'s 51 |

{punishment.name}

52 |
53 |
54 | 55 |
56 | {children} 57 |
58 | 59 |
60 |

{dictionary.words.staff}

61 | 62 | 68 | 74 | 75 |
76 | {punishment.console ? 77 | 78 | : 79 | {`${punishment.banned_by_name}'s 86 | } 87 |

{punishment.banned_by_name}

88 |
89 |
90 |
91 | ) 92 | } -------------------------------------------------------------------------------- /src/components/ui/pagination.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" 3 | 4 | import { cn } from "@/lib/utils" 5 | import { Button, ButtonProps, buttonVariants } from "@/components/ui/button" 6 | import Link from "next/link" 7 | 8 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( 9 |