nitin-rawat/anycoder-d958a76f
0
1# Use official Node.js LTS2FROM node:18-alpine AS deps3WORKDIR /app4COPY package.json package-lock.json* ./5RUN npm ci --legacy-peer-deps6 7FROM node:18-alpine AS builder8WORKDIR /app9COPY --from=deps /app/node_modules ./node_modules10COPY . .11ENV NEXT_TELEMETRY_DISABLED=112RUN npm run build13 14FROM node:18-alpine AS runner15WORKDIR /app16ENV NODE_ENV=production17ENV NEXT_TELEMETRY_DISABLED=118# Create non-root user19RUN addgroup -S nextjs && adduser -S nextjs -G nextjs20COPY --from=builder /app/public ./public21COPY --from=builder /app/.next/standalone ./22COPY --from=builder /app/.next/static ./.next/static23COPY --from=builder /app/.next/standalone/public ./public24USER nextjs25EXPOSE 786026ENV PORT=786027CMD ["node", "server.js"]28 29=== package.json ===30{31 "name": "lego-mario-nextjs",32 "version": "1.0.1",33 "private": true,34 "scripts": {35 "dev": "next dev -p 3000",36 "build": "next build",37 "start": "next start -p ${PORT:-3000}",38 "lint": "next lint"39 },40 "dependencies": {41 "next": "14.2.10",42 "react": "18.3.1",43 "react-dom": "18.3.1"44 },45 "devDependencies": {46 "autoprefixer": "10.4.20",47 "eslint": "8.57.0",48 "eslint-config-next": "14.2.10",49 "postcss": "8.4.49",50 "tailwindcss": "3.4.14"51 }52}53 54=== next.config.js ===55/** @type {import('next').NextConfig} */56const nextConfig = {57 output: 'standalone',58 reactStrictMode: true,59 images: {60 unoptimized: true61 },62 experimental: {63 typedRoutes: false64 },65 eslint: {66 ignoreDuringBuilds: true67 }68};69module.exports = nextConfig;70 71=== postcss.config.js ===72module.exports = {73 plugins: {74 tailwindcss: {},75 autoprefixer: {}76 }77};78 79=== tailwind.config.js ===80module.exports = {81 content: [82 "./pages/**/*.{js,jsx}",83 "./components/**/*.{js,jsx}"84 ],85 theme: {86 extend: {87 colors: {88 brick: {89 DEFAULT: "#C0392B",90 dark: "#922B21",91 light: "#E74C3C"92 },93 plate: {94 DEFAULT: "#2ECC71",95 dark: "#27AE60"96 },97 sky: {98 DEFAULT: "#A7D3FF",99 light: "#CBE6FF"100 },101 coin: {102 DEFAULT: "#F1C40F",103 dark: "#D4AC0D"104 }105 },106 boxShadow: {107 stud: "inset 0 2px 0 rgba(255,255,255,0.3), inset 0 -3px 0 rgba(0,0,0,0.25)"108 },109 keyframes: {110 bob: {111 "0%,100%": { transform: "translateY(0)" },112 "50%": { transform: "translateY(-4px)" }113 },114 blink: {115 "0%, 90%, 100%": { opacity: "1" },116 "95%": { opacity: "0.2" }117 }118 },119 animation: {120 bob: "bob 1.6s ease-in-out infinite",121 blink: "blink 3s linear infinite"122 }123 }124 },125 plugins: []126};127 128=== components/Header.jsx ===129import Link from "next/link";130 131export default function Header() {132 return (133 <header className="w-full bg-sky-light border-b border-sky/50">134 <div className="mx-auto max-w-6xl px-4 py-3 flex items-center justify-between">135 <div className="flex items-center gap-3">136 <div className="w-8 h-8 rounded-md bg-brick shadow-stud border-2 border-brick-dark" aria-hidden="true" />137 <h1 className="text-xl font-extrabold tracking-tight text-brick-dark">138 LEGO Mario139 <span className="sr-only">Lego Mario Game</span>140 </h1>141 </div>142 <nav aria-label="Primary">143 <ul className="flex items-center gap-4">144 <li>145 <a146 href="https://huggingface.co/spaces/akhaliq/anycoder"147 target="_blank"148 rel="noopener noreferrer"149 className="text-sm font-semibold text-blue-700 hover:text-blue-900 underline underline-offset-4"150 >151 Built with anycoder152 </a>153 </li>154 <li>155 <Link href="#how-to-play" className="text-sm text-slate-800 hover:text-slate-900">156 How to play157 </Link>158 </li>159 <li>160 <Link href="#credits" className="text-sm text-slate-800 hover:text-slate-900">161 Credits162 </Link>163 </li>164 </ul>165 </nav>166 </div>167 </header>168 );169}170 171=== components/Loading.jsx ===172export default function Loading({ label = "Loading..." }) {173 return (174 <div role="status" aria-live="polite" className="w-full flex items-center justify-center py-8">175 <div className="flex items-center gap-3">176 <div className="w-4 h-4 rounded-full bg-brick animate-bounce" />177 <div className="w-4 h-4 rounded-full bg-plate animate-bounce [animation-delay:.15s]" />178 <div className="w-4 h-4 rounded-full bg-coin animate-bounce [animation-delay:.3s]" />179 </div>180 <span className="sr-only">{label}</span>181 </div>182 );183}184 185=== components/LegoMario.jsx ===186function StudRow({ count = 4 }) {187 return (188 <div className="flex gap-1">189 {Array.from({ length: count }).map((_, i) => (190 <div key={i} className="w-2 h-2 rounded-full bg-white/40 shadow-stud" aria-hidden="true" />191 ))}192 </div>193 );194}195 196export default function LegoMario({ facing = 1, jumping = false }) {197 const dir = facing === 1 ? "" : "-scale-x-100";198 return (199 <div className={`relative select-none ${dir} transition-transform`} aria-label="Lego-style Mario character" role="img">200 <div className="w-12 h-10 bg-brick rounded-md border-2 border-brick-dark relative">201 <div className="absolute -top-2 left-1 right-1 flex justify-between">202 <StudRow count={5} />203 </div>204 </div>205 <div className="absolute -top-8 left-1/2 -translate-x-1/2 w-10 h-8 bg-coin rounded-md border-2 border-coin-dark">206 <div className="absolute top-1 left-2 w-2 h-2 bg-black rounded-full animate-blink" />207 <div className="absolute top-1 right-2 w-2 h-2 bg-black rounded-full animate-blink [animation-delay:.2s]" />208 <div className="absolute -top-2 left-1 right-1">209 <StudRow count={4} />210 </div>211 </div>212 <div className="absolute -top-11 left-1/2 -translate-x-1/2 w-12 h-3 bg-brick rounded-md border-2 border-brick-dark" />213 <div className="absolute top-2 -left-4 w-4 h-6 bg-coin rounded-md border-2 border-coin-dark" />214 <div className="absolute top-2 -right-4 w-4 h-6 bg-coin rounded-md border-2 border-coin-dark" />215 <div className={`absolute -bottom-3 left-1 w-4 h-6 bg-plate rounded-md border-2 border-plate-dark ${jumping ? "-rotate-6 -translate-y-1" : ""}`} />216 <div className={`absolute -bottom-3 right-1 w-4 h-6 bg-plate rounded-md border-2 border-plate-dark ${jumping ? "rotate-6 -translate-y-1" : ""}`} />217 </div>218 );219}220 221=== components/LegoWorld.jsx ===222import { useEffect, useRef, useState } from "react";223import LegoMario from "./LegoMario";224 225const TILE = 32;226const GRAVITY = 0.8;227const JUMP_VELOCITY = -12;228const SPEED = 4;229 230export default function LegoWorld() {231 const [ready, setReady] = useState(false);232 const [state, setState] = useState({233 x: 50,234 y: 0,235 vx: 0,236 vy: 0,237 facing: 1,238 jumping: false,239 coins: 0,240 gameOver: false241 });242 243 const level = {244 width: 64,245 height: 12,246 groundY: 9,247 coins: [248 { x: 10, y: 7, collected: false },249 { x: 15, y: 5, collected: false },250 { x: 22, y: 7, collected: false },251 { x: 30, y: 6, collected: false },252 { x: 42, y: 7, collected: false }253 ],254 blocks: [255 { x: 12, y: 8 },256 { x: 13, y: 8 },257 { x: 20, y: 8 },258 { x: 28, y: 7 }259 ]260 };261 262 const keys = useRef({ ArrowLeft: false, ArrowRight: false, Space: false, KeyZ: false, KeyX: false });263 264 useEffect(() => {265 function onDown(e) {266 if (keys.current[e.code] !== undefined) keys.current[e.code] = true;267 if (e.code === "Space") e.preventDefault();268 }269 function onUp(e) {270 if (keys.current[e.code] !== undefined) keys.current[e.code] = false;271 }272 window.addEventListener("keydown", onDown);273 window.addEventListener("keyup", onUp);274 setReady(true);275 return () => {276 window.removeEventListener("keydown", onDown);277 window.removeEventListener("keyup", onUp);278 };279 }, []);280 281 useEffect(() => {282 if (!ready) return;283 let raf;284 const tick = () => {285 setState(prev => {286 if (prev.gameOver) return prev;287 288 let { x, y, vx, vy, jumping, facing, coins } = prev;289 290 const left = keys.current.ArrowLeft;291 const right = keys.current.ArrowRight;292 if (left && !right) {293 vx = -SPEED;294 facing = -1;295 } else if (right && !left) {296 vx = SPEED;297 facing = 1;298 } else {299 vx = 0;300 }301 302 const wantJump = keys.current.Space || keys.current.KeyZ || keys.current.KeyX;303 if (wantJump && !jumping) {304 vy = JUMP_VELOCITY;305 jumping = true;306 }307 308 vy += GRAVITY;309 x += vx;310 y += vy;311 312 const groundPx = level.groundY * TILE;313 if (y >= groundPx) {314 y = groundPx;315 vy = 0;316 jumping = false;317 }318 319 level.blocks.forEach(b => {320 const bx = b.x * TILE;321 const by = b.y * TILE;322 const bw = TILE;323 const bh = TILE;324 const mw = 20;325 const mh = 28;326 const mx = x;327 const my = y - mh;328 329 const overlapX = Math.max(0, Math.min(mx + mw, bx + bw) - Math.min(mx, bx + bw));330 const overlapY = Math.max(0, Math.min(my + mh, by + bh) - Math.max(my, by));331 if (overlapX > 0 && overlapY > 0) {332 if (vy > 0 && my + mh - vy <= by) {333 y = by;334 vy = 0;335 jumping = false;336 }337 }338 });339 340 const cx = x + 10;341 const cy = y - 20;342 level.coins.forEach(c => {343 if (c.collected) return;344 const px = c.x * TILE + TILE / 2;345 const py = c.y * TILE + TILE / 2;346 const dx = cx - px;347 const dy = cy - py;348 if (dx * dx + dy * dy < 20 * 20) {349 c.collected = true;350 coins += 1;351 }352 });353 354 const minX = 0;355 const maxX = level.width * TILE - 40;356 if (x < minX) x = minX;357 if (x > maxX) x = maxX;358 359 return { ...prev, x, y, vx, vy, jumping, facing, coins };360 });361 raf = requestAnimationFrame(tick);362 };363 raf = requestAnimationFrame(tick);364 return () => cancelAnimationFrame(raf);365 }, [ready]);366 367 const camX = Math.max(0, Math.min(state.x - 200, level.width * TILE - 400));368 369 return (370 <div className="relative w-full h-[420px] overflow-hidden rounded-xl border border-sky/60 bg-sky">371 <div className="absolute top-2 left-2 z-20 px-3 py-1 rounded-md bg-white/70 backdrop-blur text-slate-800 text-sm font-semibold">372 Coins: {state.coins}373 </div>374 375 <div className="absolute inset-0" aria-hidden="true">376 <div className="absolute top-6 left-10 w-24 h-6 bg-sky-light rounded-md border border-white/50" />377 <div className="absolute top-16 left-56 w-16 h-5 bg-sky-light rounded-md border border-white/50" />378 <div className="absolute top-10 left-96 w-28 h-6 bg-sky-light rounded-md border border-white/50" />379 </div>380 381 <div className="absolute bottom-0 left-0 right-0 h-[384px]">382 <div className="absolute inset-x-0 bottom-[96px] h-[8px] bg-plate-dark/40" />383 <div className="absolute bottom-0 left-0 h-[96px] w-[4096px] bg-plate border-t-4 border-plate-dark [transform:translateX(var(--cam))]" style={{ "--cam": `-${camX}px` }}>384 <div className="absolute top-2 left-2 right-2 grid grid-cols-16 gap-2">385 {Array.from({ length: 64 }).map((_, i) => (386 <div key={i} className="w-3 h-3 rounded-full bg-white/40 shadow-stud" />387 ))}388 </div>389 </div>390 391 <div className="absolute inset-x-0 bottom-[96px] h-[288px] [transform:translateX(var(--cam))]" style={{ "--cam": `-${camX}px` }}>392 {level.blocks.map((b, i) => (393 <div394 key={i}395 className={`absolute w-8 h-8 bg-brick border-2 border-brick-dark rounded-sm [left:${b.x * TILE}px] [bottom:${(b.y - level.groundY) * TILE}px]`}396 >397 <div className="absolute inset-1 grid grid-cols-2 gap-1">398 {Array.from({ length: 4 }).map((_, j) => (399 <div key={j} className="rounded-full bg-white/40 shadow-stud" />400 ))}401 </div>402 </div>403 ))}404 405 {level.coins.map((c, i) =>406 c.collected ? null : (407 <div key={i} className={`absolute [left:${c.x * TILE + 8}px] [bottom:${(c.y - level.groundY) * TILE + 40}px]`}>408 <div className="w-6 h-6 rounded-full bg-coin border-2 border-coin-dark animate-bob" aria-label="Coin" />409 </div>410 )411 )}412 </div>413 414 <div className="absolute bottom-[96px] [transform:translateX(var(--mx))]" style={{ "--mx": `${state.x - camX}px` }}>415 <div className="[transform:translateY(var(--my))]" style={{ "--my": `${state.y - level.groundY * TILE}px` }}>416 <LegoMario facing={state.facing} jumping={state.jumping} />417 </div>418 </div>419 </div>420 421 <div className="absolute bottom-2 right-2 z-20 px-3 py-1 rounded-md bg-white/70 backdrop-blur text-slate-800 text-xs">422 Controls: ← → to move, Space/Z/X to jump423 </div>424 </div>425 );426}427 428=== components/Seo.jsx ===429import Head from "next/head";430 431export default function Seo() {432 return (433 <Head>434 <title>LEGO Mario - Next.js</title>435 <meta name="description" content="A LEGO-styled Mario platformer built with Next.js and Tailwind CSS." />436 <meta name="viewport" content="width=device-width, initial-scale=1" />437 </Head>438 );439}440 441=== pages/_app.js ===442import "@/styles/globals.css";443 444export default function App({ Component, pageProps }) {445 return <Component {...pageProps} />;446}447 448=== pages/index.js ===449import dynamic from "next/dynamic";450import Header from "@/components/Header";451import Seo from "@/components/Seo";452import Loading from "@/components/Loading";453 454const LegoWorld = dynamic(() => import("@/components/LegoWorld"), {455 ssr: false,456 loading: () => <Loading label="Loading world..." />457});458 459export default function Home() {460 return (461 <>462 <Seo />463 <Header />464 <main className="min-h-screen bg-sky-light">465 <section className="mx-auto max-w-6xl px-4 py-8">466 <div className="flex flex-col md:flex-row items-start gap-6">467 <div className="flex-1">468 <div className="mb-4">469 <h2 className="text-2xl font-bold text-slate-900">Lego Mario Platformer</h2>470 <p className="text-slate-700 mt-1">471 Run and jump to collect coins in a LEGO-styled world. Fully client-side, responsive, and accessible.472 </p>473 </div>474 <LegoWorld />475 </div>476 <aside className="w-full md:w-72">477 <div id="how-to-play" className="sticky top-4 bg-white rounded-lg border border-slate-200 p-4 shadow-sm">478 <h3 className="text-lg font-semibold text-slate-900">How to play</h3>479 <ul className="mt-2 list-disc pl-5 text-slate-700 space-y-1">480 <li>Use Left and Right arrow keys to move.</li>481 <li>Press Space, Z, or X to jump.</li>482 <li>Collect as many coins as you can.</li>483 </ul>484 <div className="mt-3 text-xs text-slate-500">485 Tip: The camera follows LEGO Mario. Try jumping on blocks to reach higher coins.486 </div>487 </div>488 </aside>489 </div>490 491 <div id="credits" className="mt-10 text-sm text-slate-600">492 This fan project uses LEGO-inspired shapes and colors; LEGO is a trademark of the LEGO Group, which does not sponsor, authorize or endorse this project.493 </div>494 </section>495 </main>496 <footer className="border-t border-slate-200 bg-white">497 <div className="mx-auto max-w-6xl px-4 py-6 text-sm text-slate-600">498 © {new Date().getFullYear()} LEGO Mario Demo. All rights reserved.499 </div>500 </footer>501 </>502 );503}504 505=== pages/api/health.js ===506export default function handler(req, res) {507 try {508 res.status(200).json({ ok: true, time: new Date().toISOString() });509 } catch (e) {510 res.status(500).json({ ok: false, error: "unexpected_error" });511 }512}513 514=== pages/api/leaderboard.js ===515let scores = [];516 517export default function handler(req, res) {518 try {519 if (req.method === "GET") {520 const top = [...scores].sort((a, b) => b.score - a.score).slice(0, 20);521 return res.status(200).json({ ok: true, scores: top });522 }523 if (req.method === "POST") {524 const { name, score } = req.body || {};525 if (typeof name !== "string" || typeof score !== "number" || score < 0) {526 return res.status(400).json({ ok: false, error: "invalid_payload" });527 }528 const entry = { name: name.slice(0, 20), score: Math.floor(score), at: Date.now() };529 scores.push(entry);530 scores = scores.slice(-200);531 return res.status(201).json({ ok: true, entry });532 }533 res.setHeader("Allow", "GET, POST");534 return res.status(405).json({ ok: false, error: "method_not_allowed" });535 } catch (e) {536 return res.status(500).json({ ok: false, error: "unexpected_error" });537 }538}539 540=== styles/globals.css ===541@tailwind base;542@tailwind components;543@tailwind utilities;544 545/* App base styles */546html, body, #__next {547 height: 100%;548}549 550:root {551 color-scheme: light;552}553 554/* Accessibility helpers */555.visually-hidden,556.sr-only {557 position: absolute!important;558 width: 1px!important;559 height: 1px!important;560 padding: 0!important;561 margin: -1px!important;562 overflow: hidden!important;563 clip: rect(0, 0, 0, 0)!important;564 white-space: nowrap!important;565 border: 0!important;566}567 568/* Disable image dragging */569img, svg {570 user-select: none;571 -webkit-user-drag: none;572}573 574/* Focus styles */575:focus-visible {576 outline: 3px solid rgba(59,130,246,.6);577 outline-offset: 2px;578}579 580/* Utility: allow bracket syntax custom props without inline style objects for Tailwind rule */581*[style*="--"] {582 /* no-op; just allowed */583}