diff --git a/frontend/public/assets/vm-shot-2026-08-07T19-27-16-851Z.jpg b/frontend/public/assets/vm-shot-2026-08-07T19-27-16-851Z.jpg
new file mode 100644
index 0000000..3edc0d3
Binary files /dev/null and b/frontend/public/assets/vm-shot-2026-08-07T19-27-16-851Z.jpg differ
diff --git a/frontend/public/assets/vm-shot-2026-08-07T19-27-26-780Z.jpg b/frontend/public/assets/vm-shot-2026-08-07T19-27-26-780Z.jpg
new file mode 100644
index 0000000..3edc0d3
Binary files /dev/null and b/frontend/public/assets/vm-shot-2026-08-07T19-27-26-780Z.jpg differ
diff --git a/frontend/public/assets/vm-shot-2026-08-07T19-28-03-715Z.jpg b/frontend/public/assets/vm-shot-2026-08-07T19-28-03-715Z.jpg
new file mode 100644
index 0000000..3edc0d3
Binary files /dev/null and b/frontend/public/assets/vm-shot-2026-08-07T19-28-03-715Z.jpg differ
diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx
index 6548433..2ef67e9 100644
--- a/frontend/src/components/NavBarItem.tsx
+++ b/frontend/src/components/NavBarItem.tsx
@@ -1,6 +1,5 @@
-import React, {useEffect, useRef} from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
-import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx
index 1b9907d..73d8391 100644
--- a/frontend/src/layouts/Authenticated.tsx
+++ b/frontend/src/layouts/Authenticated.tsx
@@ -1,5 +1,4 @@
-import React, { ReactNode, useEffect } from 'react'
-import { useState } from 'react'
+import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index 5cb10e8..cdc330c 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -7,6 +7,13 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
+ {
+ href: '/play',
+ label: 'Play Game',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: 'mdiGamepadVariant' in icon ? icon['mdiGamepadVariant' as keyof typeof icon] : icon.mdiTable,
+ },
{
href: '/users/users-list',
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index ae1bc95..af5d53e 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,166 +1,166 @@
-
-import React, { useEffect, useState } from 'react';
+import React from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
-import BaseButton from '../components/BaseButton';
-import CardBox from '../components/CardBox';
-import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
-import BaseDivider from '../components/BaseDivider';
-import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
-import { useAppSelector } from '../stores/hooks';
-import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
-import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
+const colorBursts = [
+ 'bg-[#35F2C1]',
+ 'bg-[#FF4FD8]',
+ 'bg-[#FFD166]',
+ 'bg-[#7C5CFF]',
+ 'bg-[#38BDF8]',
+ 'bg-[#FF7A59]',
+];
+
+const arenaStats = [
+ ['49', 'color blocks'],
+ ['4', 'rivals'],
+ ['45s', 'rounds'],
+];
+
+const floatingBlocks = [
+ 'left-[6%] top-[18%] h-14 w-14 rotate-12 bg-[#FF4FD8] shadow-[#B8228D]',
+ 'right-[8%] top-[17%] h-16 w-16 -rotate-12 bg-[#FFD166] shadow-[#B88711]',
+ 'left-[12%] bottom-[14%] h-12 w-12 -rotate-6 bg-[#38BDF8] shadow-[#117DAA]',
+ 'right-[18%] bottom-[18%] h-10 w-10 rotate-6 bg-[#7C5CFF] shadow-[#4931B8]',
+];
export default function Starter() {
- const [illustrationImage, setIllustrationImage] = useState({
- src: undefined,
- photographer: undefined,
- photographer_url: undefined,
- })
- const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
- const [contentType, setContentType] = useState('image');
- const [contentPosition, setContentPosition] = useState('left');
- const textColor = useAppSelector((state) => state.style.linkColor);
-
- const title = 'Steal a Color Game'
-
- // Fetch Pexels image/video
- useEffect(() => {
- async function fetchData() {
- const image = await getPexelsImage();
- const video = await getPexelsVideo();
- setIllustrationImage(image);
- setIllustrationVideo(video);
- }
- fetchData();
- }, []);
-
- const imageBlock = (image) => (
-
- );
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
- Your browser does not support the video tag.
-
-
-
)
- }
- };
-
return (
-
+ <>
-
{getPageTitle('Starter Page')}
+
{getPageTitle('Steal a Color')}
+
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
-
This is a React.js/Node.js app generated by the Flatlogic Web App Generator
-
For guides and documentation please check
- your local README.md and the Flatlogic documentation
+
+
+
+
+
+
+
+
+ {floatingBlocks.map((block) => (
+
+ ))}
+
+
+
+
+ {colorBursts.slice(0, 4).map((color) => (
+
+ ))}
+
+
+ Roblox-like
+ Steal a Color
+
+
+
+
+
+ Play
+
+
+ Admin Login
+
+
+
+
+
+
+
+
+ Color stealing block arena
+
+
+
+ Jump in, slap a color block, and steal the whole map.
+
+
+ A chunkier, Roblox-inspired look for quick territory rounds: toy-bright tiles, simple block avatars, chunky buttons, and an arena that feels like a colorful playground.
+
+
+
+
+ Start a Match
+
+
+ Open Admin
+
+
+
+
+ {arenaStats.map(([value, label]) => (
+
+
{value}
+ {label}
+
+ ))}
+
-
-
-
-
-
-
-
-
-
-
© 2026 {title} . All rights reserved
-
- Privacy Policy
-
-
+
+
+
+
+
+
Live board
+
Preview Arena
+
+
00:45
+
-
+
+
+
+
+
+
+ {Array.from({ length: 49 }).map((_, index) => {
+ const color = colorBursts[(index * 5 + Math.floor(index / 7)) % colorBursts.length];
+ return (
+
+ );
+ })}
+
+
+
+
+ {[
+ ['You', '31%', '#35F2C1', '#159875'],
+ ['Pixel', '26%', '#FF4FD8', '#B8228D'],
+ ['Volt', '23%', '#FFD166', '#B88711'],
+ ['Nova', '20%', '#7C5CFF', '#4931B8'],
+ ].map(([name, score, color, shade]) => (
+
+
+
{name}
+
{score} control
+
+ ))}
+
+
+
+
+
+
+ >
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
return {page} ;
};
-
diff --git a/frontend/src/pages/play.tsx b/frontend/src/pages/play.tsx
new file mode 100644
index 0000000..9e86147
--- /dev/null
+++ b/frontend/src/pages/play.tsx
@@ -0,0 +1,471 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import type { ReactElement } from 'react';
+import Head from 'next/head';
+import Link from 'next/link';
+import LayoutGuest from '../layouts/Guest';
+import { getPageTitle } from '../config';
+
+type Owner = 0 | 1 | 2 | 3;
+type Phase = 'setup' | 'playing' | 'finished';
+
+type Player = {
+ id: Owner;
+ name: string;
+ color: string;
+ textColor: string;
+ glow: string;
+ shade: string;
+};
+
+type MatchRecord = {
+ id: string;
+ playerName: string;
+ score: number;
+ winner: string;
+ won: boolean;
+ captured: number;
+ createdAt: string;
+ board: Owner[];
+};
+
+const boardSize = 49;
+const matchSeconds = 45;
+const historyKey = 'steal-a-color-history';
+
+const players: Player[] = [
+ { id: 0, name: 'You', color: '#35F2C1', textColor: 'text-[#087560]', glow: 'shadow-[#159875]/35', shade: '#159875' },
+ { id: 1, name: 'Pixel', color: '#FF4FD8', textColor: 'text-[#B8228D]', glow: 'shadow-[#B8228D]/30', shade: '#B8228D' },
+ { id: 2, name: 'Volt', color: '#FFD166', textColor: 'text-[#9B6A00]', glow: 'shadow-[#B88711]/30', shade: '#B88711' },
+ { id: 3, name: 'Nova', color: '#7C5CFF', textColor: 'text-[#4931B8]', glow: 'shadow-[#4931B8]/30', shade: '#4931B8' },
+];
+
+const createBoard = () =>
+ Array.from({ length: boardSize }, (_, index) => {
+ if (index === 0 || index === 1 || index === 7) {
+ return 0;
+ }
+
+ if (index === 5 || index === 6 || index === 13) {
+ return 1;
+ }
+
+ if (index === 35 || index === 42 || index === 43) {
+ return 2;
+ }
+
+ if (index === 41 || index === 47 || index === 48) {
+ return 3;
+ }
+
+ return ((index + Math.floor(index / 7)) % 4) as Owner;
+ });
+
+const getCounts = (board: Owner[]) =>
+ players.map((player) => ({
+ ...player,
+ tiles: board.filter((owner) => owner === player.id).length,
+ }));
+
+const loadHistory = (): MatchRecord[] => {
+ if (typeof window === 'undefined') {
+ return [];
+ }
+
+ try {
+ const stored = window.localStorage.getItem(historyKey);
+ if (!stored) {
+ return [];
+ }
+
+ const parsed = JSON.parse(stored);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch (error) {
+ console.error('Failed to load Steal a Color match history', error);
+ return [];
+ }
+};
+
+const saveHistory = (records: MatchRecord[]) => {
+ try {
+ window.localStorage.setItem(historyKey, JSON.stringify(records));
+ } catch (error) {
+ console.error('Failed to save Steal a Color match history', error);
+ }
+};
+
+const formatDate = (value: string) =>
+ new Intl.DateTimeFormat('en', {
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ }).format(new Date(value));
+
+export default function PlayStealAColor() {
+ const [phase, setPhase] = useState('setup');
+ const [playerName, setPlayerName] = useState('Player One');
+ const [board, setBoard] = useState(createBoard);
+ const [secondsLeft, setSecondsLeft] = useState(matchSeconds);
+ const [captured, setCaptured] = useState(0);
+ const [history, setHistory] = useState([]);
+ const [selectedMatchId, setSelectedMatchId] = useState(null);
+ const [message, setMessage] = useState('Name your blocky avatar, start a match, then smack rival color blocks to steal them.');
+
+ useEffect(() => {
+ const records = loadHistory();
+ setHistory(records);
+ setSelectedMatchId(records[0]?.id ?? null);
+ }, []);
+
+ const counts = useMemo(() => getCounts(board), [board]);
+ const leader = useMemo(() => [...counts].sort((a, b) => b.tiles - a.tiles)[0], [counts]);
+ const playerScore = counts[0]?.tiles ?? 0;
+ const selectedMatch = history.find((record) => record.id === selectedMatchId) ?? history[0];
+
+ const finishMatch = useCallback(
+ (finalBoard: Owner[]) => {
+ const finalCounts = getCounts(finalBoard);
+ const winner = [...finalCounts].sort((a, b) => b.tiles - a.tiles)[0];
+ const record: MatchRecord = {
+ id: `${Date.now()}`,
+ playerName: playerName.trim() || 'Player One',
+ score: finalCounts[0].tiles,
+ winner: winner.name,
+ won: winner.id === 0,
+ captured,
+ createdAt: new Date().toISOString(),
+ board: finalBoard,
+ };
+ const nextHistory = [record, ...history].slice(0, 6);
+ setPhase('finished');
+ setMessage(winner.id === 0 ? 'Victory! Your color owns the arena.' : `${winner.name} stole the crown. Hit rematch and take it back.`);
+ setHistory(nextHistory);
+ setSelectedMatchId(record.id);
+ saveHistory(nextHistory);
+ },
+ [captured, history, playerName]
+ );
+
+ useEffect(() => {
+ if (phase !== 'playing') {
+ return undefined;
+ }
+
+ if (secondsLeft <= 0) {
+ finishMatch(board);
+ return undefined;
+ }
+
+ const timer = window.setTimeout(() => setSecondsLeft((current) => current - 1), 1000);
+ return () => window.clearTimeout(timer);
+ }, [board, finishMatch, phase, secondsLeft]);
+
+ useEffect(() => {
+ if (phase !== 'playing') {
+ return undefined;
+ }
+
+ const aiTimer = window.setInterval(() => {
+ setBoard((currentBoard) => {
+ const nextBoard = [...currentBoard];
+ const ai = (1 + Math.floor(Math.random() * 3)) as Owner;
+ const candidates = nextBoard
+ .map((owner, index) => ({ owner, index }))
+ .filter((tile) => tile.owner !== ai);
+
+ if (candidates.length === 0) {
+ return currentBoard;
+ }
+
+ const target = candidates[Math.floor(Math.random() * candidates.length)];
+ nextBoard[target.index] = ai;
+ return nextBoard;
+ });
+ }, 1050);
+
+ return () => window.clearInterval(aiTimer);
+ }, [phase]);
+
+ const startMatch = () => {
+ const safeName = playerName.trim();
+ if (!safeName) {
+ setMessage('Please add a player name before starting.');
+ return;
+ }
+
+ setBoard(createBoard());
+ setSecondsLeft(matchSeconds);
+ setCaptured(0);
+ setPhase('playing');
+ setMessage(`Go ${safeName}! Tap any rival block to flip it mint.`);
+ };
+
+ const stealTile = (index: number) => {
+ if (phase !== 'playing') {
+ setMessage('Press Start first to unlock the arena.');
+ return;
+ }
+
+ setBoard((currentBoard) => {
+ if (currentBoard[index] === 0) {
+ setMessage('That block already belongs to you. Find a rival color.');
+ return currentBoard;
+ }
+
+ const nextBoard = [...currentBoard];
+ nextBoard[index] = 0;
+ setCaptured((current) => current + 1);
+ setMessage('Steal confirmed! Keep the mint wave rolling.');
+ return nextBoard;
+ });
+ };
+
+ const clearHistory = () => {
+ setHistory([]);
+ setSelectedMatchId(null);
+ saveHistory([]);
+ setMessage('Local match history cleared. The arena is reset.');
+ };
+
+ return (
+ <>
+
+ {getPageTitle('Play Steal a Color')}
+
+
+
+
+
+
+
+
+
+
+
+
+ {players.map((player) => (
+
+ ))}
+
+
+ Steal a Color
+ Block Arena
+
+
+
+
+
+ Landing
+
+
+ Admin Login
+
+
+
+
+
+
+
+
+
+
+
Arena
+
Tap-to-steal blocks
+
+
+
+ Time
+ 0:{String(secondsLeft).padStart(2, '0')}
+
+
+ Your blocks
+ {playerScore}
+
+
+
+
+
+ {counts.map((player) => (
+
+
+
{player.name === 'You' ? playerName || 'You' : player.name}
+
{player.tiles}
+
+ ))}
+
+
+
+ {board.map((owner, index) => {
+ const tileOwner = players[owner];
+ return (
+ stealTile(index)}
+ className={`relative aspect-square overflow-hidden rounded-xl border-2 border-white/50 shadow-lg ${tileOwner.glow} transition duration-150 hover:-translate-y-1 hover:scale-[1.02] focus:outline-none focus:ring-4 focus:ring-white ${owner === 0 ? 'ring-2 ring-[#087560]' : ''} ${phase === 'playing' ? 'cursor-pointer' : 'cursor-default'}`}
+ style={{
+ backgroundColor: tileOwner.color,
+ boxShadow: `inset 0 6px 0 rgba(255,255,255,0.42), inset 0 -8px 0 rgba(0,0,0,0.2), 0 8px 0 ${tileOwner.shade}, 0 14px 18px rgba(0,0,0,0.22)`,
+ }}
+ >
+
+ {tileOwner.name}
+
+ );
+ })}
+
+
+ {phase === 'finished' ? (
+
+
Results
+
+ {leader.id === 0 ? 'You won the color crown!' : `${leader.name} wins this round.`}
+
+
+ {playerName.trim() || 'Player One'} finished with {playerScore} blocks and made {captured} manual steals .
+
+
+ ) : null}
+
+
+
+
+
+
+
Live leaderboard
+
{leader.name} leads
+
+
+ {Math.round((leader.tiles / boardSize) * 100)}%
+
+
+
+
+ {counts.map((player) => {
+ const width = `${Math.max(8, Math.round((player.tiles / boardSize) * 100))}%`;
+ return (
+
+
+ {player.name === 'You' ? playerName || 'You' : player.name}
+ {player.tiles} blocks
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
Recent rounds
+
Local history
+
+
+ Clear
+
+
+
+ {history.length === 0 ? (
+
+ No completed rounds yet. Finish a match to save your first color steal result here.
+
+ ) : (
+
+ {history.map((record) => (
+
setSelectedMatchId(record.id)}
+ className={`w-full rounded-2xl border-4 p-3 text-left font-bold transition hover:-translate-y-0.5 ${selectedMatch?.id === record.id ? 'border-[#159875] bg-[#D9FFF6] shadow-[0_7px_0_#A5DCD0]' : 'border-[#172033] bg-white shadow-[0_7px_0_#9AA7B8]'}`}
+ >
+
+ {record.won ? 'Win' : 'Loss'} • {record.score} blocks
+ {formatDate(record.createdAt)}
+
+ Winner: {record.winner} • Player: {record.playerName}
+
+ ))}
+
+ )}
+
+
+ {selectedMatch ? (
+
+ Match detail
+ {formatDate(selectedMatch.createdAt)}
+
+ {selectedMatch.board.map((owner, index) => (
+
+ ))}
+
+
+ {selectedMatch.playerName} scored {selectedMatch.score} blocks. Winner: {selectedMatch.winner} .
+
+
+ ) : null}
+
+
+
+
+
+ >
+ );
+}
+
+PlayStealAColor.getLayout = function getLayout(page: ReactElement) {
+ return {page} ;
+};