diff options
Diffstat (limited to 'frontend/app/pages')
| -rw-r--r-- | frontend/app/pages/DashboardPage.tsx | 108 | ||||
| -rw-r--r-- | frontend/app/pages/GolfPlayPage.tsx | 74 | ||||
| -rw-r--r-- | frontend/app/pages/GolfWatchPage.tsx | 78 | ||||
| -rw-r--r-- | frontend/app/pages/IndexPage.tsx | 39 | ||||
| -rw-r--r-- | frontend/app/pages/LoginPage.tsx | 101 | ||||
| -rw-r--r-- | frontend/app/pages/TournamentPage.tsx | 429 |
6 files changed, 829 insertions, 0 deletions
diff --git a/frontend/app/pages/DashboardPage.tsx b/frontend/app/pages/DashboardPage.tsx new file mode 100644 index 0000000..c81014d --- /dev/null +++ b/frontend/app/pages/DashboardPage.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from "react"; +import { useLocation } from "wouter"; +import { createApiClient } from "../api/client"; +import type { components } from "../api/schema"; +import { getToken } from "../auth"; +import BorderedContainerWithCaption from "../components/BorderedContainerWithCaption"; +import NavigateLink from "../components/NavigateLink"; +import UserIcon from "../components/UserIcon"; +import { APP_NAME, BASE_PATH } from "../config"; +import { useAuth } from "../hooks/useAuth"; +import { usePageTitle } from "../hooks/usePageTitle"; + +type Game = components["schemas"]["Game"]; + +export default function DashboardPage() { + usePageTitle(`Dashboard | ${APP_NAME}`); + + const { user, logout } = useAuth(); + const [, navigate] = useLocation(); + + const [games, setGames] = useState<Game[]>([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const token = getToken(); + if (!token) return; + const apiClient = createApiClient(token); + apiClient + .getGames() + .then(({ games }) => setGames(games)) + .finally(() => setLoading(false)); + }, []); + + function handleLogout() { + logout(); + navigate("/"); + } + + if (loading) { + return ( + <div className="min-h-screen bg-gray-100 flex items-center justify-center"> + <p className="text-gray-500">Loading...</p> + </div> + ); + } + + return ( + <div className="p-6 bg-gray-100 min-h-screen flex flex-col items-center gap-4"> + {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> + <BorderedContainerWithCaption caption="試合一覧"> + <div className="px-4"> + {games.length === 0 ? ( + <p>エントリーできる試合はありません</p> + ) : ( + <ul className="divide-y divide-gray-300"> + {games.map((game) => ( + <li + key={game.game_id} + className="flex justify-between items-center py-2 gap-4" + > + <div> + <span className="font-medium text-gray-800"> + {game.display_name} + </span> + </div> + <div className="flex gap-2"> + <NavigateLink to={`/golf/${game.game_id}/play`}> + 対戦 + </NavigateLink> + <NavigateLink to={`/golf/${game.game_id}/watch`}> + 観戦 + </NavigateLink> + </div> + </li> + ))} + </ul> + )} + </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 && ( + <a + href={ + import.meta.env.DEV + ? `http://localhost:8004${BASE_PATH}admin/dashboard` + : `${BASE_PATH}admin/dashboard` + } + className="text-lg text-white bg-sky-600 px-4 py-2 rounded-sm transition duration-300 hover:bg-sky-500 focus:ring-3 focus:ring-sky-400 focus:outline-hidden" + > + Admin Dashboard + </a> + )} + </div> + ); +} diff --git a/frontend/app/pages/GolfPlayPage.tsx b/frontend/app/pages/GolfPlayPage.tsx new file mode 100644 index 0000000..3fddbf8 --- /dev/null +++ b/frontend/app/pages/GolfPlayPage.tsx @@ -0,0 +1,74 @@ +import { Provider as JotaiProvider, createStore } 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 { getToken } from "../auth"; +import GolfPlayApp from "../components/GolfPlayApp"; +import { APP_NAME } from "../config"; +import { useAuth } from "../hooks/useAuth"; +import { usePageTitle } from "../hooks/usePageTitle"; + +type Game = components["schemas"]["Game"]; +type LatestGameState = components["schemas"]["LatestGameState"]; + +export default function GolfPlayPage({ gameId }: { gameId: string }) { + const { user } = useAuth(); + const [, navigate] = useLocation(); + + const [game, setGame] = useState<Game | null>(null); + const [gameState, setGameState] = useState<LatestGameState | null>(null); + const [loading, setLoading] = useState(true); + + const gameIdNum = Number(gameId); + + usePageTitle( + game + ? `Golf Playing ${game.display_name} | ${APP_NAME}` + : `Golf Playing | ${APP_NAME}`, + ); + + useEffect(() => { + const token = getToken(); + if (!token) return; + const apiClient = createApiClient(token); + Promise.all([ + apiClient.getGame(gameIdNum), + apiClient.getGamePlayLatestState(gameIdNum), + ]) + .then(([{ game }, { state }]) => { + setGame(game); + setGameState(state); + }) + .catch(() => navigate("/dashboard")) + .finally(() => setLoading(false)); + }, [gameIdNum, navigate]); + + const store = useMemo(() => { + if (!game || !user) return null; + return createStore(); + }, [game, user]); + + if (loading || !game || !gameState || !user || !store) { + return ( + <div className="min-h-screen bg-gray-100 flex items-center justify-center"> + <p className="text-gray-500">Loading...</p> + </div> + ); + } + + const token = getToken()!; + + return ( + <JotaiProvider store={store}> + <ApiClientContext.Provider value={createApiClient(token)}> + <GolfPlayApp + key={game.game_id} + game={game} + player={user} + initialGameState={gameState} + /> + </ApiClientContext.Provider> + </JotaiProvider> + ); +} diff --git a/frontend/app/pages/GolfWatchPage.tsx b/frontend/app/pages/GolfWatchPage.tsx new file mode 100644 index 0000000..317f860 --- /dev/null +++ b/frontend/app/pages/GolfWatchPage.tsx @@ -0,0 +1,78 @@ +import { Provider as JotaiProvider, createStore } 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 { getToken } from "../auth"; +import GolfWatchApp from "../components/GolfWatchApp"; +import { APP_NAME } from "../config"; +import { usePageTitle } from "../hooks/usePageTitle"; + +type Game = components["schemas"]["Game"]; +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 gameIdNum = Number(gameId); + + usePageTitle( + game + ? `Golf Watching ${game.display_name} | ${APP_NAME}` + : `Golf Watching | ${APP_NAME}`, + ); + + useEffect(() => { + const token = getToken(); + if (!token) return; + const apiClient = createApiClient(token); + Promise.all([ + apiClient.getGame(gameIdNum), + apiClient.getGameWatchRanking(gameIdNum), + apiClient.getGameWatchLatestStates(gameIdNum), + ]) + .then(([{ game }, { ranking }, { states }]) => { + setGame(game); + setRanking(ranking); + setGameStates(states); + }) + .catch(() => navigate("/dashboard")) + .finally(() => setLoading(false)); + }, [gameIdNum, navigate]); + + const store = useMemo(() => { + if (!game) return null; + return createStore(); + }, [game]); + + if (loading || !game || !store) { + return ( + <div className="min-h-screen bg-gray-100 flex items-center justify-center"> + <p className="text-gray-500">Loading...</p> + </div> + ); + } + + const token = getToken()!; + + return ( + <JotaiProvider store={store}> + <ApiClientContext.Provider value={createApiClient(token)}> + <GolfWatchApp + key={game.game_id} + game={game} + initialGameStates={gameStates} + initialRanking={ranking} + /> + </ApiClientContext.Provider> + </JotaiProvider> + ); +} diff --git a/frontend/app/pages/IndexPage.tsx b/frontend/app/pages/IndexPage.tsx new file mode 100644 index 0000000..088cdc5 --- /dev/null +++ b/frontend/app/pages/IndexPage.tsx @@ -0,0 +1,39 @@ +import BorderedContainer from "../components/BorderedContainer"; +import NavigateLink from "../components/NavigateLink"; +import { APP_NAME, BASE_PATH } from "../config"; +import { usePageTitle } from "../hooks/usePageTitle"; + +export default function IndexPage() { + usePageTitle(APP_NAME); + + return ( + <div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center gap-y-6"> + <img + src={`${BASE_PATH}logo.svg`} + alt="iOSDC Japan 2025" + className="w-96 h-auto" + /> + <div className="text-center"> + <div className="font-bold text-transparent bg-clip-text bg-iosdc-japan"> + <div className="text-6xl">Swift Code Battle</div> + </div> + </div> + <div className="mx-2"> + <BorderedContainer> + <p className="text-gray-900 max-w-prose"> + Swift コードバトルは指示された動作をする Swift + コードをより短く書けた方が勝ち、という 1 対 1 + の対戦コンテンツです。9/6 + に実施された予選を勝ち抜いたプレイヤーによるトーナメント形式での + コードバトルを 9/19 (金) day0 + に実施します。ここでは短いコードが正義です! + 可読性も保守性も放り投げた、イベントならではのコードをお楽しみください! + </p> + </BorderedContainer> + </div> + <div> + <NavigateLink to="/login">ログイン</NavigateLink> + </div> + </div> + ); +} diff --git a/frontend/app/pages/LoginPage.tsx b/frontend/app/pages/LoginPage.tsx new file mode 100644 index 0000000..c10b819 --- /dev/null +++ b/frontend/app/pages/LoginPage.tsx @@ -0,0 +1,101 @@ +import { type FormEvent, useState } from "react"; +import { useLocation } from "wouter"; +import BorderedContainer from "../components/BorderedContainer"; +import InputText from "../components/InputText"; +import SubmitButton from "../components/SubmitButton"; +import { APP_NAME } from "../config"; +import { useAuth } from "../hooks/useAuth"; +import { usePageTitle } from "../hooks/usePageTitle"; + +export default function LoginPage() { + usePageTitle(`Login | ${APP_NAME}`); + + const { login } = useAuth(); + const [, navigate] = useLocation(); + + const [error, setError] = useState<string | null>(null); + const [fieldErrors, setFieldErrors] = useState<{ + username?: string; + password?: string; + }>({}); + const [submitting, setSubmitting] = useState(false); + + async function handleSubmit(e: FormEvent<HTMLFormElement>) { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const username = String(formData.get("username")); + const password = String(formData.get("password")); + + const errors: { username?: string; password?: string } = {}; + if (username === "") errors.username = "ユーザー名を入力してください"; + if (password === "") errors.password = "パスワードを入力してください"; + if (Object.keys(errors).length > 0) { + setFieldErrors(errors); + setError("ユーザー名またはパスワードが誤っています"); + return; + } + + setSubmitting(true); + setError(null); + setFieldErrors({}); + + try { + await login(username, password); + navigate("/dashboard"); + } catch (err) { + setError(err instanceof Error ? err.message : "ログインに失敗しました"); + } finally { + setSubmitting(false); + } + } + + return ( + <div className="min-h-screen bg-gray-100 flex items-center justify-center"> + <div className="mx-2"> + <BorderedContainer> + <form onSubmit={handleSubmit} className="w-full max-w-sm p-2"> + <h2 className="text-2xl mb-6 text-center"> + fortee アカウントでログイン + </h2> + {error && <p className="text-sky-500 text-sm mb-4">{error}</p>} + <div className="mb-4 flex flex-col gap-1"> + <label + htmlFor="username" + className="block text-sm font-medium text-gray-700" + > + ユーザー名 + </label> + <InputText type="text" name="username" id="username" required /> + {fieldErrors.username && ( + <p className="text-red-500 text-sm">{fieldErrors.username}</p> + )} + </div> + <div className="mb-6 flex flex-col gap-1"> + <label + htmlFor="password" + className="block text-sm font-medium text-gray-700" + > + パスワード + </label> + <InputText + type="password" + name="password" + id="password" + autoComplete="current-password" + required + /> + {fieldErrors.password && ( + <p className="text-red-500 text-sm">{fieldErrors.password}</p> + )} + </div> + <div className="flex justify-center"> + <SubmitButton type="submit" disabled={submitting}> + {submitting ? "ログイン中..." : "ログイン"} + </SubmitButton> + </div> + </form> + </BorderedContainer> + </div> + </div> + ); +} diff --git a/frontend/app/pages/TournamentPage.tsx b/frontend/app/pages/TournamentPage.tsx new file mode 100644 index 0000000..43ea790 --- /dev/null +++ b/frontend/app/pages/TournamentPage.tsx @@ -0,0 +1,429 @@ +import { useEffect, useState } from "react"; +import { createApiClient } from "../api/client"; +import type { components } from "../api/schema"; +import { getToken } from "../auth"; +import BorderedContainer from "../components/BorderedContainer"; +import UserIcon from "../components/UserIcon"; +import { APP_NAME } from "../config"; +import { usePageTitle } from "../hooks/usePageTitle"; + +type TournamentMatch = components["schemas"]["TournamentMatch"]; +type User = components["schemas"]["User"]; + +function Player({ player, rank }: { player: User | null; rank: number }) { + return ( + <BorderedContainer> + <div className="flex flex-col items-center gap-2"> + <span className="text-gray-800 text-md">予選 {rank} 位</span> + <span className="font-medium text-lg">{player?.display_name}</span> + {player?.icon_path && ( + <UserIcon + iconPath={player.icon_path} + displayName={player.display_name} + className="w-16 h-16 my-auto" + /> + )} + </div> + </BorderedContainer> + ); +} + +function BranchVL({ className = "" }: { className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div></div> + <div className={`border-l-4 ${className}`}></div> + </div> + ); +} + +function BranchVR({ className = "" }: { className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div className={`border-r-4 ${className}`}></div> + <div></div> + </div> + ); +} + +function BranchVL2({ + score, + className = "", +}: { score: number | null; className?: string }) { + return ( + <div className="grid grid-cols-3"> + <div className={`border-r-4 ${className}`}></div> + <div className={`border-t-4 p-2 font-bold text-xl ${className}`}> + {score} + </div> + <div className={`border-t-4 ${className}`}></div> + </div> + ); +} + +function BranchVR2({ + score, + className = "", +}: { score: number | null; className?: string }) { + return ( + <div className="grid grid-cols-3"> + <div className={`border-t-4 ${className}`}></div> + <div className={`border-t-4 p-2 font-bold text-xl ${className}`}> + {score} + </div> + <div className={`border-l-4 ${className}`}></div> + </div> + ); +} + +function BranchV3({ className = "" }: { className?: string }) { + return <div className={`border-r-4 ${className}`}></div>; +} + +function BranchH({ + score, + className1, + className2, + className3, +}: { + score?: number | null; + className1: string; + className2: string; + className3: string; +}) { + return ( + <div className="grid grid-cols-3"> + <div className={`border-t-4 ${className1}`}></div> + <div className={`border-t-4 ${className2}`}></div> + <div className={`border-t-4 p-2 font-bold text-xl ${className3}`}> + {score} + </div> + </div> + ); +} + +function BranchH2({ + score, + className1, + className2, + className3, +}: { + score?: number | null; + className1: string; + className2: string; + className3: string; +}) { + return ( + <div className="grid grid-cols-3"> + <div + className={`border-t-4 p-2 font-bold text-xl text-right ${className1}`} + > + {score} + </div> + <div className={`border-t-4 ${className2}`}></div> + <div className={`border-t-4 ${className3}`}></div> + </div> + ); +} + +function BranchL({ + score, + className = "", +}: { score: number | null; className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div></div> + <div + className={`border-l-4 border-t-4 p-2 font-bold text-xl ${className}`} + > + {score} + </div> + </div> + ); +} + +function BranchR({ + score, + className = "", +}: { score: number | null; className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div + className={`border-r-4 border-t-4 p-2 font-bold text-xl text-right ${className}`} + > + {score} + </div> + <div></div> + </div> + ); +} + +function BranchL2({ className = "" }: { className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div className={`border-l-4 ${className}`}></div> + <div></div> + </div> + ); +} + +function BranchR2({ className = "" }: { className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div></div> + <div className={`border-r-4 ${className}`}></div> + </div> + ); +} + +function getPlayer(match: TournamentMatch, playerID: number): User | null { + if (match.player1?.user_id === playerID) return match.player1; + if (match.player2?.user_id === playerID) return match.player2; + return null; +} + +function getScore(match: TournamentMatch, playerIDs: number[]): number | null { + if (match.player1 && playerIDs.includes(match.player1.user_id)) + return match.player1_score ?? null; + if (match.player2 && playerIDs.includes(match.player2.user_id)) + return match.player2_score ?? null; + return null; +} + +function getBorderColor(match: TournamentMatch, playerIDs: number[]): string { + if (!match.winner) { + return "border-black"; + } + if (playerIDs.includes(match.winner)) { + return "border-pink-700"; + } + return "border-gray-400"; +} + +export default function TournamentPage() { + usePageTitle(`Tournament | ${APP_NAME}`); + + const [tournament, setTournament] = useState<{ + matches: TournamentMatch[]; + } | null>(null); + const [playerIDs, setPlayerIDs] = useState<number[]>([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const game1 = Number(params.get("game1")); + const game2 = Number(params.get("game2")); + const game3 = Number(params.get("game3")); + const game4 = Number(params.get("game4")); + const game5 = Number(params.get("game5")); + + if (!game1 || !game2 || !game3 || !game4 || !game5) { + setError("Missing or invalid game parameters"); + setLoading(false); + return; + } + + const pIDs = [ + Number(params.get("player1")), + Number(params.get("player2")), + Number(params.get("player3")), + Number(params.get("player4")), + Number(params.get("player5")), + Number(params.get("player6")), + ]; + + if (pIDs.some((id) => !id)) { + setError("Missing or invalid player parameters"); + setLoading(false); + return; + } + + setPlayerIDs(pIDs); + + const token = getToken(); + if (!token) return; + const apiClient = createApiClient(token); + apiClient + .getTournament(game1, game2, game3, game4, game5) + .then(({ tournament }) => setTournament(tournament)) + .catch(() => setError("Failed to load tournament")) + .finally(() => setLoading(false)); + }, []); + + if (loading) { + return ( + <div className="min-h-screen bg-gray-100 flex items-center justify-center"> + <p className="text-gray-500">Loading...</p> + </div> + ); + } + + if (error || !tournament) { + return ( + <div className="min-h-screen bg-gray-100 flex items-center justify-center"> + <p className="text-red-500">{error || "Failed to load tournament"}</p> + </div> + ); + } + + const match1 = tournament.matches[0]!; + const match2 = tournament.matches[1]!; + const match3 = tournament.matches[2]!; + const match4 = tournament.matches[3]!; + const match5 = tournament.matches[4]!; + + const playerID1 = playerIDs[0]!; + const playerID2 = playerIDs[1]!; + const playerID3 = playerIDs[2]!; + const playerID4 = playerIDs[3]!; + const playerID5 = playerIDs[4]!; + const playerID6 = playerIDs[5]!; + + const player5 = getPlayer(match1, playerID5); + const player4 = getPlayer(match1, playerID4); + const player3 = getPlayer(match2, playerID3); + const player6 = getPlayer(match2, playerID6); + const player1 = getPlayer(match3, playerID1); + const player2 = getPlayer(match4, playerID2); + + return ( + <div className="p-6 bg-gray-100 min-h-screen"> + <div className="max-w-5xl mx-auto"> + <h1 className="text-3xl font-bold text-transparent bg-clip-text bg-iosdc-japan text-center mb-8"> + iOSDC Japan 2025 Swift Code Battle + </h1> + + <div className="grid grid-rows-5"> + <div className="grid grid-cols-6"> + <div></div> + <div></div> + <BranchV3 + className={getBorderColor(match5, [ + playerID1, + playerID5, + playerID4, + playerID3, + playerID6, + playerID2, + ])} + /> + <div></div> + <div></div> + <div></div> + </div> + <div className="grid grid-cols-6"> + <div></div> + <BranchVL2 + score={getScore(match5, [playerID1, playerID5, playerID4])} + className={getBorderColor(match5, [ + playerID1, + playerID5, + playerID4, + ])} + /> + <BranchH + className1={getBorderColor(match5, [ + playerID1, + playerID5, + playerID4, + ])} + className2={getBorderColor(match5, [ + playerID1, + playerID5, + playerID4, + ])} + className3={getBorderColor(match5, [ + playerID1, + playerID5, + playerID4, + ])} + /> + <BranchH + className1={getBorderColor(match5, [ + playerID3, + playerID6, + playerID2, + ])} + className2={getBorderColor(match5, [ + playerID3, + playerID6, + playerID2, + ])} + className3={getBorderColor(match5, [ + playerID3, + playerID6, + playerID2, + ])} + /> + <BranchVR2 + score={getScore(match5, [playerID3, playerID6, playerID2])} + className={getBorderColor(match5, [ + playerID3, + playerID6, + playerID2, + ])} + /> + <div></div> + </div> + <div className="grid grid-cols-6"> + <BranchL + score={getScore(match3, [playerID1])} + className={getBorderColor(match3, [playerID1])} + /> + <BranchH + score={getScore(match3, [playerID5, playerID4])} + className1={getBorderColor(match3, [playerID1])} + className2={getBorderColor(match3, [playerID5, playerID4])} + className3={getBorderColor(match3, [playerID5, playerID4])} + /> + <BranchL2 + className={getBorderColor(match3, [playerID5, playerID4])} + /> + <BranchR2 + className={getBorderColor(match4, [playerID3, playerID6])} + /> + <BranchH2 + score={getScore(match4, [playerID3, playerID6])} + className1={getBorderColor(match4, [playerID3, playerID6])} + className2={getBorderColor(match4, [playerID3, playerID6])} + className3={getBorderColor(match4, [playerID2])} + /> + <BranchR + score={getScore(match4, [playerID2])} + className={getBorderColor(match4, [playerID2])} + /> + </div> + <div className="grid grid-cols-6"> + <BranchVL className={getBorderColor(match3, [playerID1])} /> + <BranchL + score={getScore(match1, [playerID5])} + className={getBorderColor(match1, [playerID5])} + /> + <BranchR + score={getScore(match1, [playerID4])} + className={getBorderColor(match1, [playerID4])} + /> + <BranchL + score={getScore(match2, [playerID3])} + className={getBorderColor(match2, [playerID3])} + /> + <BranchR + score={getScore(match2, [playerID6])} + className={getBorderColor(match2, [playerID6])} + /> + <BranchVR className={getBorderColor(match4, [playerID2])} /> + </div> + <div className="grid grid-cols-6 gap-6"> + <Player player={player1} rank={1} /> + <Player player={player5} rank={5} /> + <Player player={player4} rank={4} /> + <Player player={player3} rank={3} /> + <Player player={player6} rank={6} /> + <Player player={player2} rank={2} /> + </div> + </div> + </div> + </div> + ); +} |
