Migrate Tír na nÓg landing page to Next.js

Summary of changes:
- Created the 'feature/migration-nextjs' branch.
- Initialized a Next.js project with React components and App Router structure.
- Created reusable UI components: Viewport, World, Room, and Door in 'app/components/'.
- Implemented spatial navigation logic (WASD/Arrows and clicks) using React 'useState' and 'useEffect' in 'app/page.js'.
- Migrated the Tír na nÓg pixel-art aesthetic and layout transitions into 'app/globals.css'.
- Configured a Gitea Runner CI/CD workflow in '.gitea/workflows/deploy.yml' for automated builds and linting.
- Added a '.gitignore' file to manage project artifacts.

Co-authored-by: micmug <185775013+micmug@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot]
2026-04-11 23:17:56 +00:00
parent 822cd22dee
commit f6d50d8def
11 changed files with 172 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
export default function Door({ direction, target, label, onNavigate }) {
return (
<button className={`door door-${direction}`} data-target={target} onClick={() => onNavigate(target)}>
{label}
</button>
);
}
+7
View File
@@ -0,0 +1,7 @@
export default function Room({ id, isActive, children }) {
return (
<section id={id} className={`room ${isActive ? 'active' : ''}`}>
{children}
</section>
);
}
+3
View File
@@ -0,0 +1,3 @@
export default function Viewport({ children }) {
return <div id="viewport">{children}</div>;
}
+9
View File
@@ -0,0 +1,9 @@
export default function World({ children, currentPos }) {
const translateX = -currentPos.x * 100;
const translateY = -currentPos.y * 100;
return (
<div id="world" style={{ transform: `translate(${translateX}vw, ${translateY}vh)` }}>
{children}
</div>
);
}
+17
View File
@@ -0,0 +1,17 @@
:root { --gold: #f1c40f; --emerald: #2ecc71; --dark-green: #1b4d3e; --night: #2c3e50; --cloud: #ecf0f1; --pixel-border: 4px; }
* { margin: 0; padding: 0; box-sizing: border-box; image-rendering: pixelated; }
body { font-family: 'Courier New', Courier, monospace; background-color: var(--night); color: var(--cloud); overflow: hidden; line-height: 1.6; }
#viewport { width: 100vw; height: 100vh; overflow: hidden; position: relative; }
#world { display: grid; grid-template-columns: repeat(2, 100vw); grid-template-rows: repeat(2, 100vh); transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); width: 200vw; height: 200vh; }
.room { width: 100vw; height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; position: relative; border: var(--pixel-border) solid var(--gold); background: linear-gradient(135deg, var(--dark-green) 0%, var(--night) 100%); text-align: center; padding: 2rem; }
.content { background: rgba(0, 0, 0, 0.7); padding: 2rem; border: var(--pixel-border) double var(--emerald); box-shadow: 10px 10px 0px var(--dark-green); max-width: 80%; }
h1, h2 { color: var(--gold); text-transform: uppercase; margin-bottom: 1rem; font-size: 3rem; text-shadow: 4px 4px 0px var(--dark-green); }
p { font-size: 1.2rem; margin-bottom: 2rem; }
.doors { position: absolute; width: 100%; height: 100%; pointer-events: none; top: 0; left: 0; }
.door { pointer-events: auto; position: absolute; padding: 1rem; background: var(--gold); color: var(--night); border: var(--pixel-border) solid var(--dark-green); cursor: pointer; font-family: inherit; font-weight: bold; font-size: 1rem; transition: all 0.2s; }
.door:hover { background: var(--emerald); color: white; transform: scale(1.1); }
.door-north { top: 20px; left: 50%; transform: translateX(-50%); }
.door-south { bottom: 20px; left: 50%; transform: translateX(-50%); }
.door-east { right: 20px; top: 50%; transform: translateY(-50%); }
.door-west { left: 20px; top: 50%; transform: translateY(-50%); }
@media (max-width: 768px) { h1, h2 { font-size: 2rem; } .content { max-width: 95%; } }
+12
View File
@@ -0,0 +1,12 @@
import './globals.css';
export const metadata = {
title: 'faebits | Tír na nÓg',
description: 'Welcome to the Land of Eternal Youth.',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
+70
View File
@@ -0,0 +1,70 @@
'use client';
import { useState, useEffect } from 'react';
import Viewport from './components/Viewport';
import World from './components/World';
import Room from './components/Room';
import Door from './components/Door';
export default function Home() {
const [currentPos, setCurrentPos] = useState({ x: 0, y: 0 });
const gridSize = { width: 2, height: 2 };
const navigateTo = (targetStr) => {
const [x, y] = targetStr.split(',').map(Number);
if (!isNaN(x) && !isNaN(y)) { setCurrentPos({ x, y }); }
};
const moveRelative = (dx, dy) => {
setCurrentPos((prev) => {
const newX = prev.x + dx;
const newY = prev.y + dy;
if (newX >= 0 && newX < gridSize.width && newY >= 0 && newY < gridSize.height) {
return { x: newX, y: newY };
}
return prev;
});
};
useEffect(() => {
const handleKeyDown = (e) => {
switch (e.key.toLowerCase()) {
case 'arrowup': case 'w': moveRelative(0, -1); break;
case 'arrowdown': case 's': moveRelative(0, 1); break;
case 'arrowleft': case 'a': moveRelative(-1, 0); break;
case 'arrowright': case 'd': moveRelative(1, 0); break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
return (
<Viewport>
<World currentPos={currentPos}>
<Room id="room-0-0" isActive={currentPos.x === 0 && currentPos.y === 0}>
<div className="content"><h1>Tír na nÓg</h1><p>Welcome to the Land of Eternal Youth.</p></div>
<div className="doors">
<Door direction="east" target="1,0" label="To the Meadow →" onNavigate={navigateTo} />
<Door direction="south" target="0,1" label="Down the Alleys ↓" onNavigate={navigateTo} />
</div>
</Room>
<Room id="room-1-0" isActive={currentPos.x === 1 && currentPos.y === 0}>
<div className="content"><h2>The Meadow</h2><p>Where pixels bloom and legends grow.</p></div>
<div className="doors">
<Door direction="west" target="0,0" label="← Back to the Gate" onNavigate={navigateTo} />
<Door direction="south" target="1,1" label="To the Portal ↓" onNavigate={navigateTo} />
</div>
</Room>
<Room id="room-0-1" isActive={currentPos.x === 0 && currentPos.y === 1}>
<div className="content"><h2>The Alleys</h2><p>Navigating the narrow paths of creation.</p></div>
<div className="doors">
<Door direction="north" target="0,0" label="↑ Up to the Gate" onNavigate={navigateTo} />
<Door direction="east" target="1,1" label="To the Portal →" onNavigate={navigateTo} />
</div>
</Room>
<Room id="room-1-1" isActive={currentPos.x === 1 && currentPos.y === 1}>
<div className="content"><h2>The Portal</h2><p>Step through to join the faebits.</p></div>
<div className="doors">
<Door direction="north" target="1,0" label="↑ Up to the Meadow" onNavigate={navigateTo} />
<Door direction="west" target="0,1" label="← Back to the Alleys" onNavigate={navigateTo} />
</div>
</Room>
</World>
</Viewport>
);
}