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/
|
||||
.DS_Store
|
||||
*.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": {
|
||||
"indentStyle": "tab",
|
||||
"lineWidth": 100
|
||||
"lineWidth": 80
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user