Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions apps/frontend/src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { createEffect, createMemo } from "solid-js";
import { Router, Route, useNavigate, useSearchParams } from "@solidjs/router";
import { games } from "@/features/games/registry";
import { getHomeGamePath } from "@/features/games/utils";

import Layout from "@/app/layout";
import AboutPage from "@/app/pages/about";
import AdminPage from "@/app/pages/admin";
import HomePage from "@/app/pages/home";
import Layout from "@/app/layout";
import LeaderboardPage from "@/app/pages/leaderboard";
import ProfilePage from "@/app/pages/profile";
import type { GameId } from "@/features/games/types";

import { games } from "@/features/games/core/registry";
import { getHomeGamePath } from "@/features/games/core/utils";
import type { GameId } from "@/features/games/core/types";
import type { WordBankId } from "@/features/content/word-banks/types";

function App() {
Expand Down
8 changes: 4 additions & 4 deletions apps/frontend/src/app/pages/home.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createMemo } from "solid-js";
import { Dynamic } from "solid-js/web";

import GameSelector from "@/features/games/components/game-selector";
import { GameCards } from "@/features/games/core/components/GameCards";
import { FeedbackFeed } from "@/features/feedback/components/feedback-feed";
import { games } from "@/features/games/registry";
import type { GameId } from "@/features/games/types";
import { games } from "@/features/games/core/registry";
import type { GameId } from "@/features/games/core/types";
import type { WordBankId } from "@/features/content/word-banks/types";

