Compare commits

..

12 Commits

Author SHA1 Message Date
Flatlogic Bot
9d43f0198b Edit README.md via Editor 2026-08-08 20:55:51 +00:00
Flatlogic Bot
63e8dbb74c Edit README.md via Editor 2026-08-08 16:50:51 +00:00
Flatlogic Bot
ba77f6de29 Revert to version 3100f21 2026-08-08 16:49:23 +00:00
Flatlogic Bot
347cf90d9a Revert to version 9ef19d6 2026-08-08 16:49:07 +00:00
Flatlogic Bot
6496b9b028 Edit README.md via Editor 2026-08-08 16:48:25 +00:00
Flatlogic Bot
b2361c88cd Edit README.md via Editor 2026-08-08 14:49:25 +00:00
Flatlogic Bot
0a138d49cd Edit README.md via Editor 2026-08-08 14:45:24 +00:00
Flatlogic Bot
80fddf14b6 Edit README.md via Editor 2026-08-08 14:43:30 +00:00
Flatlogic Bot
9e67379715 Edit README.md via Editor 2026-08-08 14:39:47 +00:00
Flatlogic Bot
c78167af3b Edit README.md via Editor 2026-08-08 14:38:29 +00:00
Flatlogic Bot
7a48570466 Edit README.md via Editor 2026-08-08 13:21:06 +00:00
Flatlogic Bot
3100f21471 steal a color 2026-08-07 19:33:12 +00:00
9 changed files with 649 additions and 150 deletions

View File

