add snake and continue with consistent styling

This commit is contained in:
fccview
2026-01-01 08:18:17 +00:00
parent 6fe92ef3fa
commit e58c1070d6
18 changed files with 596 additions and 26 deletions

View File

@@ -301,7 +301,7 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
onNewTaskClick={() => setIsNewCronModalOpen(true)}
/>
) : (
<div className="space-y-4 max-h-[55vh] min-h-[55vh] overflow-y-auto tui-scrollbar">
<div className="space-y-4 max-h-[55vh] min-h-[55vh] overflow-y-auto tui-scrollbar pr-2">
{loadedSettings ? (
filteredJobs.map((job) =>
minimalMode ? (

View File

@@ -0,0 +1,431 @@
"use client";
import { useEffect, useRef, useState, useCallback } from "react";
import { useTranslations } from "next-intl";
import { ArrowUpIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowClockwiseIcon, PlayIcon, PauseIcon } from "@phosphor-icons/react";
interface Position {
x: number;
y: number;
}
type Direction = "UP" | "DOWN" | "LEFT" | "RIGHT";
const GRID_SIZE = 20;
const INITIAL_SNAKE: Position[] = [
{ x: 10, y: 10 },
{ x: 9, y: 10 },
{ x: 8, y: 10 },
];
const INITIAL_DIRECTION: Direction = "RIGHT";
const GAME_SPEED = 150;
export const SnakeGame = () => {
const t = useTranslations("notFound");
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [snake, setSnake] = useState<Position[]>(INITIAL_SNAKE);
const [direction, setDirection] = useState<Direction>(INITIAL_DIRECTION);
const [food, setFood] = useState<Position>({ x: 15, y: 15 });
const [gameOver, setGameOver] = useState(false);
const [gameStarted, setGameStarted] = useState(false);
const [score, setScore] = useState(0);
const [highScore, setHighScore] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [colors, setColors] = useState({ snake: "#00ff00", food: "#ff0000", grid: "#333333" });
const [cellSize, setCellSize] = useState(20);
const directionRef = useRef<Direction>(INITIAL_DIRECTION);
const gameLoopRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const savedHighScore = localStorage.getItem("snakeHighScore");
if (savedHighScore) {
setHighScore(parseInt(savedHighScore));
}
const updateColors = () => {
const theme = document.documentElement.getAttribute("data-webtui-theme");
if (theme === "catppuccin-mocha") {
setColors({
snake: "#9ca0b0",
food: "#f38ba8",
grid: "#313244",
});
} else {
setColors({
snake: "#313244",
food: "#d20f39",
grid: "#9ca0b0",
});
}
};
updateColors();
const observer = new MutationObserver(updateColors);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-webtui-theme"],
});
const updateCellSize = () => {
if (containerRef.current) {
const containerWidth = containerRef.current.offsetWidth;
const maxCanvasSize = Math.min(containerWidth - 32, 400);
const newCellSize = Math.floor(maxCanvasSize / GRID_SIZE);
setCellSize(newCellSize);
}
};
updateCellSize();
window.addEventListener("resize", updateCellSize);
return () => {
observer.disconnect();
window.removeEventListener("resize", updateCellSize);
};
}, []);
const generateFood = useCallback((): Position => {
let newFood: Position;
do {
newFood = {
x: Math.floor(Math.random() * GRID_SIZE),
y: Math.floor(Math.random() * GRID_SIZE),
};
} while (snake.some((segment) => segment.x === newFood.x && segment.y === newFood.y));
return newFood;
}, [snake]);
const resetGame = useCallback(() => {
setSnake(INITIAL_SNAKE);
setDirection(INITIAL_DIRECTION);
directionRef.current = INITIAL_DIRECTION;
setFood(generateFood());
setGameOver(false);
setGameStarted(true);
setScore(0);
setIsPaused(false);
}, [generateFood]);
const draw = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const theme = document.documentElement.getAttribute("data-webtui-theme");
const bgColor = theme === "catppuccin-mocha" ? "#1e1e2e" : "#eff1f5";
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = colors.grid;
ctx.lineWidth = 1;
for (let i = 0; i <= GRID_SIZE; i++) {
ctx.beginPath();
ctx.moveTo(i * cellSize, 0);
ctx.lineTo(i * cellSize, GRID_SIZE * cellSize);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * cellSize);
ctx.lineTo(GRID_SIZE * cellSize, i * cellSize);
ctx.stroke();
}
snake.forEach((segment) => {
ctx.fillStyle = colors.snake;
ctx.fillRect(
segment.x * cellSize + 1,
segment.y * cellSize + 1,
cellSize - 2,
cellSize - 2
);
});
ctx.fillStyle = colors.food;
ctx.fillRect(
food.x * cellSize + 1,
food.y * cellSize + 1,
cellSize - 2,
cellSize - 2
);
}, [snake, food, colors, cellSize]);
useEffect(() => {
draw();
}, [draw]);
const moveSnake = useCallback(() => {
if (gameOver || !gameStarted || isPaused) return;
setSnake((prevSnake) => {
const head = prevSnake[0];
const newHead: Position = { ...head };
switch (directionRef.current) {
case "UP":
newHead.y -= 1;
break;
case "DOWN":
newHead.y += 1;
break;
case "LEFT":
newHead.x -= 1;
break;
case "RIGHT":
newHead.x += 1;
break;
}
if (
newHead.x < 0 ||
newHead.x >= GRID_SIZE ||
newHead.y < 0 ||
newHead.y >= GRID_SIZE ||
prevSnake.some((segment) => segment.x === newHead.x && segment.y === newHead.y)
) {
setGameOver(true);
setGameStarted(false);
return prevSnake;
}
const newSnake = [newHead, ...prevSnake];
if (newHead.x === food.x && newHead.y === food.y) {
setScore((prev) => {
const newScore = prev + 10;
if (newScore > highScore) {
setHighScore(newScore);
localStorage.setItem("snakeHighScore", newScore.toString());
}
return newScore;
});
setFood(generateFood());
} else {
newSnake.pop();
}
return newSnake;
});
}, [gameOver, gameStarted, isPaused, food, highScore, generateFood]);
useEffect(() => {
if (gameStarted && !gameOver && !isPaused) {
gameLoopRef.current = setInterval(moveSnake, GAME_SPEED);
}
return () => {
if (gameLoopRef.current) {
clearInterval(gameLoopRef.current);
}
};
}, [gameStarted, gameOver, isPaused, moveSnake]);
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.code === "Space") {
e.preventDefault();
if (gameOver) {
resetGame();
} else if (!gameStarted) {
setGameStarted(true);
}
return;
}
if (e.code === "KeyP") {
e.preventDefault();
if (gameStarted && !gameOver) {
setIsPaused((prev) => !prev);
}
return;
}
if (!gameStarted || gameOver || isPaused) return;
let newDirection: Direction | null = null;
switch (e.key) {
case "ArrowUp":
if (directionRef.current !== "DOWN") {
newDirection = "UP";
}
break;
case "ArrowDown":
if (directionRef.current !== "UP") {
newDirection = "DOWN";
}
break;
case "ArrowLeft":
if (directionRef.current !== "RIGHT") {
newDirection = "LEFT";
}
break;
case "ArrowRight":
if (directionRef.current !== "LEFT") {
newDirection = "RIGHT";
}
break;
}
if (newDirection) {
e.preventDefault();
directionRef.current = newDirection;
setDirection(newDirection);
}
};
window.addEventListener("keydown", handleKeyPress);
return () => window.removeEventListener("keydown", handleKeyPress);
}, [gameOver, gameStarted, isPaused, resetGame]);
const handleTouchMove = (dir: Direction) => {
if (!gameStarted) {
setGameStarted(true);
directionRef.current = dir;
setDirection(dir);
return;
}
if (gameOver || isPaused) return;
let canMove = false;
switch (dir) {
case "UP":
canMove = directionRef.current !== "DOWN";
break;
case "DOWN":
canMove = directionRef.current !== "UP";
break;
case "LEFT":
canMove = directionRef.current !== "RIGHT";
break;
case "RIGHT":
canMove = directionRef.current !== "LEFT";
break;
}
if (canMove) {
directionRef.current = dir;
setDirection(dir);
}
};
const handleCanvasClick = () => {
if (gameOver) {
resetGame();
} else if (!gameStarted) {
setGameStarted(true);
}
};
return (
<div ref={containerRef} className="flex flex-col items-center gap-4 w-full">
<div className="tui-card p-4 w-full max-w-md">
<div className="flex justify-between items-center mb-4 terminal-font">
<div className="text-sm">
<span className="text-status-success">{t("score")}:</span> {score}
</div>
<div className="text-sm">
<span className="text-status-info">{t("highScore")}:</span> {highScore}
</div>
</div>
<div className="relative">
<canvas
ref={canvasRef}
width={GRID_SIZE * cellSize}
height={GRID_SIZE * cellSize}
className="ascii-border mx-auto cursor-pointer bg-background0"
onClick={handleCanvasClick}
/>
{!gameStarted && !gameOver && (
<div
className="absolute inset-0 flex items-center justify-center bg-background0 bg-opacity-90 ascii-border cursor-pointer"
onClick={handleCanvasClick}
>
<div className="text-center terminal-font">
<p className="text-lg font-bold uppercase mb-2">{t("pressToStart")}</p>
<p className="text-xs text-foreground0 opacity-70">{t("pauseGame")}</p>
</div>
</div>
)}
{gameOver && (
<div
className="absolute inset-0 flex items-center justify-center bg-background0 bg-opacity-90 ascii-border cursor-pointer"
onClick={handleCanvasClick}
>
<div className="text-center terminal-font">
<p className="text-2xl font-bold uppercase text-status-error mb-2">
{t("gameOver")}
</p>
<p className="text-lg mb-1">
{t("score")}: {score}
</p>
<p className="text-sm text-foreground0 opacity-70">{t("pressToRestart")}</p>
</div>
</div>
)}
{isPaused && (
<div className="absolute inset-0 flex items-center justify-center bg-background0 bg-opacity-90 ascii-border">
<div className="text-center terminal-font">
<p className="text-2xl font-bold uppercase text-status-warning">
{t("paused")}
</p>
</div>
</div>
)}
</div>
<div className="mt-4 grid grid-cols-3 gap-2 md:hidden">
<div></div>
<button
onClick={() => handleTouchMove("UP")}
className="ascii-border bg-background1 p-3 active:bg-background2 terminal-font flex items-center justify-center"
>
<ArrowUpIcon size={20} weight="bold" />
</button>
<div></div>
<button
onClick={() => handleTouchMove("LEFT")}
className="ascii-border bg-background1 p-3 active:bg-background2 terminal-font flex items-center justify-center"
>
<ArrowLeftIcon size={20} weight="bold" />
</button>
<button
onClick={() => (gameStarted && !gameOver ? setIsPaused(!isPaused) : resetGame())}
className="ascii-border bg-background1 p-3 active:bg-background2 terminal-font flex items-center justify-center"
>
{gameOver ? <ArrowClockwiseIcon size={20} weight="bold" /> : isPaused ? <PlayIcon size={20} weight="fill" /> : <PauseIcon size={20} weight="fill" />}
</button>
<button
onClick={() => handleTouchMove("RIGHT")}
className="ascii-border bg-background1 p-3 active:bg-background2 terminal-font flex items-center justify-center"
>
<ArrowRightIcon size={20} weight="bold" />
</button>
<div></div>
<button
onClick={() => handleTouchMove("DOWN")}
className="ascii-border bg-background1 p-3 active:bg-background2 terminal-font flex items-center justify-center"
>
<ArrowDownIcon size={20} weight="bold" />
</button>
<div></div>
</div>
<div className="mt-4 terminal-font text-xs text-center text-foreground0 opacity-70">
<p className="hidden md:block">{t("useArrowKeys")}</p>
<p className="md:hidden">{t("tapToMove")}</p>
</div>
</div>
</div>
);
};

