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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,3 +2,7 @@ dist/
|
|||||||
node_modules/
|
node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.local
|
*.local
|
||||||
|
.mise.local.toml
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|||||||
34
CLAUDE.md
Normal file
34
CLAUDE.md
Normal 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
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"indentStyle": "tab",
|
"indentStyle": "tab",
|
||||||
"lineWidth": 100
|
"lineWidth": 80
|
||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ export function FlipCard({ word, flipped, onFlip }: FlipCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flip-card", flipped && "flipped", instant && "flip-card--instant")}
|
className={cn(
|
||||||
|
"flip-card",
|
||||||
|
flipped && "flipped",
|
||||||
|
instant && "flip-card--instant",
|
||||||
|
)}
|
||||||
onClick={onFlip}
|
onClick={onFlip}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") onFlip();
|
if (e.key === "Enter" || e.key === " ") onFlip();
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ describe("SetupScreen", () => {
|
|||||||
it("renders the setup screen", () => {
|
it("renders the setup screen", () => {
|
||||||
render(<SetupScreen />);
|
render(<SetupScreen />);
|
||||||
expect(screen.getByText("Imposter")).toBeInTheDocument();
|
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", () => {
|
it("adds a player via button click", () => {
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ export function SetupScreen() {
|
|||||||
<header className="flex flex-col gap-3">
|
<header className="flex flex-col gap-3">
|
||||||
<Badge className="self-center">Partyspiel</Badge>
|
<Badge className="self-center">Partyspiel</Badge>
|
||||||
<h1>Imposter</h1>
|
<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>
|
</header>
|
||||||
|
|
||||||
<div className="rounded-3xl bg-surface p-6 shadow-[0_2px_16px_rgba(0,0,0,0.06)] animate-fade-in">
|
<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
|
Alle löschen
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button variant="primary" onClick={startRound} disabled={!canStart} className="flex-1">
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={startRound}
|
||||||
|
disabled={!canStart}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
Spiel starten
|
Spiel starten
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!canStart && players.length > 0 && (
|
{!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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
import type { HTMLAttributes } from "react";
|
import type { HTMLAttributes } from "react";
|
||||||
|
|
||||||
export function Badge({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
export function Badge({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLSpanElement>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
import { type InputHTMLAttributes, forwardRef } from "react";
|
import { type InputHTMLAttributes, forwardRef } from "react";
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
|
export const Input = forwardRef<
|
||||||
({ className, ...props }, ref) => {
|
HTMLInputElement,
|
||||||
return (
|
InputHTMLAttributes<HTMLInputElement>
|
||||||
<input
|
>(({ className, ...props }, ref) => {
|
||||||
ref={ref}
|
return (
|
||||||
className={cn(
|
<input
|
||||||
"w-full rounded-2xl border-2 border-[rgba(26,26,26,0.08)] bg-surface px-5 py-3.5",
|
ref={ref}
|
||||||
"font-body text-base text-[#1a1a1a] placeholder:text-[#5b5b5b]",
|
className={cn(
|
||||||
"outline-none transition-colors focus:border-primary",
|
"w-full rounded-2xl border-2 border-[rgba(26,26,26,0.08)] bg-surface px-5 py-3.5",
|
||||||
className,
|
"font-body text-base text-[#1a1a1a] placeholder:text-[#5b5b5b]",
|
||||||
)}
|
"outline-none transition-colors focus:border-primary",
|
||||||
{...props}
|
className,
|
||||||
/>
|
)}
|
||||||
);
|
{...props}
|
||||||
},
|
/>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Input.displayName = "Input";
|
Input.displayName = "Input";
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ describe("game-store", () => {
|
|||||||
it("adds a player and persists to localStorage", () => {
|
it("adds a player and persists to localStorage", () => {
|
||||||
getState().addPlayer("Alice");
|
getState().addPlayer("Alice");
|
||||||
expect(getState().players).toEqual(["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", () => {
|
it("trims whitespace", () => {
|
||||||
@@ -58,7 +60,9 @@ describe("game-store", () => {
|
|||||||
getState().addPlayer("Bob");
|
getState().addPlayer("Bob");
|
||||||
getState().clearPlayers();
|
getState().clearPlayers();
|
||||||
expect(getState().players).toEqual([]);
|
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", () => {
|
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();
|
getState().loadPlayers();
|
||||||
expect(getState().players).toEqual(["Alice", "Bob"]);
|
expect(getState().players).toEqual(["Alice", "Bob"]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -93,7 +93,9 @@ export const useGameStore = create<GameState & GameActions>((set, get) => ({
|
|||||||
if (raw) {
|
if (raw) {
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (Array.isArray(parsed)) {
|
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 {
|
} catch {
|
||||||
|
|||||||
@@ -20,4 +20,7 @@ const localStorageMock: Storage = {
|
|||||||
key: (index: number) => [...store.keys()][index] ?? null,
|
key: (index: number) => [...store.keys()][index] ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.defineProperty(globalThis, "localStorage", { value: localStorageMock, writable: true });
|
Object.defineProperty(globalThis, "localStorage", {
|
||||||
|
value: localStorageMock,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user