@ -242,3 +242,26 @@ The **docker folder** contains a couple of helper scripts:
Afterwards, continue to start your project in the backend directory by running:
`yarn start`
admin commands tools for admin@flatlogic.com
admin_pass for admin@flatlogic.com
password for admin@flatlogic.com PASSWORD fac6a658
Google
import { useReportWebVitals } from 'next/web-vitals'
const handleWebVitals = (metric) => {
switch (metric.name) {
case 'FCP': {
// handle FCP results
}
case 'LCP': {
// handle LCP results
}
// ...
}
}
function MyApp({ Component, pageProps }) {
useReportWebVitals(handleWebVitals)
return <Component {...pageProps} />
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View File

@ -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'

View File

@ -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'

View File

@ -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',

View File

@ -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) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<>
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Steal a Color')}</title>
<meta
name="description"
content="A fast, colorful browser territory game with a blocky color-stealing arena."
/>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Steal a Color Game app!"/>
<main className="min-h-screen overflow-hidden bg-[#8DE7FF] text-[#172033]">
<section className="relative isolate flex min-h-screen flex-col px-5 py-6 sm:px-8 lg:px-12">
<div className="absolute inset-0 -z-20 bg-[linear-gradient(180deg,_#75D9FF_0%,_#C9F6FF_50%,_#9BE36F_50%,_#43B02A_100%)]" />
<div className="absolute inset-x-0 bottom-0 -z-10 h-[42%] bg-[linear-gradient(90deg,_rgba(255,255,255,0.16)_1px,_transparent_1px),linear-gradient(0deg,_rgba(255,255,255,0.16)_1px,_transparent_1px)] bg-[size:54px_54px] opacity-70" />
<div className="absolute left-[8%] top-[12%] -z-10 h-16 w-48 rounded-full bg-white/70 blur-sm" />
<div className="absolute right-[14%] top-[9%] -z-10 h-20 w-64 rounded-full bg-white/65 blur-sm" />
<div className="absolute left-[42%] top-[18%] -z-10 h-12 w-36 rounded-full bg-white/55 blur-sm" />
<div className="space-y-3">
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center '>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
{floatingBlocks.map((block) => (
<span
key={block}
className={`absolute -z-10 hidden rounded-xl border-4 border-white/45 shadow-[0_10px_0_rgba(0,0,0,0.22),0_18px_30px_rgba(0,0,0,0.2)] md:block ${block}`}
/>
))}
<header className="mx-auto flex w-full max-w-7xl items-center justify-between rounded-3xl border-4 border-white bg-white/90 px-4 py-3 shadow-[0_10px_0_#B6D7EF,0_18px_35px_rgba(25,60,100,0.22)] backdrop-blur">
<Link href="/" className="flex items-center gap-3 rounded-2xl focus:outline-none focus:ring-4 focus:ring-[#FFD166]">
<span className="grid h-12 w-12 grid-cols-2 overflow-hidden rounded-xl border-4 border-[#172033] shadow-[0_6px_0_#7E8795]">
{colorBursts.slice(0, 4).map((color) => (
<span key={color} className={color} />
))}
</span>
<span>
<span className="block text-xs font-black uppercase tracking-[0.28em] text-[#627084]">Roblox-like</span>
<span className="block text-xl font-black leading-none text-[#172033]">Steal a Color</span>
</span>
</Link>
<nav className="flex items-center gap-2 text-sm font-black">
<Link href="/play" className="hidden rounded-2xl border-2 border-[#172033] bg-[#FFD166] px-4 py-2 text-[#172033] shadow-[0_5px_0_#B88711] transition hover:-translate-y-0.5 hover:shadow-[0_7px_0_#B88711] sm:inline-flex">
Play
</Link>
<Link href="/login" className="rounded-2xl border-2 border-[#172033] bg-white px-4 py-2 text-[#172033] shadow-[0_5px_0_#9AA7B8] transition hover:-translate-y-0.5 hover:shadow-[0_7px_0_#9AA7B8]">
Admin Login
</Link>
</nav>
</header>
<div className="mx-auto grid w-full max-w-7xl flex-1 items-center gap-12 py-12 lg:grid-cols-[1fr_1fr] lg:py-16">
<div className="max-w-3xl">
<div className="mb-6 inline-flex items-center gap-3 rounded-2xl border-4 border-[#172033] bg-white px-4 py-3 text-sm font-black uppercase tracking-[0.18em] text-[#172033] shadow-[0_8px_0_#9AA7B8]">
<span className="h-4 w-4 rounded bg-[#35F2C1] shadow-[0_4px_0_#159875]" />
Color stealing block arena
</div>
<h1 className="max-w-3xl text-5xl font-black leading-[0.95] tracking-tight text-[#172033] drop-shadow-[0_5px_0_rgba(255,255,255,0.9)] sm:text-6xl lg:text-7xl">
Jump in, slap a color block, and steal the whole map.
</h1>
<p className="mt-6 max-w-2xl rounded-3xl border-4 border-white bg-white/85 p-5 text-lg font-bold leading-8 text-[#42526B] shadow-[0_10px_0_#B6D7EF] sm:text-xl">
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.
</p>
<div className="mt-8 flex flex-col gap-4 sm:flex-row">
<Link href="/play" className="inline-flex items-center justify-center rounded-2xl border-4 border-[#172033] bg-[#35F2C1] px-7 py-4 text-base font-black uppercase tracking-[0.08em] text-[#07111F] shadow-[0_9px_0_#159875,0_18px_26px_rgba(21,152,117,0.26)] transition hover:-translate-y-1 hover:shadow-[0_12px_0_#159875,0_22px_32px_rgba(21,152,117,0.26)] focus:outline-none focus:ring-4 focus:ring-[#FFD166]">
Start a Match
</Link>
<Link href="/login" className="inline-flex items-center justify-center rounded-2xl border-4 border-[#172033] bg-[#FF4FD8] px-7 py-4 text-base font-black uppercase tracking-[0.08em] text-white shadow-[0_9px_0_#B8228D,0_18px_26px_rgba(184,34,141,0.24)] transition hover:-translate-y-1 hover:shadow-[0_12px_0_#B8228D,0_22px_32px_rgba(184,34,141,0.24)] focus:outline-none focus:ring-4 focus:ring-[#FFD166]">
Open Admin
</Link>
</div>
<dl className="mt-10 grid max-w-2xl grid-cols-3 gap-3">
{arenaStats.map(([value, label]) => (
<div key={label} className="rounded-2xl border-4 border-[#172033] bg-white p-4 text-center shadow-[0_8px_0_#9AA7B8]">
<dt className="text-3xl font-black text-[#172033] sm:text-4xl">{value}</dt>
<dd className="mt-1 text-[0.65rem] font-black uppercase tracking-[0.2em] text-[#627084] sm:text-xs">{label}</dd>
</div>
))}
</dl>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
<div className="relative mx-auto w-full max-w-xl">
<div className="absolute -bottom-8 left-1/2 h-20 w-[92%] -translate-x-1/2 rounded-[50%] bg-[#1E7C22]/35 blur-xl" />
<div className="relative rounded-[2rem] border-4 border-[#172033] bg-[#7BD957] p-5 shadow-[0_16px_0_#2F8A24,0_30px_40px_rgba(20,76,32,0.28)]">
<div className="mb-4 flex items-center justify-between rounded-2xl border-4 border-white bg-white/90 px-4 py-3 shadow-[0_7px_0_#B6D7EF]">
<div>
<p className="text-xs font-black uppercase tracking-[0.28em] text-[#627084]">Live board</p>
<p className="text-2xl font-black text-[#172033]">Preview Arena</p>
</div>
<span className="rounded-xl border-2 border-[#172033] bg-[#FF4FD8] px-3 py-2 text-sm font-black text-white shadow-[0_5px_0_#B8228D]">00:45</span>
</div>
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<div className="relative rounded-[1.5rem] border-4 border-[#172033] bg-[#244A2B] p-3 shadow-[inset_0_8px_0_rgba(255,255,255,0.16)]">
<div className="pointer-events-none absolute -top-8 left-8 flex items-end gap-2">
<span className="block h-7 w-7 rounded-lg border-2 border-[#172033] bg-[#FFD1A3] shadow-[0_4px_0_#B6783C]" />
<span className="block h-10 w-9 rounded-lg border-2 border-[#172033] bg-[#35F2C1] shadow-[0_6px_0_#159875]" />
</div>
<div className="grid grid-cols-7 gap-2">
{Array.from({ length: 49 }).map((_, index) => {
const color = colorBursts[(index * 5 + Math.floor(index / 7)) % colorBursts.length];
return (
<div
key={index}
className={`${color} aspect-square rounded-xl border-2 border-white/45 shadow-[inset_0_5px_0_rgba(255,255,255,0.45),inset_0_-6px_0_rgba(0,0,0,0.18),0_7px_0_rgba(0,0,0,0.22)] transition duration-300 hover:-translate-y-1`}
/>
);
})}
</div>
</div>
</div>
<div className="mt-5 grid grid-cols-2 gap-3 sm:grid-cols-4">
{[
['You', '31%', '#35F2C1', '#159875'],
['Pixel', '26%', '#FF4FD8', '#B8228D'],
['Volt', '23%', '#FFD166', '#B88711'],
['Nova', '20%', '#7C5CFF', '#4931B8'],
].map(([name, score, color, shade]) => (
<div key={name} className="rounded-2xl border-4 border-[#172033] bg-white p-3 shadow-[0_7px_0_#9AA7B8]">
<div className="mb-2 h-5 rounded-lg border-2 border-[#172033]" style={{ backgroundColor: color, boxShadow: `0 4px 0 ${shade}` }} />
<p className="text-sm font-black text-[#172033]">{name}</p>
<p className="text-xs font-bold text-[#627084]">{score} control</p>
</div>
))}
</div>
</div>
</div>
</div>
</section>
</main>
</>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

