Skip to content

Commit 2f16cbc

Browse files
committed
Login page. Read-only for Spectators
1 parent 1b04fa4 commit 2f16cbc

10 files changed

Lines changed: 354 additions & 88 deletions

File tree

artifacts/game-client/src/App.tsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2-
import { Route, Router, Switch, Redirect } from "wouter";
2+
import { Route, Router, Switch } from "wouter";
33
import { Toaster } from "@/components/ui/toaster";
44
import { TooltipProvider } from "@/components/ui/tooltip";
5+
import { LoginPage } from "@/pages/LoginPage";
56
import { ViewerPage } from "@/pages/ViewerPage";
67
import { GamePage } from "@/pages/GamePage";
78
import { SpectatorPage } from "@/pages/SpectatorPage";
@@ -15,6 +16,7 @@ import Adepts2NotFound from "@/apps/adepts-game-2/pages/not-found";
1516
import Adepts3Home from "@/apps/adepts-game-3/pages/Home";
1617
import Adepts3NotFound from "@/apps/adepts-game-3/pages/not-found";
1718
import { GamePhaseArrows } from "@/components/GamePhaseArrows";
19+
import { RequireLogin } from "@/components/RequireLogin";
1820

1921
const queryClient = new QueryClient();
2022

@@ -33,24 +35,30 @@ function App() {
3335
<Route path="/spectate" component={SpectatorPage} />
3436
<Route path="/game" component={GamePage} />
3537
<Route path="/adepts-game" nest>
36-
<Switch>
37-
<Route path="/" component={Adepts1Home} />
38-
<Route component={Adepts1NotFound} />
39-
</Switch>
38+
<RequireLogin>
39+
<Switch>
40+
<Route path="/" component={Adepts1Home} />
41+
<Route component={Adepts1NotFound} />
42+
</Switch>
43+
</RequireLogin>
4044
</Route>
4145
<Route path="/adepts-game-2" nest>
42-
<Switch>
43-
<Route path="/" component={Adepts2Home} />
44-
<Route component={Adepts2NotFound} />
45-
</Switch>
46+
<RequireLogin>
47+
<Switch>
48+
<Route path="/" component={Adepts2Home} />
49+
<Route component={Adepts2NotFound} />
50+
</Switch>
51+
</RequireLogin>
4652
</Route>
4753
<Route path="/adepts-game-3" nest>
48-
<Switch>
49-
<Route path="/" component={Adepts3Home} />
50-
<Route component={Adepts3NotFound} />
51-
</Switch>
54+
<RequireLogin>
55+
<Switch>
56+
<Route path="/" component={Adepts3Home} />
57+
<Route component={Adepts3NotFound} />
58+
</Switch>
59+
</RequireLogin>
5260
</Route>
53-
<Route path="/"><Redirect to="/adepts-game/" /></Route>
61+
<Route path="/" component={LoginPage} />
5462
</Switch>
5563
<GamePhaseArrows />
5664
</>

artifacts/game-client/src/apps/adepts-game-2/pages/Home.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Scoreboard } from "@/lib/adepts-scoreboard";
44
import { QuizBoard } from "@/lib/adepts-quiz-board";
55
import { QuestionModal } from "@/lib/adepts-question-modal";
66
import { GamePhaseNav } from "@/components/GamePhaseArrows";
7+
import { useRole } from "@/hooks/useRole";
78