type HomeProps = {
Expand All @@ -23,7 +23,7 @@ function HomePage(props: HomeProps) {
<div class="flex flex-1 flex-col gap-12">
{!props.selectedGameId && (
<>
<GameSelector
<GameCards
activeGameId={props.selectedGameId}
onSelectGame={props.onSelectGame}
/>
Expand Down
8 changes: 4 additions & 4 deletions apps/frontend/src/app/pages/leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { For, createSignal } from "solid-js";

import { gameRegistry } from "@/features/games/registry";
import { gameRegistry } from "@/features/games/core/registry";
import type { LeaderboardDifficulty } from "@/features/leaderboard/types";
import { LeaderboardTable } from "@/features/leaderboard/components/leaderboard-table";
import { getGameName } from "@/features/games/utils";
import type { GameId } from "@/features/games/types";
import { getGameName } from "@/features/games/core/utils";
import type { GameId } from "@/features/games/core/types";

const difficulties: LeaderboardDifficulty[] = ["easy", "medium", "hard"];

function LeaderboardPage() {
const [gameId, setGameId] = createSignal<GameId>(
gameRegistry[0]?.id ?? "falling-words",
gameRegistry[0]?.id ?? "survival",
);

const [difficulty, setDifficulty] =
Expand Down
20 changes: 11 additions & 9 deletions apps/frontend/src/features/commandline/registry.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { useNavigate, useLocation, useSearchParams } from "@solidjs/router";
import { themes } from "@/features/content/themes/registry";

import { wordBanks } from "@/features/content/word-banks/registry";
import type { WordBankId } from "@/features/content/word-banks/types";
import { gameRegistry } from "@/features/games/registry";
import { getHomeGamePath } from "@/features/games/utils";
import type { ThemeName } from "@/features/content/themes/types";
import type {
CommandlineItem,
CommandlineScope,
} from "@/features/commandline/types";
import type { GameId } from "@/features/games/types";

import { gameRegistry } from "@/features/games/core/registry";
import { getHomeGamePath } from "@/features/games/core/utils";
import type { GameId } from "@/features/games/core/types";

import { themes } from "@/features/content/themes/registry";
import { themeManager } from "@/features/content/themes/manager";
import type { ThemeName } from "@/features/content/themes/types";

import { useAuthSession } from "@/features/auth/hooks";

import type { CommandlineItem, CommandlineScope } from "./types.ts";

export function createCommandlineRegistry(
setScope: (scope: CommandlineScope) => void,
): Record<CommandlineScope, CommandlineItem[]> {
Expand Down
22 changes: 0 additions & 22 deletions apps/frontend/src/features/games/components/game-input.tsx

This file was deleted.

29 changes: 0 additions & 29 deletions apps/frontend/src/features/games/components/game-selector.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,3 @@ export function DifficultySelector<T extends string>(
</div>
);
}

//TODO: rename to mode selector instead
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { For } from "solid-js";
import { ArrowRight } from "lucide-solid";
import type { GameId } from "@/features/games/types";

import { gameRegistry } from "@/features/games/core/registry";
import type { GameId } from "@/features/games/core/types";

type GameCardProps = {
name: string;
Expand Down Expand Up @@ -30,3 +33,25 @@ export function GameCard(props: GameCardProps) {
</button>
);
}

type GameCardsProps = {
activeGameId?: GameId | null;
onSelectGame: (gameId: GameId) => void;
};

export function GameCards(props: GameCardsProps) {
return (
<div class="grid w-full grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<For each={gameRegistry}>
{(game) => (
<GameCard
id={game.id}
name={game.name}
description={game.description}
onClick={() => props.onSelectGame(game.id)}
/>
)}
</For>
</div>
);
}
42 changes: 42 additions & 0 deletions apps/frontend/src/features/games/core/components/GameInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { onMount } from "solid-js";

type GameInputProps = {
value: string;
onInput: (value: string) => void;
onNext: () => void;
onPrevious: () => void;
onReset: () => void;
};

export function GameInput(props: GameInputProps) {
let ref: HTMLInputElement | undefined;

Check warning on line 12 in apps/frontend/src/features/games/core/components/GameInput.tsx

View workflow job for this annotation

GitHub Actions / Lint, Format and Build Checks

eslint(no-unassigned-vars)

'ref' is always 'undefined' because it's never assigned.

onMount(() => ref?.focus());

return (
<input
ref={ref}
type="text"
class="absolute -left-[9999px] opacity-0"
value={props.value}
onInput={(e) => props.onInput(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Escape") {
props.onReset();
return;
}

if (e.key === " ") {
e.preventDefault();
props.onNext();
return;
}

if (e.key === "Backspace" && props.value.length === 0) {
e.preventDefault();
props.onPrevious();
}
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export function GameOver(props: GameOverProps) {
</div>
);
}

// TODO: make this game overlay ie for all phases ig
56 changes: 56 additions & 0 deletions apps/frontend/src/features/games/core/engine/Word.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Index, Show } from "solid-js";

import { analyzeWord } from "./analyze-word";
import type { CharacterState, WordState } from "../types";

type WordProps = {
word: WordState;
isActive?: boolean;
};

export function Word(props: WordProps) {
const chars = () => analyzeWord(props.word.expected, props.word.typed);

return (
<div class="inline-flex relative">
<Index each={chars()}>
{(char, i) => (
<span class="relative">
<Show when={props.isActive && i === props.word.typed.length}>
<span class="absolute bottom-[-2px] left-0 h-[2px] w-full bg-(--caret) animate-pulse" />
</Show>

<Character char={char().value} state={char().state} />
</span>
)}
</Index>

<span class="relative inline-block w-[0.5em]">
<Show
when={
props.isActive &&
props.word.typed.length >= props.word.expected.length
}
>
<span class="absolute bottom-[-2px] left-0 h-[2px] w-full bg-(--caret) animate-pulse" />
</Show>
</span>
</div>
);
}

type CharacterProps = {
char: string;
state: CharacterState;
};

const classes: Record<CharacterState, string> = {
correct: "text-(--text)",
incorrect: "text-(--error)",
pending: "text-(--sub)",
extra: "text-(--error)",
};

function Character(props: CharacterProps) {
return <span class={classes[props.state]}>{props.char}</span>;
}
38 changes: 38 additions & 0 deletions apps/frontend/src/features/games/core/engine/analyze-word.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { AnalyzedCharacter, CharacterState } from "../types";

export function analyzeWord(
expected: string,
typed: string,
): AnalyzedCharacter[] {
const result: AnalyzedCharacter[] = [];

const max = Math.max(expected.length, typed.length);

for (let i = 0; i < max; i++) {
const expectedChar = expected[i];
const typedChar = typed[i];

// extra chars
if (i >= expected.length) {
result.push({
value: typedChar!,
state: "extra",
});

continue;
}

let state: CharacterState = "pending";

if (typedChar != null) {
state = typedChar === expectedChar ? "correct" : "incorrect";
}

result.push({
value: expectedChar!,
state,
});
}

return result;
}
Loading
Loading