mirror of
https://github.com/fccview/cronmaster.git
synced 2026-04-17 12:38:47 -04:00
add snake and continue with consistent styling
This commit is contained in:
@@ -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 ? (
|
||||
|
||||
431
app/_components/FeatureComponents/Games/SnakeGame.tsx
Normal file
431
app/_components/FeatureComponents/Games/SnakeGame.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -78,7 +78,6 @@ export const DeleteScriptModal = ({
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={onConfirm}
|
||||
className="btn-destructive"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
111
app/not-found.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user