89
function resolveUrl(url: string): string {
910
if (!url) return url;
@@ -12,6 +13,7 @@ function resolveUrl(url: string): string {
1213
}
1314

1415
export default function Home() {
16+
const { isSpectator } = useRole();
1517
const {
1618
state,
1719
updatePlayerName,
@@ -67,7 +69,7 @@ export default function Home() {
6769
Adepts-game 2
6870
</span>
6971
<div className="ml-auto flex items-center gap-3">
70-
<GamePhaseNav />
72+
{!isSpectator && <GamePhaseNav />}
7173
<div style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "monospace", fontSize: 11, color: "#2ecc71" }}>
7274
<div style={{ width: 8, height: 8, borderRadius: "50%", background: "#2ecc71", boxShadow: "0 0 8px #2ecc71" }} />
7375
Онлайн
@@ -82,6 +84,7 @@ export default function Home() {
8284
questions={state.questions}
8385
onUpdateTheme={updateThemeName}
8486
onQuestionClick={handleQuestionClick}
87+
readonly={isSpectator}
8588
/>
8689
</main>
8790

@@ -91,6 +94,7 @@ export default function Home() {
9194
onUpdateName={updatePlayerName}
9295
onUpdateScore={updatePlayerScore}
9396
onResetScores={resetScores}
97+
readonly={isSpectator}
9498
/>
9599
</div>
96100

artifacts/game-client/src/apps/adepts-game-3/pages/Home.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Scoreboard } from "@/lib/adepts-scoreboard";
44
import { QuizBoard } from "@/lib/adepts-quiz-board";
55
import { QuestionModal } from "@/lib/adepts-question-modal";
66
import { GamePhaseNav } from "@/components/GamePhaseArrows";
7+
import { useRole } from "@/hooks/useRole";
78

89
function resolveUrl(url: string): string {
910
if (!url) return url;
@@ -12,6 +13,7 @@ function resolveUrl(url: string): string {
1213
}
1314

1415
export default function Home() {
16+
const { isSpectator } = useRole();
1517
const {
1618
state,
1719
updatePlayerName,
@@ -67,7 +69,7 @@ export default function Home() {
6769
Adepts-game 3
6870
</span>
6971
<div className="ml-auto flex items-center gap-3">
70-
<GamePhaseNav />
72+
{!isSpectator && <GamePhaseNav />}
7173
<div style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "monospace", fontSize: 11, color: "#2ecc71" }}>
7274
<div style={{ width: 8, height: 8, borderRadius: "50%", background: "#2ecc71", boxShadow: "0 0 8px #2ecc71" }} />
7375
Онлайн
@@ -82,6 +84,7 @@ export default function Home() {
8284
questions={state.questions}
8385
onUpdateTheme={updateThemeName}
8486
onQuestionClick={handleQuestionClick}
87+
readonly={isSpectator}
8588
/>
8689
</main>
8790

@@ -91,6 +94,7 @@ export default function Home() {
9194
onUpdateName={updatePlayerName}
9295
onUpdateScore={updatePlayerScore}
9396
onResetScores={resetScores}
97+
readonly={isSpectator}
9498
/>
9599
</div>
96100

artifacts/game-client/src/apps/adepts-game/pages/Home.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Scoreboard } from "@/lib/adepts-scoreboard";
44
import { QuizBoard } from "@/lib/adepts-quiz-board";
55
import { QuestionModal } from "@/lib/adepts-question-modal";
66
import { GamePhaseNav } from "@/components/GamePhaseArrows";
7+
import { useRole } from "@/hooks/useRole";
78

89
function resolveUrl(url: string): string {
910
if (!url) return url;
@@ -12,6 +13,7 @@ function resolveUrl(url: string): string {
1213
}
1314

1415
export default function Home() {
16+
const { isSpectator } = useRole();
1517
const {
1618
state,
1719
updatePlayerName,
@@ -67,7 +69,7 @@ export default function Home() {
6769
Adepts-game
6870
</span>
6971
<div className="ml-auto flex items-center gap-3">
70-
<GamePhaseNav />
72+
{!isSpectator && <GamePhaseNav />}
7173
<div style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "monospace", fontSize: 11, color: "#2ecc71" }}>
7274
<div style={{ width: 8, height: 8, borderRadius: "50%", background: "#2ecc71", boxShadow: "0 0 8px #2ecc71" }} />
7375
Онлайн
@@ -82,6 +84,7 @@ export default function Home() {
8284
questions={state.questions}
8385
onUpdateTheme={updateThemeName}
8486
onQuestionClick={handleQuestionClick}
87+
readonly={isSpectator}
8588
/>
8689
</main>
8790

@@ -91,6 +94,7 @@ export default function Home() {
9194
onUpdateName={updatePlayerName}
9295
onUpdateScore={updatePlayerScore}
9396
onResetScores={resetScores}
97+
readonly={isSpectator}
9498
/>
9599
</div>
96100

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useEffect } from "react";
2+
3+
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
4+
5+
/** Redirects to the login page if the user has not entered their nick. */
6+
export function RequireLogin({ children }: { children: React.ReactNode }) {
7+
const isLoggedIn = Boolean(localStorage.getItem("player_nick"));
8+
9+
useEffect(() => {
10+
if (!isLoggedIn) {
11+
window.location.replace(`${base}/`);
12+
}
13+
}, [isLoggedIn]);
14+
15+
if (!isLoggedIn) return null;
16+
17+
return <>{children}</>;
18+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/** Returns true when the current user arrived via the login page (spectator role). */
2+
export function useRole() {
3+
const isSpectator = localStorage.getItem("player_role") === "spectator";
4+
return { isSpectator };
5+
}

artifacts/game-client/src/lib/adepts-quiz-board/QuizBoard.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface QuizBoardProps {
1414
questions: Question[][];
1515
onUpdateTheme: (index: number, name: string) => void;
1616
onQuestionClick: (themeIndex: number, questionIndex: number) => void;
17+
readonly?: boolean;
1718
}
1819

1920
export function QuizBoard({
@@ -22,6 +23,7 @@ export function QuizBoard({
2223
questions,
2324
onUpdateTheme,
2425
onQuestionClick,
26+
readonly = false,
2527
}: QuizBoardProps) {
2628
const [editingTheme, setEditingTheme] = useState<number | null>(null);
2729
const theme2LineBreaks = board === 2;
@@ -43,7 +45,7 @@ export function QuizBoard({
4345
damping: 22,
4446
stiffness: 180,
4547
}}
46-
className="w-[21%] relative flex items-center rounded-xl overflow-hidden cursor-text group"
48+
className={`w-[21%] relative flex items-center rounded-xl overflow-hidden group ${readonly ? "cursor-default" : "cursor-text"}`}
4749
style={{
4850
background: "linear-gradient(105deg, hsla(270,40%,12%,0.95) 0%, hsla(270,30%,9%,0.7) 100%)",
4951
borderLeft: "3px solid hsla(280,65%,58%,0.85)",
@@ -52,7 +54,7 @@ export function QuizBoard({
5254
borderLeftColor: "hsla(280,65%,58%,0.85)",
5355
boxShadow: "inset 0 0 30px hsla(280,60%,15%,0.4)",
5456
}}
55-
onClick={() => setEditingTheme(tIdx)}
57+
onClick={() => !readonly && setEditingTheme(tIdx)}
5658
>
5759
{/* Hover shimmer */}
5860
<motion.div
@@ -140,23 +142,25 @@ export function QuizBoard({
140142
stiffness: 200,
141143
},
142144
}}
143-
whileHover={!q.used ? {
145+
whileHover={!q.used && !readonly ? {
144146
scale: 1.06,
145147
y: -3,
146148
transition: { type: "tween", duration: 0.08, ease: "easeOut" },
147149
} : {}}
148-
whileTap={!q.used ? {
150+
whileTap={!q.used && !readonly ? {
149151
scale: 0.94,
150152
transition: { type: "tween", duration: 0.06 },
151153
} : {}}
152154
transition={{ type: "tween", duration: 0.1, ease: "easeOut" }}
153-
onClick={() => onQuestionClick(tIdx, qIdx)}
155+
onClick={() => !readonly && onQuestionClick(tIdx, qIdx)}
154156
className={`
155157
relative w-full h-full rounded-xl border flex items-center justify-center
156158
font-display font-bold transition-colors duration-150
157159
${q.used
158160
? "bg-background/20 border-border/50 text-muted-foreground/30 cursor-not-allowed"
159-
: "bg-secondary/40 border-accent/30 text-primary hover:bg-secondary hover:border-accent hover:shadow-[0_0_22px_hsla(280,65%,50%,0.45)] cursor-pointer"
161+
: readonly
162+
? "bg-secondary/40 border-accent/30 text-primary cursor-default"
163+
: "bg-secondary/40 border-accent/30 text-primary hover:bg-secondary hover:border-accent hover:shadow-[0_0_22px_hsla(280,65%,50%,0.45)] cursor-pointer"
160164
}
161165
`}
162166
>

0 commit comments

Comments
 (0)