View File

@@ -35,7 +35,7 @@ export const TabbedInterface = ({
>
<ClockIcon className="h-4 w-4" />
{t("cronjobs.cronJobs")}
<span className="ml-1 text-xs bg-background2 px-2 py-0.5 ascii-border font-medium">
<span className="ml-1 text-xs bg-background0 px-2 py-0.5 ascii-border font-medium">
{cronJobs.length}
</span>
</button>
@@ -48,7 +48,7 @@ export const TabbedInterface = ({
>
<FileTextIcon className="h-4 w-4" />
{t("scripts.scripts")}
<span className="ml-1 text-xs bg-background2 px-2 py-0.5 ascii-border font-medium">
<span className="ml-1 text-xs bg-background0 px-2 py-0.5 ascii-border font-medium">
{scripts.length}
</span>
</button>

View File

@@ -78,7 +78,6 @@ export const DeleteScriptModal = ({
<Button
variant="destructive"
onClick={onConfirm}
className="btn-destructive"
disabled={isDeleting}
>
{isDeleting ? (

View File

@@ -82,7 +82,6 @@ export const DeleteTaskModal = ({
<Button
variant="destructive"
onClick={onConfirm}
className="btn-destructive"
>
<TrashIcon className="h-4 w-4 mr-2" />
Delete Task

View File

@@ -182,7 +182,7 @@ export const LogsModal = ({
{logs.length > 0 && (
<Button
onClick={handleDeleteAllLogs}
className="btn-destructive glow-primary"
variant="destructive"
size="sm"
>
<TrashIcon className="w-4 h-4 mr-2" />
@@ -251,7 +251,7 @@ export const LogsModal = ({
e.stopPropagation();
handleDeleteLog(log.filename);
}}
className="btn-destructive glow-primary p-1 h-auto"
variant="destructive"
size="sm"
>
<TrashIcon className="w-3 h-3" />
@@ -271,7 +271,7 @@ export const LogsModal = ({
{t("common.loading")}...
</div>
) : selectedLog ? (
<pre className="h-full overflow-auto bg-background2 p-4 ascii-border text-xs font-mono whitespace-pre-wrap terminal-font">
<pre className="h-full overflow-auto bg-background0 tui-scrollbar p-4 ascii-border text-xs font-mono whitespace-pre-wrap terminal-font">
{logContent}
</pre>
) : (

View File

@@ -120,7 +120,7 @@ export const RestoreBackupModal = ({
<p>{t("cronjobs.noBackupsFound")}</p>
</div>
) : (
<div className="space-y-3 max-h-[500px] overflow-y-auto tui-scrollbar">
<div className="space-y-3 max-h-[500px] overflow-y-auto tui-scrollbar pr-2 pb-2">
{backups.map((backup) => (
<div
key={backup.filename}
@@ -178,11 +178,11 @@ export const RestoreBackupModal = ({
<UploadIcon className="h-3 w-3" />
</Button>
<Button
variant="outline"
variant="destructive"
size="sm"
onClick={() => handleDelete(backup.filename)}
disabled={deletingFilename === backup.filename}
className="btn-destructive h-8 px-3"
className="h-8 px-3"
title={t("cronjobs.deleteBackup")}
>
{deletingFilename === backup.filename ? (

View File

@@ -145,7 +145,7 @@ export const ScriptModal = ({
</div>
</div>
<div className="flex justify-between items-center gap-3 pt-4 ascii-border border-t">
<div className="flex justify-between items-center gap-3 pt-4 border-border border-t">
<div>
{isDraft && onClearDraft && (
<Button

View File

@@ -123,7 +123,7 @@ export const BashSnippetHelper = ({
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="MagnifyingGlassIcon bash snippets..."
placeholder="Search bash snippets..."
className="pl-9"
/>
</div>
@@ -209,7 +209,6 @@ export const BashSnippetHelper = ({
variant="outline"
size="sm"
onClick={() => handleCopy(snippet)}
className="h-6 w-8 p-0 text-xs"
>
{copiedId === snippet.id ? (
<CheckIcon className="h-3 w-3" />
@@ -222,7 +221,7 @@ export const BashSnippetHelper = ({
variant="default"
size="sm"
onClick={() => handleInsert(snippet)}
className="h-6 px-3 text-xs flex-1"
className="flex-1"
>
Insert
</Button>

View File

@@ -97,7 +97,7 @@ export const CronExpressionHelper = ({
</div>
{explanation && (
<div className="bg-background2 p-2 ascii-border terminal-font">
<div className="bg-background0 p-2 text-status-warning ascii-border terminal-font">
<div className="space-y-1">
<div className="flex items-start gap-2">
<InfoIcon className="h-3 w-3 text-primary mt-0.5 flex-shrink-0" />
@@ -166,7 +166,7 @@ export const CronExpressionHelper = ({
<Input
value={patternSearch}
onChange={(e) => setPatternSearch(e.target.value)}
placeholder="MagnifyingGlassIcon patterns..."
placeholder="Search patterns..."
className="pl-9"
/>
</div>

View File

@@ -206,7 +206,7 @@ export const ScriptsManager = ({
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-background2 ascii-border">
<div className="p-2 bg-background0 ascii-border">
<FileTextIcon className="h-5 w-5 text-primary" />
</div>
<div>
@@ -331,7 +331,7 @@ export const ScriptsManager = ({
setSelectedScript(script);
setIsDeleteModalOpen(true);
}}
className="btn-destructive h-8 px-3"
className="h-8 px-3"
title="Delete script"
aria-label="Delete script"
>

View File

@@ -59,7 +59,7 @@ export const Modal = ({
}
}}
>
<div className="ascii-border border-t-0 border-l-0 border-r-0 p-4 flex justify-between items-center bg-background0">
<div className="border-border border-b p-4 flex justify-between items-center bg-background0">
<h2 className="terminal-font font-bold uppercase">{title}</h2>
{showCloseButton && (
<Button variant="ghost" size="icon" onClick={onClose}>

View File

@@ -188,5 +188,21 @@
"warnings": {
"wrapperScriptModified": "Wrapper Script Modified",
"wrapperScriptModifiedDescription": "Your cron-log-wrapper.sh script has been modified from the official version. This may affect logging functionality. Consider reverting to the official version or ensure your changes don't break the required format for log parsing."
},
"notFound": {
"title": "404 - Page Not Found",
"subtitle": "ERROR: The requested resource could not be located",
"message": "The page you're looking for doesn't exist. Want to play snake instead?",
"gameOver": "GAME OVER",
"score": "Score",
"highScore": "High Score",
"pressToStart": "Press SPACE or tap to start",
"pressToRestart": "Press SPACE or tap to restart",
"controls": "Controls",
"useArrowKeys": "Use arrow keys to move",
"tapToMove": "Tap screen edges to move",
"goHome": "Return to Dashboard",
"pauseGame": "Press P to pause",
"paused": "PAUSED"
}
}

View File

@@ -184,5 +184,22 @@
"warnings": {
"wrapperScriptModified": "Script Wrapper Modificato",
"wrapperScriptModifiedDescription": "Il tuo script cron-log-wrapper.sh è stato modificato dalla versione ufficiale. Questo potrebbe influenzare la funzionalità di logging. Considera di ripristinare la versione ufficiale o assicurati che le tue modifiche non interrompano il formato richiesto per l'analisi dei log."
},
"notFound": {
"title": "404 - Pagina Non Trovata",
"subtitle": "ERRORE: La risorsa richiesta non è stata trovata",
"message": "La pagina che stai cercando non esiste. Partitella a snake?",
"playSnake": "Gioca a Snake mentre sei qui",
"gameOver": "GAME OVER",
"score": "Punteggio",
"highScore": "Punteggio Massimo",
"pressToStart": "Premi SPAZIO o tocca per iniziare",
"pressToRestart": "Premi SPAZIO o tocca per ricominciare",
"controls": "Controlli",
"useArrowKeys": "Usa i tasti freccia per muoverti",
"tapToMove": "Tocca i bordi dello schermo per muoverti",
"goHome": "Torna alla Dashboard",
"pauseGame": "Premi P per mettere in pausa",
"paused": "IN PAUSA"
}
}

View File

@@ -19,7 +19,6 @@
box-sizing: border-box;
}
/* Force scrollbars to always be visible */
::-webkit-scrollbar {
-webkit-appearance: none;
}
@@ -32,8 +31,6 @@
}
@layer utilities {
.ascii-border {
border: 1px solid var(--box-border-color, var(--foreground2));
border-radius: 0;
@@ -45,11 +42,12 @@
.tui-scrollbar {
scrollbar-width: auto !important;
padding-right: 15px;
}
.tui-scrollbar::-webkit-scrollbar {
-webkit-appearance: none;
width: 10px;
width: 7px;
height: 10px;
background-color: var(--background1) !important;
}

111
app/not-found.tsx Normal file
View File

@@ -0,0 +1,111 @@
import Link from "next/link";
import { getTranslations } from "@/app/_server/actions/translations";
import { SnakeGame } from "@/app/_components/FeatureComponents/Games/SnakeGame";
import { Logo } from "@/app/_components/GlobalComponents/Logo/Logo";
import { SystemInfoCard } from "@/app/_components/FeatureComponents/System/SystemInfo";
import { ThemeToggle } from "@/app/_components/FeatureComponents/Theme/ThemeToggle";
import { LogoutButton } from "@/app/_components/FeatureComponents/LoginForm/LogoutButton";
import { ToastContainer } from "@/app/_components/GlobalComponents/UIElements/Toast";
import { PWAInstallPrompt } from "@/app/_components/FeatureComponents/PWA/PWAInstallPrompt";
import { SSEProvider } from "@/app/_contexts/SSEContext";
export default async function NotFound() {
const t = await getTranslations();
const liveUpdatesEnabled =
(typeof process.env.LIVE_UPDATES === "boolean" &&
process.env.LIVE_UPDATES === true) ||
process.env.LIVE_UPDATES !== "false";
const initialSystemInfo = {
hostname: "Loading...",
platform: "Loading...",
uptime: "Loading...",
memory: {
total: "0 B",
used: "0 B",
free: "0 B",
usage: 0,
status: "Loading",
},
cpu: {
model: "Loading...",
cores: 0,
usage: 0,
status: "Loading",
},
gpu: {
model: "Loading...",
status: "Loading",
},
disk: {
total: "0 B",
used: "0 B",
free: "0 B",
usage: 0,
status: "Loading",
},
systemStatus: {
overall: "Loading",
details: "Fetching system information...",
},
};
const bodyClass = process.env.DISABLE_SYSTEM_STATS === "true" ? "no-sidebar" : "";
return (
<SSEProvider liveUpdatesEnabled={liveUpdatesEnabled}>
<div className={`min-h-screen bg-background0 ${bodyClass}`}>
<header className="ascii-border !border-r-0 sticky top-0 z-20 bg-background0 lg:h-[90px]">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between lg:justify-center">
<div className="flex items-center gap-4">
<Logo size={48} showGlow={true} />
<div>
<h1 className="text-xl sm:text-2xl lg:text-3xl font-bold terminal-font uppercase">
Cr*nMaster
</h1>
<p className="text-xs terminal-font flex items-center gap-2">
{t("common.cronManagementMadeEasy")}
</p>
</div>
</div>
{process.env.AUTH_PASSWORD && (
<div className="lg:absolute lg:right-10">
<LogoutButton />
</div>
)}
</div>
</div>
</header>
{process.env.DISABLE_SYSTEM_STATS !== "true" && (
<SystemInfoCard systemInfo={initialSystemInfo} />
)}
<main className="transition-all duration-300">
<div className="px-4 py-8 lg:px-8">
<div className="text-center mt-6 mb-12">
<div className="text-6xl font-bold terminal-font text-status-error mb-2">404</div>
<p className="terminal-font text-sm mb-4">{t("notFound.message")}</p>
<Link
href="/"
className="ascii-border bg-background1 hover:bg-background2 px-4 py-2 terminal-font uppercase font-bold transition-colors text-sm inline-block"
>
{t("notFound.goHome")}
</Link>
</div>
<SnakeGame />
</div>
</main>
<ToastContainer />
<div className="flex items-center gap-2 fixed bottom-4 left-4 lg:right-4 lg:left-auto z-10 bg-background0 ascii-border p-1">
<ThemeToggle />
<PWAInstallPrompt />
</div>
</div>
</SSEProvider>
);
}

View File

@@ -93,7 +93,7 @@ export default async function Home() {
)}
<main className="transition-all duration-300">
<div className="container mx-auto px-4 py-8 lg:px-8">
<div className="px-4 py-8 lg:px-8">
<WrapperScriptWarning />
<TabbedInterface cronJobs={cronJobs} scripts={scripts} />
</div>

View File

File diff suppressed because one or more lines are too long