471
frontend/src/pages/play.tsx Normal file
View File

@ -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<Phase>('setup');
const [playerName, setPlayerName] = useState('Player One');
const [board, setBoard] = useState<Owner[]>(createBoard);
const [secondsLeft, setSecondsLeft] = useState(matchSeconds);
const [captured, setCaptured] = useState(0);
const [history, setHistory] = useState<MatchRecord[]>([]);
const [selectedMatchId, setSelectedMatchId] = useState<string | null>(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 (
<>
<Head>
<title>{getPageTitle('Play Steal a Color')}</title>
</Head>
<main className="min-h-screen overflow-hidden bg-[#8DE7FF] text-[#172033]">
<section className="relative px-4 py-5 sm:px-6 lg:px-8">
<div className="absolute inset-0 -z-20 bg-[linear-gradient(180deg,_#73D9FF_0%,_#D8FAFF_48%,_#95DE69_48%,_#42AF2A_100%)]" />
<div className="absolute inset-x-0 bottom-0 -z-10 h-[50%] bg-[linear-gradient(90deg,_rgba(255,255,255,0.18)_1px,_transparent_1px),linear-gradient(0deg,_rgba(255,255,255,0.18)_1px,_transparent_1px)] bg-[size:50px_50px] opacity-75" />
<div className="absolute left-[5%] top-16 -z-10 h-14 w-48 rounded-full bg-white/70 blur-sm" />
<div className="absolute right-[9%] top-24 -z-10 h-16 w-60 rounded-full bg-white/65 blur-sm" />
<div className="relative mx-auto max-w-7xl">
<header className="mb-6 flex flex-col gap-3 rounded-3xl border-4 border-white bg-white/90 p-3 shadow-[0_10px_0_#B6D7EF,0_18px_35px_rgba(25,60,100,0.22)] backdrop-blur md:flex-row md:items-center md:justify-between">
<Link href="/" className="flex items-center gap-3 rounded-2xl px-2 py-1 focus:outline-none focus:ring-4 focus:ring-[#FFD166]">
<span className="grid h-12 w-12 grid-cols-2 overflow-hidden rounded-xl border-4 border-[#172033] shadow-[0_6px_0_#7E8795]">
{players.map((player) => (
<span key={player.id} style={{ backgroundColor: player.color }} />
))}
</span>
<span>
<span className="block text-xs font-black uppercase tracking-[0.25em] text-[#627084]">Steal a Color</span>
<span className="text-2xl font-black text-[#172033]">Block Arena</span>
</span>
</Link>
<nav className="flex flex-wrap gap-2 text-sm font-black">
<Link href="/" className="rounded-2xl border-2 border-[#172033] bg-[#FFD166] px-4 py-2 text-[#172033] shadow-[0_5px_0_#B88711] transition hover:-translate-y-0.5">
Landing
</Link>
<Link href="/login" className="rounded-2xl border-2 border-[#172033] bg-white px-4 py-2 text-[#172033] shadow-[0_5px_0_#9AA7B8] transition hover:-translate-y-0.5">
Admin Login
</Link>
</nav>
</header>
<div className="grid gap-6 lg:grid-cols-[21rem_1fr_22rem]">
<aside className="space-y-4">
<section className="rounded-3xl border-4 border-[#172033] bg-white p-5 shadow-[0_12px_0_#9AA7B8,0_22px_30px_rgba(23,32,51,0.18)]">
<p className="inline-flex rounded-xl border-2 border-[#172033] bg-[#FF4FD8] px-3 py-2 text-xs font-black uppercase tracking-[0.2em] text-white shadow-[0_5px_0_#B8228D]">Match setup</p>
<h1 className="mt-5 text-3xl font-black leading-tight tracking-tight text-[#172033]">Claim the bright blocks before the rivals steal them.</h1>
<label className="mt-5 block text-sm font-black uppercase tracking-[0.18em] text-[#627084]" htmlFor="playerName">
Player name
</label>
<input
id="playerName"
value={playerName}
maxLength={24}
disabled={phase === 'playing'}
onChange={(event) => setPlayerName(event.target.value)}
className="mt-2 w-full rounded-2xl border-4 border-[#172033] bg-[#F7FBFF] px-4 py-3 font-black text-[#172033] outline-none shadow-[inset_0_5px_0_rgba(255,255,255,0.85),0_6px_0_#9AA7B8] transition placeholder:text-[#8B9AB0] focus:ring-4 focus:ring-[#FFD166] disabled:cursor-not-allowed disabled:opacity-60"
placeholder="Your nickname"
/>
<div className="mt-5 grid grid-cols-2 gap-3">
<button
type="button"
onClick={startMatch}
className="rounded-2xl border-4 border-[#172033] bg-[#35F2C1] px-4 py-3 text-sm font-black uppercase tracking-[0.08em] text-[#07111F] shadow-[0_8px_0_#159875] transition hover:-translate-y-0.5 hover:shadow-[0_10px_0_#159875] focus:outline-none focus:ring-4 focus:ring-[#FFD166]"
>
{phase === 'finished' ? 'Rematch' : 'Start'}
</button>
<button
type="button"
onClick={() => finishMatch(board)}
disabled={phase !== 'playing'}
className="rounded-2xl border-4 border-[#172033] bg-[#FF4FD8] px-4 py-3 text-sm font-black uppercase tracking-[0.08em] text-white shadow-[0_8px_0_#B8228D] transition hover:-translate-y-0.5 hover:shadow-[0_10px_0_#B8228D] focus:outline-none focus:ring-4 focus:ring-[#FFD166] disabled:cursor-not-allowed disabled:opacity-40"
>
Finish
</button>
</div>
<div className="mt-5 rounded-2xl border-4 border-[#159875] bg-[#D9FFF6] p-4 text-sm font-bold leading-6 text-[#087560] shadow-[0_7px_0_#A5DCD0]">
{message}
</div>
</section>
<section className="rounded-3xl border-4 border-[#172033] bg-[#FFD166] p-5 shadow-[0_10px_0_#B88711]">
<p className="text-xs font-black uppercase tracking-[0.25em] text-[#775000]">How to steal</p>
<ol className="mt-3 space-y-3 text-sm font-black leading-6 text-[#172033]">
<li><strong>1.</strong> You are mint. Tap a rival block to flip it.</li>
<li><strong>2.</strong> AI rivals steal back every second.</li>
<li><strong>3.</strong> Biggest color count after 45 seconds wins.</li>
</ol>
</section>
</aside>
<section className="rounded-[2rem] border-4 border-[#172033] bg-[#7BD957] p-3 shadow-[0_16px_0_#2F8A24,0_28px_40px_rgba(20,76,32,0.28)] sm:p-5">
<div className="mb-4 flex flex-col gap-3 rounded-3xl border-4 border-white bg-white/90 p-4 shadow-[0_8px_0_#B6D7EF] sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-xs font-black uppercase tracking-[0.28em] text-[#627084]">Arena</p>
<h2 className="text-3xl font-black text-[#172033]">Tap-to-steal blocks</h2>
</div>
<div className="flex items-center gap-3">
<div className="rounded-2xl border-4 border-[#172033] bg-[#FF4FD8] px-4 py-2 text-center font-black text-white shadow-[0_7px_0_#B8228D]">
<span className="block text-xs uppercase tracking-[0.2em] text-white/80">Time</span>
<span className="text-3xl">0:{String(secondsLeft).padStart(2, '0')}</span>
</div>
<div className="rounded-2xl border-4 border-[#172033] bg-white px-4 py-2 text-center font-black text-[#172033] shadow-[0_7px_0_#9AA7B8]">
<span className="block text-xs uppercase tracking-[0.2em] text-[#627084]">Your blocks</span>
<span className="text-3xl">{playerScore}</span>
</div>
</div>
</div>
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
{counts.map((player) => (
<div key={player.id} className="rounded-2xl border-4 border-[#172033] bg-white p-2 text-center shadow-[0_7px_0_#9AA7B8]">
<span className="mx-auto block h-8 w-8 rounded-lg border-2 border-[#172033]" style={{ backgroundColor: player.color, boxShadow: `0 5px 0 ${player.shade}` }} />
<p className="mt-2 truncate text-xs font-black text-[#172033]">{player.name === 'You' ? playerName || 'You' : player.name}</p>
<p className="text-[0.65rem] font-black uppercase tracking-[0.16em] text-[#627084]">{player.tiles}</p>
</div>
))}
</div>
<div className="grid grid-cols-7 gap-2 rounded-[1.5rem] border-4 border-[#172033] bg-[#244A2B] p-2 shadow-[inset_0_8px_0_rgba(255,255,255,0.16)] sm:gap-3 sm:p-4">
{board.map((owner, index) => {
const tileOwner = players[owner];
return (
<button
key={`${index}-${owner}`}
type="button"
aria-label={`Tile ${index + 1}, owned by ${tileOwner.name}`}
onClick={() => 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)`,
}}
>
<span className="absolute left-2 top-2 h-2 w-1/2 rounded-full bg-white/45" />
<span className="sr-only">{tileOwner.name}</span>
</button>
);
})}
</div>
{phase === 'finished' ? (
<div className="mt-5 rounded-3xl border-4 border-[#172033] bg-white p-5 text-[#172033] shadow-[0_10px_0_#9AA7B8]">
<p className="text-xs font-black uppercase tracking-[0.25em] text-[#627084]">Results</p>
<h3 className="mt-2 text-3xl font-black">
{leader.id === 0 ? 'You won the color crown!' : `${leader.name} wins this round.`}
</h3>
<p className="mt-2 font-bold text-[#42526B]">
{playerName.trim() || 'Player One'} finished with <strong>{playerScore} blocks</strong> and made <strong>{captured} manual steals</strong>.
</p>
</div>
) : null}
</section>
<aside className="space-y-4">
<section className="rounded-3xl border-4 border-[#172033] bg-white p-5 shadow-[0_12px_0_#9AA7B8,0_22px_30px_rgba(23,32,51,0.16)]">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-xs font-black uppercase tracking-[0.25em] text-[#627084]">Live leaderboard</p>
<h2 className="text-2xl font-black text-[#172033]">{leader.name} leads</h2>
</div>
<span className="rounded-xl border-2 border-[#172033] px-3 py-2 text-xs font-black text-[#07111F] shadow-[0_5px_0_rgba(0,0,0,0.22)]" style={{ backgroundColor: leader.color }}>
{Math.round((leader.tiles / boardSize) * 100)}%
</span>
</div>
<div className="mt-5 space-y-4">
{counts.map((player) => {
const width = `${Math.max(8, Math.round((player.tiles / boardSize) * 100))}%`;
return (
<div key={player.id}>
<div className="mb-1 flex items-center justify-between text-sm font-black">
<span className={player.textColor}>{player.name === 'You' ? playerName || 'You' : player.name}</span>
<span className="text-[#172033]">{player.tiles} blocks</span>
</div>
<div className="h-5 overflow-hidden rounded-xl border-2 border-[#172033] bg-[#E8F2FF] shadow-[inset_0_3px_0_rgba(255,255,255,0.9)]">
<div className="h-full rounded-lg border-r-2 border-[#172033]/30" style={{ width, backgroundColor: player.color, boxShadow: `inset 0 4px 0 rgba(255,255,255,0.35), inset 0 -4px 0 ${player.shade}` }} />
</div>
</div>
);
})}
</div>
</section>
<section className="rounded-3xl border-4 border-[#172033] bg-[#DDFBFF] p-5 shadow-[0_10px_0_#77BFD0]">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-black uppercase tracking-[0.25em] text-[#3E7582]">Recent rounds</p>
<h2 className="text-2xl font-black text-[#172033]">Local history</h2>
</div>
<button type="button" onClick={clearHistory} className="rounded-xl border-2 border-[#172033] bg-white px-3 py-1 text-xs font-black text-[#172033] shadow-[0_4px_0_#9AA7B8] transition hover:-translate-y-0.5">
Clear
</button>
</div>
{history.length === 0 ? (
<div className="mt-4 rounded-2xl border-4 border-dashed border-[#3E7582] bg-white/65 p-4 text-sm font-bold leading-6 text-[#42526B]">
No completed rounds yet. Finish a match to save your first color steal result here.
</div>
) : (
<div className="mt-4 space-y-3">
{history.map((record) => (
<button
type="button"
key={record.id}
onClick={() => 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]'}`}
>
<div className="flex items-center justify-between gap-3">
<span className="font-black text-[#172033]">{record.won ? 'Win' : 'Loss'} {record.score} blocks</span>
<span className="text-xs text-[#627084]">{formatDate(record.createdAt)}</span>
</div>
<p className="mt-1 text-xs text-[#627084]">Winner: {record.winner} Player: {record.playerName}</p>
</button>
))}
</div>
)}
</section>
{selectedMatch ? (
<section className="rounded-3xl border-4 border-[#172033] bg-white p-5 shadow-[0_10px_0_#9AA7B8]">
<p className="text-xs font-black uppercase tracking-[0.25em] text-[#627084]">Match detail</p>
<h2 className="mt-1 text-xl font-black text-[#172033]">{formatDate(selectedMatch.createdAt)}</h2>
<div className="mt-4 grid grid-cols-7 gap-1 rounded-2xl border-4 border-[#172033] bg-[#244A2B] p-2">
{selectedMatch.board.map((owner, index) => (
<span key={`${selectedMatch.id}-${index}`} className="aspect-square rounded-md border border-white/40" style={{ backgroundColor: players[owner].color }} />
))}
</div>
<p className="mt-3 text-sm font-bold leading-6 text-[#42526B]">
{selectedMatch.playerName} scored <strong className="text-[#172033]">{selectedMatch.score}</strong> blocks. Winner: <strong className="text-[#172033]">{selectedMatch.winner}</strong>.
</p>
</section>
) : null}
</aside>
</div>
</div>
</section>
</main>
</>
);
}
PlayStealAColor.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};