normalize biome config (lineWidth 80), rename mise.toml → .mise.toml, add CLAUDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 07:22:31 +01:00
parent e6fb09d4da
commit ef50a77b07
12 changed files with 97 additions and 28 deletions

4
.gitignore vendored
View File

@@ -2,3 +2,7 @@ dist/
node_modules/
.DS_Store
*.local
.mise.local.toml
.env
.env.local
.env.*.local

34
CLAUDE.md Normal file
View File

@@ -0,0 +1,34 @@
# impstr — Imposter Party Game
Static PWA party game (like Werewolf/Mafia). No backend, no accounts — runs entirely in the browser.
## Stack
- **Frontend:** React 19, Vite, Tailwind CSS 4, TanStack Router, Zustand
- **Linting:** Biome (tabs, 80 chars, double quotes)
## Project Structure
```
src/
├── features/ ← game logic, UI components
├── routes/ ← TanStack Router file-based routes
└── shared/ ← shared components, utilities
```
## Local Development
```bash
bun install
bun run dev
```
## Deployment
```bash
./deploy.sh
```
Deploys to Uberspace (`serve.uber.space`):
- Static files → `/var/www/virtual/serve/html/impstr/`
- No backend, no database

View File

@@ -5,7 +5,7 @@
},
"formatter": {
"indentStyle": "tab",
"lineWidth": 100
"lineWidth": 80
},
"linter": {
"enabled": true,

View File

@@ -24,7 +24,11 @@ export function FlipCard({ word, flipped, onFlip }: FlipCardProps) {
return (
<div
className={cn("flip-card", flipped && "flipped", instant && "flip-card--instant")}
className={cn(
"flip-card",
flipped && "flipped",
instant && "flip-card--instant",
)}
onClick={onFlip}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") onFlip();

View File

@@ -24,7 +24,9 @@ describe("SetupScreen", () => {
it("renders the setup screen", () => {
render(<SetupScreen />);
expect(screen.getByText("Imposter")).toBeInTheDocument();
expect(screen.getByPlaceholderText("Spieler hinzufügen…")).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Spieler hinzufügen…"),
).toBeInTheDocument();
});
it("adds a player via button click", () => {

View File

@@ -39,7 +39,9 @@ export function SetupScreen() {
<header className="flex flex-col gap-3">
<Badge className="self-center">Partyspiel</Badge>
<h1>Imposter</h1>
<p className="text-[#5b5b5b]">Findet heraus, wer das geheime Wort nicht kennt!</p>
<p className="text-[#5b5b5b]">
Findet heraus, wer das geheime Wort nicht kennt!
</p>
</header>
<div className="rounded-3xl bg-surface p-6 shadow-[0_2px_16px_rgba(0,0,0,0.06)] animate-fade-in">
@@ -83,14 +85,21 @@ export function SetupScreen() {
Alle löschen
</Button>
)}
<Button variant="primary" onClick={startRound} disabled={!canStart} className="flex-1">
<Button
variant="primary"
onClick={startRound}
disabled={!canStart}
className="flex-1"
>
Spiel starten
</Button>
</div>
</div>
{!canStart && players.length > 0 && (
<p className="text-sm text-[#5b5b5b]">Mindestens {MIN_PLAYERS} Spieler benötigt</p>
<p className="text-sm text-[#5b5b5b]">
Mindestens {MIN_PLAYERS} Spieler benötigt
</p>
)}
</div>
);

View File

@@ -1,7 +1,10 @@
import { cn } from "@/shared/lib/utils";
import type { HTMLAttributes } from "react";
export function Badge({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
export function Badge({
className,
...props
}: HTMLAttributes<HTMLSpanElement>) {
return (
<span
className={cn(

View File

@@ -1,21 +1,22 @@
import { cn } from "@/shared/lib/utils";
import { type InputHTMLAttributes, forwardRef } from "react";
export const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
({ className, ...props }, ref) => {
return (
<input
ref={ref}
className={cn(
"w-full rounded-2xl border-2 border-[rgba(26,26,26,0.08)] bg-surface px-5 py-3.5",
"font-body text-base text-[#1a1a1a] placeholder:text-[#5b5b5b]",
"outline-none transition-colors focus:border-primary",
className,
)}
{...props}
/>
);
},
);
export const Input = forwardRef<
HTMLInputElement,
InputHTMLAttributes<HTMLInputElement>
>(({ className, ...props }, ref) => {
return (
<input
ref={ref}
className={cn(
"w-full rounded-2xl border-2 border-[rgba(26,26,26,0.08)] bg-surface px-5 py-3.5",
"font-body text-base text-[#1a1a1a] placeholder:text-[#5b5b5b]",
"outline-none transition-colors focus:border-primary",
className,
)}
{...props}
/>
);
});
Input.displayName = "Input";

View File

@@ -27,7 +27,9 @@ describe("game-store", () => {
it("adds a player and persists to localStorage", () => {
getState().addPlayer("Alice");
expect(getState().players).toEqual(["Alice"]);
expect(JSON.parse(localStorage.getItem("impstr-players") ?? "[]")).toEqual(["Alice"]);
expect(
JSON.parse(localStorage.getItem("impstr-players") ?? "[]"),
).toEqual(["Alice"]);
});
it("trims whitespace", () => {
@@ -58,7 +60,9 @@ describe("game-store", () => {
getState().addPlayer("Bob");
getState().clearPlayers();
expect(getState().players).toEqual([]);
expect(JSON.parse(localStorage.getItem("impstr-players") ?? "[]")).toEqual([]);
expect(
JSON.parse(localStorage.getItem("impstr-players") ?? "[]"),
).toEqual([]);
});
});
@@ -76,7 +80,10 @@ describe("game-store", () => {
});
it("filters out non-string entries", () => {
localStorage.setItem("impstr-players", JSON.stringify(["Alice", 42, null, "Bob"]));
localStorage.setItem(
"impstr-players",
JSON.stringify(["Alice", 42, null, "Bob"]),
);
getState().loadPlayers();
expect(getState().players).toEqual(["Alice", "Bob"]);
});

View File

@@ -93,7 +93,9 @@ export const useGameStore = create<GameState & GameActions>((set, get) => ({
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
set({ players: parsed.filter((p): p is string => typeof p === "string") });
set({
players: parsed.filter((p): p is string => typeof p === "string"),
});
}
}
} catch {

View File

@@ -20,4 +20,7 @@ const localStorageMock: Storage = {
key: (index: number) => [...store.keys()][index] ?? null,
};
Object.defineProperty(globalThis, "localStorage", { value: localStorageMock, writable: true });
Object.defineProperty(globalThis, "localStorage", {
value: localStorageMock,
writable: true,
});