aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-02-20 17:53:55 +0000
committerClaude <noreply@anthropic.com>2026-02-20 17:53:55 +0000
commit4af93cae1ca54ad7c9bc7eb4b56c010f55c4c72d (patch)
tree804f0aee031469aad98a15cf8307cc9f0794c5bc /frontend
parent00354d392a0bcddaac71fee7b6aae721e5747f59 (diff)
downloadphperkaigi-2026-albatross-4af93cae1ca54ad7c9bc7eb4b56c010f55c4c72d.tar.gz
phperkaigi-2026-albatross-4af93cae1ca54ad7c9bc7eb4b56c010f55c4c72d.tar.zst
phperkaigi-2026-albatross-4af93cae1ca54ad7c9bc7eb4b56c010f55c4c72d.zip
feat: allow viewing/spectating games without login
Make watch, ranking, game list, and tournament endpoints accessible without authentication. Unauthenticated users can browse games and spectate from the index page, while play/submit/preview still require login. https://claude.ai/code/session_019j9tNcnLsLz15e1qtbmeqe
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/App.tsx14
-rw-r--r--frontend/app/pages/DashboardPage.tsx54
-rw-r--r--frontend/app/pages/GolfWatchPage.tsx18
-rw-r--r--frontend/app/pages/IndexPage.tsx3
4 files changed, 51 insertions, 38 deletions
diff --git a/frontend/app/App.tsx b/frontend/app/App.tsx
index 3f1333a..be608e4 100644
--- a/frontend/app/App.tsx
+++ b/frontend/app/App.tsx
@@ -26,9 +26,7 @@ export default function App() {
</PublicOnlyRoute>
</Route>
<Route path="/dashboard">
- <ProtectedRoute>
- <DashboardPage />
- </ProtectedRoute>
+ <DashboardPage />
</Route>
<Route path="/golf/:gameId/preview">
{(params) => (
@@ -52,17 +50,11 @@ export default function App() {
)}
</Route>
<Route path="/golf/:gameId/watch">
- {(params) => (
- <ProtectedRoute>
- <GolfWatchPage gameId={params.gameId} />
- </ProtectedRoute>
- )}
+ {(params) => <GolfWatchPage gameId={params.gameId} />}
</Route>
<Route path="/tournament/:tournamentId">
{(params) => (
- <ProtectedRoute>
- <TournamentPage tournamentId={params.tournamentId} />
- </ProtectedRoute>
+ <TournamentPage tournamentId={params.tournamentId} />
)}
</Route>
<Route>
diff --git a/frontend/app/pages/DashboardPage.tsx b/frontend/app/pages/DashboardPage.tsx
index 00db3f0..92c0f04 100644
--- a/frontend/app/pages/DashboardPage.tsx
+++ b/frontend/app/pages/DashboardPage.tsx
@@ -14,7 +14,7 @@ type Game = components["schemas"]["Game"];
export default function DashboardPage() {
usePageTitle(`Dashboard | ${APP_NAME}`);
- const { user, logout } = useAuth();
+ const { user, isLoggedIn, isLoading: authLoading, logout } = useAuth();
const [, navigate] = useLocation();
const [games, setGames] = useState<Game[]>([]);
@@ -33,7 +33,7 @@ export default function DashboardPage() {
navigate("/");
}
- if (loading) {
+ if (loading || authLoading) {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<p className="text-gray-500">Loading...</p>
@@ -43,18 +43,24 @@ export default function DashboardPage() {
return (
<div className="p-6 bg-gray-100 min-h-screen flex flex-col items-center gap-4">
- {user?.icon_path && (
+ {isLoggedIn && user?.icon_path && (
<UserIcon
iconPath={user.icon_path}
displayName={user.display_name}
className="w-24 h-24"
/>
)}
- <h1 className="text-3xl font-bold text-gray-800">{user?.display_name}</h1>
+ {isLoggedIn ? (
+ <h1 className="text-3xl font-bold text-gray-800">
+ {user?.display_name}
+ </h1>
+ ) : (
+ <h1 className="text-3xl font-bold text-gray-800">試合一覧</h1>
+ )}
<BorderedContainerWithCaption caption="試合一覧">
<div className="px-4">
{games.length === 0 ? (
- <p>エントリーできる試合はありません</p>
+ <p>試合はありません</p>
) : (
<ul className="divide-y divide-gray-300">
{games.map((game) => (
@@ -68,20 +74,24 @@ export default function DashboardPage() {
</span>
</div>
<div className="flex gap-2">
- {game.started_at == null && (
+ {isLoggedIn && game.started_at == null && (
<NavigateLink to={`/golf/${game.game_id}/preview`}>
問題を見る
</NavigateLink>
)}
- <NavigateLink to={`/golf/${game.game_id}/play`}>
- 対戦
- </NavigateLink>
+ {isLoggedIn && (
+ <NavigateLink to={`/golf/${game.game_id}/play`}>
+ 対戦
+ </NavigateLink>
+ )}
<NavigateLink to={`/golf/${game.game_id}/watch`}>
観戦
</NavigateLink>
- <NavigateLink to={`/golf/${game.game_id}/submissions`}>
- 提出履歴
- </NavigateLink>
+ {isLoggedIn && (
+ <NavigateLink to={`/golf/${game.game_id}/submissions`}>
+ 提出履歴
+ </NavigateLink>
+ )}
</div>
</li>
))}
@@ -89,14 +99,18 @@ export default function DashboardPage() {
)}
</div>
</BorderedContainerWithCaption>
- <button
- type="button"
- onClick={handleLogout}
- className="px-4 py-2 bg-red-500 text-white rounded-sm transition duration-300 hover:bg-red-700 focus:ring-3 focus:ring-red-400 focus:outline-hidden"
- >
- ログアウト
- </button>
- {user?.is_admin && (
+ {isLoggedIn ? (
+ <button
+ type="button"
+ onClick={handleLogout}
+ className="px-4 py-2 bg-red-500 text-white rounded-sm transition duration-300 hover:bg-red-700 focus:ring-3 focus:ring-red-400 focus:outline-hidden"
+ >
+ ログアウト
+ </button>
+ ) : (
+ <NavigateLink to="/login">ログイン</NavigateLink>
+ )}
+ {isLoggedIn && user?.is_admin && (
<a
href={
import.meta.env.DEV
diff --git a/frontend/app/pages/GolfWatchPage.tsx b/frontend/app/pages/GolfWatchPage.tsx
index 4f76136..168bd6f 100644
--- a/frontend/app/pages/GolfWatchPage.tsx
+++ b/frontend/app/pages/GolfWatchPage.tsx
@@ -1,6 +1,5 @@
import { createStore, Provider as JotaiProvider } from "jotai";
import { useEffect, useMemo, useState } from "react";
-import { useLocation } from "wouter";
import { ApiClientContext, createApiClient } from "../api/client";
import type { components } from "../api/schema";
import GolfWatchApp from "../components/GolfWatchApp";
@@ -12,14 +11,13 @@ type LatestGameState = components["schemas"]["LatestGameState"];
type RankingEntry = components["schemas"]["RankingEntry"];
export default function GolfWatchPage({ gameId }: { gameId: string }) {
- const [, navigate] = useLocation();
-
const [game, setGame] = useState<Game | null>(null);
const [ranking, setRanking] = useState<RankingEntry[]>([]);
const [gameStates, setGameStates] = useState<{
[key: string]: LatestGameState;
}>({});
const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
const gameIdNum = Number(gameId);
@@ -41,16 +39,16 @@ export default function GolfWatchPage({ gameId }: { gameId: string }) {
setRanking(ranking);
setGameStates(states);
})
- .catch(() => navigate("/dashboard"))
+ .catch(() => setError(true))
.finally(() => setLoading(false));
- }, [gameIdNum, navigate]);
+ }, [gameIdNum]);
const store = useMemo(() => {
if (!game) return null;
return createStore();
}, [game]);
- if (loading || !game || !store) {
+ if (loading) {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<p className="text-gray-500">Loading...</p>
@@ -58,6 +56,14 @@ export default function GolfWatchPage({ gameId }: { gameId: string }) {
);
}
+ if (error || !game || !store) {
+ return (
+ <div className="min-h-screen bg-gray-100 flex items-center justify-center">
+ <p className="text-red-500">試合が見つかりませんでした</p>
+ </div>
+ );
+ }
+
return (
<JotaiProvider store={store}>
<ApiClientContext.Provider value={createApiClient()}>
diff --git a/frontend/app/pages/IndexPage.tsx b/frontend/app/pages/IndexPage.tsx
index 120720a..03e135b 100644
--- a/frontend/app/pages/IndexPage.tsx
+++ b/frontend/app/pages/IndexPage.tsx
@@ -31,7 +31,8 @@ export default function IndexPage() {
</p>
</BorderedContainer>
</div>
- <div>
+ <div className="flex gap-4">
+ <NavigateLink to="/dashboard">観戦する</NavigateLink>
<NavigateLink to="/login">ログイン</NavigateLink>
</div>
</div>