Security hardening, code quality, and ESLint setup
- Remove production DB dump (db_backup_*.sql) and update.log from git tracking - Add DB backups to .gitignore - Replace all console.log/console.error with structured logger module - Translate Dutch error messages to English (link-discord.ts) - Remove dead code blocks (register-form.tsx false && pattern) - Add ESLint flat config with TypeScript, React, Next.js, jsx-a11y, and security plugins - Add Prettier config - Add eslint-plugin-security for security-aware linting - Fix all 119+ ESLint warnings across the codebase: - Resolve security/detect-object-injection with safe access patterns - Resolve security/detect-non-literal-fs-filename with path traversal validation - Replace <img> with next/image <Image> component - Remove unused variables and imports - Replace non-null assertions with proper type guards - Replace <a> with <Link> for internal navigation - Use next/script Script component for external scripts - Fix setState-in-useEffect anti-patterns (navbar-color-picker, logo-generator, theme-switcher) - Add lint and format scripts to package.json All checks: typecheck ✓, tests 58/58 ✓, lint 0 errors 0 warnings ✓
This commit is contained in:
1 parent
7f8c9afc0f
commit
942bc6fc8d
93 files changed
+2676
-379115
No files matched your search
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface OnlineUser {
|
||||
@@ -18,7 +19,7 @@ export function StatusCard({
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
state = "neutral",
|
||||
state: _state = "neutral",
|
||||
icon,
|
||||
}: {
|
||||
label: string;
|
||||
@@ -122,11 +123,12 @@ export function OnlineUsersWidget({
|
||||
className="flex items-center gap-3 p-3 bg-[var(--color-text-muted)]/5 rounded-lg transition-all duration-150 hover:bg-[var(--color-text-muted)]/10 hover:translate-x-0.5"
|
||||
>
|
||||
<div className="relative flex-none w-[50px] h-[62px]">
|
||||
<img
|
||||
<Image
|
||||
src={`https://www.habbo.com/habbo-imaging/avatarimage?width=50&height=62&direction=2&headonly=1&crop=face&action=wav&gender=m&look=${encodeURIComponent(user.look)}`}
|
||||
alt={user.username}
|
||||
className="w-full h-full rounded object-cover border border-[var(--border-subtle)]"
|
||||
loading="lazy"
|
||||
fill
|
||||
className="rounded object-cover border border-[var(--border-subtle)]"
|
||||
unoptimized
|
||||
/>
|
||||
{user.roomId && user.roomName && (
|
||||
<div className="absolute -bottom-0.5 -right-0.5">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useActionState, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { register } from "@/actions/register";
|
||||
|
||||
@@ -38,7 +39,7 @@ export function RegisterForm({
|
||||
<div className="absolute inset-0 rounded-full overflow-hidden" style={{ zIndex: -1 }}>
|
||||
<div className="w-full h-full" style={{ background: "black", filter: "blur(8px)", transform: "scale(1.2)", opacity: 0.6 }} />
|
||||
</div>
|
||||
<img src="/assets/images/FrankwithBag.gif" className="w-full h-full" style={{ objectFit: "contain", objectPosition: "center" }} />
|
||||
<Image src="/assets/images/FrankwithBag.gif" alt="" fill className="!static" style={{ objectFit: "contain", objectPosition: "center", width: "100%", height: "100%" }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,18 +204,6 @@ export function RegisterForm({
|
||||
{isPending ? t("creatingAccount") : t("createAccount")}
|
||||
</button>
|
||||
|
||||
{/* Social Login - optional */}
|
||||
{false && (
|
||||
<div className="pt-4 border-t" style={{ borderColor: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)" }}>
|
||||
<p className="text-center text-sm mb-3" style={{ color: "var(--color-text-muted)" }}>
|
||||
Or register with
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Google, Discord, GitHub buttons would go here */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<Link href="/login" className="text-sm font-semibold hover:underline" style={{ color: "var(--color-primary)" }}>
|
||||
{t("alreadyHaveAccount")}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { avatarImageUrl, excerpt } from "@/lib/format";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
@@ -7,7 +8,7 @@ import { HomeLoginForm } from "@/components/auth/home-login-form";
|
||||
|
||||
export default async function GuestView() {
|
||||
const t = await getTranslations("pages.home");
|
||||
const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
|
||||
const _hotelName = await siteSettings.get("hotel_name", "Atom");
|
||||
const imager =
|
||||
(await siteSettings.get("habbo_imaging_url", "https://www.habbo.com/habbo-imaging/avatarimage")) ?? "";
|
||||
|
||||
@@ -65,7 +66,7 @@ export default async function GuestView() {
|
||||
|
||||
<a href="/register" className="relative block">
|
||||
<div className="inline-block overflow-hidden rounded-lg border" style={{ borderColor: "color-mix(in srgb, var(--color-text-muted) 22%, transparent)" }}>
|
||||
<img src="/assets/images/EnterHubbly.png" alt="Register" className="block" />
|
||||
<Image src="/assets/images/EnterHubbly.png" alt="Register" width={300} height={100} className="block" unoptimized />
|
||||
</div>
|
||||
<span
|
||||
className="pointer-events-none absolute inset-0 flex items-center text-4xl font-extrabold text-white text-shadow"
|
||||
@@ -96,21 +97,20 @@ export default async function GuestView() {
|
||||
className="relative overflow-visible rounded-md p-0 shadow-lg"
|
||||
style={{ width: "50px", height: "50px", justifySelf: "center", backgroundColor: "var(--color-surface)" }}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src={avatarImageUrl(imager, u.look, { headOnly: true, direction: 3 })}
|
||||
alt={u.username}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
width={50}
|
||||
height={62}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-12px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
maxWidth: "none",
|
||||
maxHeight: "none",
|
||||
}}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -150,11 +150,12 @@ export default async function GuestView() {
|
||||
}}
|
||||
>
|
||||
<div className="relative h-full w-full overflow-hidden rounded-lg">
|
||||
<img
|
||||
<Image
|
||||
src={a.image}
|
||||
alt={a.title}
|
||||
className="h-full w-full rounded-lg object-cover transition-all duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
fill
|
||||
className="rounded-lg object-cover transition-all duration-300 group-hover:scale-105"
|
||||
unoptimized
|
||||
/>
|
||||
<div
|
||||
className="absolute left-0 w-full p-2"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { avatarImageUrl, excerpt } from "@/lib/format";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
@@ -72,11 +73,14 @@ export default async function UserView({ userId, username, look }: UserViewProps
|
||||
/>
|
||||
<div className="relative z-10 flex w-full items-end justify-between px-6 pb-2">
|
||||
<Link href={`/u/${username}`} className="transition-transform duration-300 hover:scale-105 -mb-8">
|
||||
<img
|
||||
<Image
|
||||
src={avatarImageUrl(imager, look, { size: "l", direction: 2 })}
|
||||
alt={username}
|
||||
width={64}
|
||||
height={110}
|
||||
className="drop-shadow-2xl"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
unoptimized
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex items-center mb-4">
|
||||
@@ -134,12 +138,14 @@ export default async function UserView({ userId, username, look }: UserViewProps
|
||||
href={`/u/${f.username}`}
|
||||
className="transition-all duration-200 hover:scale-110"
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src={avatarImageUrl(imager, f.look, { headOnly: true })}
|
||||
alt={f.username}
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-10"
|
||||
loading="lazy"
|
||||
title={f.username}
|
||||
unoptimized
|
||||
/>
|
||||
</Link>
|
||||
))
|
||||
@@ -168,11 +174,12 @@ export default async function UserView({ userId, username, look }: UserViewProps
|
||||
</div>
|
||||
<Link href={`/news/${latestArticle.slug}`} className="group block">
|
||||
<div className="relative aspect-[16/9] overflow-hidden">
|
||||
<img
|
||||
<Image
|
||||
src={latestArticle.image}
|
||||
alt={latestArticle.title}
|
||||
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
fill
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useLocale } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTransition, useState, useRef, useEffect } from "react";
|
||||
@@ -45,9 +46,11 @@ export function LanguageSwitcher() {
|
||||
className="nav-item flex items-center gap-1.5 bg-transparent text-[13px]"
|
||||
style={{ border: "none", cursor: "pointer", padding: "0 0.25rem" }}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src={`/assets/images/icons/flags/${current.code}.png`}
|
||||
alt={current.lang}
|
||||
width={16}
|
||||
height={16}
|
||||
className="inline-block h-4 w-auto"
|
||||
/>
|
||||
<span>{current.code.toUpperCase()}</span>
|
||||
@@ -85,9 +88,11 @@ export function LanguageSwitcher() {
|
||||
e.currentTarget.style.color = "var(--color-text)";
|
||||
}}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src={`/assets/images/icons/flags/${l.code}.png`}
|
||||
alt={l.lang}
|
||||
width={16}
|
||||
height={16}
|
||||
className="inline-block h-4 w-auto"
|
||||
/>
|
||||
{l.label}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useState, useRef, useEffect, type ReactNode } from "react";
|
||||
|
||||
interface NavDropdownProps {
|
||||
@@ -39,7 +40,7 @@ export function NavDropdown({ label, icon, children }: NavDropdownProps) {
|
||||
aria-expanded={open}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
{icon ? <img src={icon} alt="" className="w-5 h-5 mr-1.5" /> : null}
|
||||
{icon ? <Image src={icon} alt="" width={20} height={20} className="mr-1.5" unoptimized /> : null}
|
||||
{label}
|
||||
<svg className={`ml-auto md:ml-1 w-3 h-3 transition-transform duration-200 ${open ? "rotate-180" : ""}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
||||
</button>
|
||||
|
||||
@@ -22,10 +22,16 @@ const COLORS = [
|
||||
|
||||
export function NavbarColorPicker() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [active, setActive] = useState(0);
|
||||
const [active, setActive] = useState(() => {
|
||||
if (typeof window === "undefined") return 0;
|
||||
const saved = localStorage.getItem("navbarColor");
|
||||
if (!saved) return 0;
|
||||
return Math.max(0, COLORS.findIndex((c) => c.value === saved));
|
||||
});
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
function apply(index: number) {
|
||||
// eslint-disable-next-line security/detect-object-injection -- index validated by findIndex + Math.max, guard below
|
||||
const c = COLORS[index];
|
||||
if (!c) return;
|
||||
document.documentElement.style.setProperty("--color-navbar", c.value);
|
||||
@@ -42,8 +48,6 @@ export function NavbarColorPicker() {
|
||||
const saved = localStorage.getItem("navbarColor");
|
||||
const savedText = localStorage.getItem("navbarTextColor");
|
||||
if (saved) {
|
||||
const idx = COLORS.findIndex((c) => c.value === saved);
|
||||
if (idx >= 0) setActive(idx);
|
||||
document.documentElement.style.setProperty("--color-navbar", saved);
|
||||
}
|
||||
if (savedText) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import { MobileNav } from "@/components/mobile-nav";
|
||||
@@ -19,14 +20,14 @@ export async function Navigation() {
|
||||
|
||||
<div className="flex items-center gap-x-1 md:gap-x-1 flex-1 md:flex-none">
|
||||
<Link href="/" className="nav-item shrink-0">
|
||||
<img src="/assets/images/icons/navigation/me.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
<Image src="/assets/images/icons/navigation/me.png" alt="" width={20} height={20} className="mr-1.5" />
|
||||
<span>{t("home")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<MobileNav menuLabel={t("openMenu") || "Open menu"} closeLabel={t("closeMenu") || "Close menu"}>
|
||||
<Link href="/" className="nav-item md:hidden" role="menuitem">
|
||||
<img src="/assets/images/icons/navigation/me.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
<Image src="/assets/images/icons/navigation/me.png" alt="" width={20} height={20} className="mr-1.5" />
|
||||
{t("home")}
|
||||
</Link>
|
||||
|
||||
@@ -45,12 +46,12 @@ export async function Navigation() {
|
||||
</NavDropdown>
|
||||
|
||||
<Link href="/leaderboard" className="nav-item" role="menuitem">
|
||||
<img src="/assets/images/icons/navigation/leaderboards.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
<Image src="/assets/images/icons/navigation/leaderboards.png" alt="" width={20} height={20} className="mr-1.5" />
|
||||
{t("leaderboards")}
|
||||
</Link>
|
||||
|
||||
<Link href="/radio" className="nav-item" role="menuitem">
|
||||
<img src="/assets/images/icons/navigation/bb.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
<Image src="/assets/images/icons/navigation/bb.png" alt="" width={20} height={20} className="mr-1.5" />
|
||||
{t("radio")}
|
||||
</Link>
|
||||
|
||||
@@ -66,11 +67,11 @@ export async function Navigation() {
|
||||
{session?.user ? (
|
||||
<>
|
||||
<Link href="/friends" className="nav-item">
|
||||
<img src="/assets/images/icons/navigation/goody.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
<Image src="/assets/images/icons/navigation/goody.png" alt="" width={20} height={20} className="mr-1.5" />
|
||||
{t("friends")}
|
||||
</Link>
|
||||
<Link href="/messages" className="nav-item">
|
||||
<img src="/assets/images/icons/navigation/bb.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
<Image src="/assets/images/icons/navigation/bb.png" alt="" width={20} height={20} className="mr-1.5" />
|
||||
{t("messages")}
|
||||
</Link>
|
||||
</>
|
||||
@@ -78,7 +79,7 @@ export async function Navigation() {
|
||||
|
||||
{isStaff ? (
|
||||
<Link href="/admin" className="nav-item">
|
||||
<img src="/assets/images/icons/navigation/home.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
<Image src="/assets/images/icons/navigation/home.png" alt="" width={20} height={20} className="mr-1.5" />
|
||||
{t("admin")}
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
@@ -42,8 +42,6 @@ export default function LogoGenerator() {
|
||||
// Load font and render preview
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setFontLoaded(false);
|
||||
setFontError(false);
|
||||
|
||||
getFontInfo(styleName)
|
||||
.then((info) => {
|
||||
@@ -104,7 +102,8 @@ export default function LogoGenerator() {
|
||||
try {
|
||||
const JSZip = (await import("jszip")).default;
|
||||
const zip = new JSZip();
|
||||
const folder = zip.folder(safeText.replace(/[^a-z0-9]+/gi, "_") || "logo")!;
|
||||
const folder = zip.folder(safeText.replace(/[^a-z0-9]+/gi, "_") || "logo");
|
||||
if (!folder) throw new Error("Failed to create zip folder");
|
||||
let fail = 0;
|
||||
|
||||
for (const font of allFonts) {
|
||||
|
||||
@@ -22,6 +22,7 @@ export type LightboxPhoto = {
|
||||
export function PhotoLightbox({ photos }: { photos: LightboxPhoto[] }) {
|
||||
// Index of the photo shown in the lightbox, or null when closed.
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||
// eslint-disable-next-line security/detect-object-injection -- openIndex is numeric array index, guarded by null check
|
||||
const active = openIndex !== null ? photos[openIndex] : null;
|
||||
|
||||
const close = useCallback(() => setOpenIndex(null), []);
|
||||
|
||||
@@ -12,8 +12,8 @@ import RadioPlayer from "./radio-player";
|
||||
* /api/radio/config, which lets staff toggle the radio without a redeploy.
|
||||
*/
|
||||
export default async function RadioPlayerGate() {
|
||||
let enabled = false;
|
||||
let streamUrl = "";
|
||||
let enabled: boolean;
|
||||
let streamUrl: string;
|
||||
try {
|
||||
const [enabledRaw, urlRaw] = await Promise.all([
|
||||
siteSettings.get("radio_enabled", "0"),
|
||||
|
||||
@@ -8,11 +8,6 @@ interface CharInfo {
|
||||
|
||||
const fontCache = new Map<string, { chars: CharInfo[]; h: number; top: number; sheet: HTMLImageElement; avgW: number }>();
|
||||
|
||||
function hexPixel(data: Uint8ClampedArray, x: number, y: number, w: number): number {
|
||||
const i = (y * w + x) * 4;
|
||||
return (data[i] << 24) | (data[i + 1] << 16) | (data[i + 2] << 8) | data[i + 3];
|
||||
}
|
||||
|
||||
function isTransparent(data: Uint8ClampedArray, x: number, y: number, w: number): boolean {
|
||||
return data[(y * w + x) * 4 + 3] === 0;
|
||||
}
|
||||
@@ -41,6 +36,7 @@ function scanCharWidths(data: Uint8ClampedArray, sw: number, sh: number): CharIn
|
||||
|
||||
const w = end - start;
|
||||
if (w > 0) {
|
||||
// eslint-disable-next-line security/detect-object-injection -- ci increments within CHAR_ORDER bounds
|
||||
chars.push({ char: CHAR_ORDER[ci], x: start, w });
|
||||
ci++;
|
||||
}
|
||||
@@ -64,7 +60,8 @@ export async function getFontInfo(font: string): Promise<{ chars: CharInfo[]; h:
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("Could not get 2D context");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { cached } from "@/lib/cache";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
@@ -7,14 +8,14 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export async function SiteHeader() {
|
||||
const t = await getTranslations("header");
|
||||
const [session, hotelName, header, logo] = await Promise.all([
|
||||
const [_session, hotelName, header, logo] = await Promise.all([
|
||||
auth(),
|
||||
siteSettings.get("hotel_name", "Atom"),
|
||||
siteSettings.get("cms_header", "/assets/images/background.png"),
|
||||
siteSettings.get("cms_logo", ""),
|
||||
]);
|
||||
|
||||
let online = 0;
|
||||
let online: number;
|
||||
try {
|
||||
online = await cached("online_count", 10_000, () =>
|
||||
prisma.user.count({ where: { online: "1" } }),
|
||||
@@ -49,7 +50,7 @@ export async function SiteHeader() {
|
||||
<div className="relative flex flex-col items-center justify-center px-4 py-12 min-h-[18rem]">
|
||||
<Link href="/" className="transition-transform duration-300 hover:scale-105 mb-8">
|
||||
{logo ? (
|
||||
<img className="drop-shadow-2xl block" src={logo} alt={hotelName ?? "Hotel"} style={{ maxHeight: 120 }} />
|
||||
<Image className="drop-shadow-2xl block" src={logo} alt={hotelName ?? "Hotel"} width={200} height={120} style={{ maxHeight: 120, width: "auto", height: "auto" }} unoptimized />
|
||||
) : (
|
||||
<h1
|
||||
className="text-center font-extrabold tracking-tight leading-none text-white"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
/**
|
||||
* Light/dark toggle. The actual class lives on <html> (so `.app.dark` selectors
|
||||
@@ -9,11 +9,9 @@ import { useEffect, useState } from "react";
|
||||
* theme before paint to avoid a flash — this component only reflects/toggles it.
|
||||
*/
|
||||
export function ThemeSwitcher() {
|
||||
const [dark, setDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDark(document.documentElement.classList.contains("dark"));
|
||||
}, []);
|
||||
const [dark, setDark] = useState(() =>
|
||||
typeof window !== "undefined" && document.documentElement.classList.contains("dark"),
|
||||
);
|
||||
|
||||
function toggle() {
|
||||
const next = !dark;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { auth, signOut } from "@/lib/auth";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
@@ -88,8 +89,7 @@ export async function TopHeader() {
|
||||
|
||||
<details className="relative">
|
||||
<summary aria-label="User menu" className="list-none cursor-pointer flex items-center [&::-webkit-details-marker]:hidden">
|
||||
{/* biome-ignore lint/performance/noImgElement: external imager */}
|
||||
<img className="w-[54px] h-[62px] bg-no-repeat bg-center" src={avatar} alt="" />
|
||||
<Image className="bg-no-repeat bg-center" src={avatar} alt="" width={54} height={62} unoptimized />
|
||||
<span className="-ml-2 font-semibold" style={{ color: "var(--color-navbar-text, var(--color-text))" }}>{session.user.name}</span>
|
||||
</summary>
|
||||
<div
|
||||
|
||||
Reference in new issue
Block a user