Initial addition of the Arcade study feature. Add connections as the first working game.
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m4s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m4s
This commit is contained in:
parent
7b90409f2e
commit
611a585757
43 changed files with 5990 additions and 68 deletions
|
|
@ -0,0 +1,18 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { ARCADE_RENDERERS } from "@/components/arcade/rendererRegistry";
|
||||
import { getArcadePack } from "@/services/arcadeService";
|
||||
|
||||
export default async function ConnectionsPlayPage(
|
||||
props: PageProps<"/[classSlug]/arcade/connections/[packId]/play">
|
||||
) {
|
||||
const [{ classSlug, packId }, query] = await Promise.all([props.params, props.searchParams]);
|
||||
const pack = await getArcadePack(packId);
|
||||
if (!pack || pack.class.slug !== classSlug || pack.gameType !== "connections") notFound();
|
||||
const mistakesValue = Number(query.mistakes);
|
||||
const allowedMistakes = Number.isInteger(mistakesValue) && mistakesValue >= 1 && mistakesValue <= 8
|
||||
? mistakesValue
|
||||
: pack.normalized.settings.allowedMistakes;
|
||||
const Renderer = ARCADE_RENDERERS.connections;
|
||||
return <Renderer seed={randomUUID()} packId={pack.id} packName={pack.name} classSlug={classSlug} pack={pack.normalized} settings={{ allowedMistakes, oneAwayFeedback: query.oneAway !== "0" }} />;
|
||||
}
|
||||
12
src/app/(protected)/[classSlug]/arcade/connections/page.tsx
Normal file
12
src/app/(protected)/[classSlug]/arcade/connections/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { ConnectionsHub } from "@/components/arcade/ConnectionsHub";
|
||||
import { getClassBySlug } from "@/services/classService";
|
||||
import { listArcadePacks } from "@/services/arcadeService";
|
||||
|
||||
export default async function ConnectionsHubPage(props: PageProps<"/[classSlug]/arcade/connections">) {
|
||||
const { classSlug } = await props.params;
|
||||
const classItem = await getClassBySlug(classSlug);
|
||||
if (!classItem) notFound();
|
||||
const packs = await listArcadePacks(classItem.id, "connections");
|
||||
return <ConnectionsHub classId={classItem.id} classSlug={classSlug} initialPacks={packs} />;
|
||||
}
|
||||
3
src/app/(protected)/[classSlug]/arcade/layout.tsx
Normal file
3
src/app/(protected)/[classSlug]/arcade/layout.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default function ArcadeLayout({ children }: { children: React.ReactNode }) {
|
||||
return <div className="arcade-route">{children}</div>;
|
||||
}
|
||||
46
src/app/(protected)/[classSlug]/arcade/page.tsx
Normal file
46
src/app/(protected)/[classSlug]/arcade/page.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import Link from "next/link";
|
||||
import { ARCADE_GAMES } from "@/config/arcadeGames";
|
||||
|
||||
function ConnectionsVisual() {
|
||||
return <div className="arcade-visual connections-visual" aria-hidden><div className="connections-visual-grid">{Array.from({ length: 16 }, (_, index) => <i key={index} />)}</div><span className="connections-visual-label">4 hidden groups</span></div>;
|
||||
}
|
||||
|
||||
function CrosswordVisual() {
|
||||
const cells = ["C", "", "", "", "R", "", "O", "", "S", "T", "U", "D", "Y", "", "S", "", "", "", "W", "", "", "", "O", "", "", "", "R", "", "", "", "D", ""];
|
||||
return <div className="arcade-visual crossword-visual" aria-hidden><div className="crossword-grid">{cells.map((letter, index) => <i key={index} className={letter ? "has-letter" : ""}>{letter}</i>)}</div><span className="crossword-pencil">✎</span></div>;
|
||||
}
|
||||
|
||||
function FallingBlocksVisual() {
|
||||
return <div className="arcade-visual blocks-visual" aria-hidden><span className="blocks-arrow">↓</span><div className="blocks-piece blocks-piece-a"><i /><i /><i /><i /></div><div className="blocks-piece blocks-piece-b"><i /><i /><i /></div><div className="blocks-floor">{Array.from({ length: 9 }, (_, index) => <i key={index} />)}</div></div>;
|
||||
}
|
||||
|
||||
function AsteroidVisual() {
|
||||
return <div className="arcade-visual asteroid-visual" aria-hidden><i className="arcade-star star-a" /><i className="arcade-star star-b" /><i className="arcade-star star-c" /><span className="asteroid asteroid-a" /><span className="asteroid asteroid-b" /><span className="asteroid asteroid-c" /><span className="space-station"><i /></span><span className="laser-beam" /><span className="shield-ring" /></div>;
|
||||
}
|
||||
|
||||
function GameVisual({ gameKey }: { gameKey: (typeof ARCADE_GAMES)[number]["key"] }) {
|
||||
if (gameKey === "connections") return <ConnectionsVisual />;
|
||||
if (gameKey === "crossword") return <CrosswordVisual />;
|
||||
if (gameKey === "falling-blocks") return <FallingBlocksVisual />;
|
||||
return <AsteroidVisual />;
|
||||
}
|
||||
|
||||
export default async function ArcadePage(props: PageProps<"/[classSlug]/arcade">) {
|
||||
const { classSlug } = await props.params;
|
||||
return (
|
||||
<div className="arcade-lobby pb-20">
|
||||
<div className="arcade-lobby-heading mb-8">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-primary">Insert curiosity</p>
|
||||
<h2 className="mt-2 text-4xl font-black tracking-[-0.05em] text-text-heading sm:text-5xl">Choose your cabinet</h2>
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-text-secondary">Step away from the desk and turn your study material into a quick challenge.</p>
|
||||
</div>
|
||||
|
||||
<div className="arcade-cabinet-grid grid gap-5 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{ARCADE_GAMES.map((game) => {
|
||||
const card = <article className={`arcade-cabinet arcade-cabinet-${game.key} ${game.available ? "is-playable" : "is-coming"}`}><GameVisual gameKey={game.key} /><div className="arcade-cabinet-copy"><p className="arcade-cabinet-status">{game.estimatedMinutes}</p><h3>{game.name}</h3><p>{game.description}</p><span className="arcade-cabinet-action">{game.available ? "Play Connections →" : "Coming soon"}</span></div></article>;
|
||||
return game.available ? <Link key={game.key} href={`/${classSlug}/arcade/${game.path}`} className="rounded-[1.75rem] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary">{card}</Link> : <div key={game.key}>{card}</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue