diff --git a/api/openapi.yaml b/api/openapi.yaml index aa7e655..1671577 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -69,7 +69,8 @@ paths: in: path required: true schema: - type: integer + type: string + format: uuid responses: 200: $ref: '#/components/responses/taskResponse' @@ -81,7 +82,8 @@ paths: in: path required: true schema: - type: integer + type: string + format: uuid requestBody: $ref: "#/components/requestBodies/enterCodeRequest" responses: @@ -94,7 +96,8 @@ components: type: object properties: id: - type: integer + type: string + format: uuid username: type: string required: [ id, username ] @@ -102,21 +105,44 @@ components: type: object properties: id: - type: integer + type: string + format: uuid title: type: string description: type: string type: $ref: "#/components/schemas/gameType" + points: + type: integer + taskCount: + type: integer + createdAt: + type: string + authors: + type: array + items: + $ref: "#/components/schemas/userView" required: - id - title - description - type + - points + - taskCount + - createdAt + - authors taskView: type: object properties: + message: + type: string + enum: + - ok_code + - invalid_code + - old_code + - next_level + - game_complete title: type: string text: @@ -125,10 +151,6 @@ components: type: array items: $ref: '#/components/schemas/codeView' - entered: - type: array - items: - $ref: '#/components/schemas/codeView' solutions: type: array items: @@ -137,7 +159,6 @@ components: - title - text - codes - - entered - solutions codeView: type: object @@ -278,7 +299,8 @@ components: type: object properties: id: - type: integer + type: string + format: uuid username: type: string email: @@ -287,6 +309,10 @@ components: type: integer level: type: integer + expToCurrentLevel: + type: integer + expToNextLevel: + type: integer games: type: array items: @@ -297,6 +323,8 @@ components: - email - experience - level + - expToCurrentLevel + - expToNextLevel - games errorResponse: description: '' diff --git a/api/server.go b/api/server.go index b911cd5..a76693a 100644 --- a/api/server.go +++ b/api/server.go @@ -16,16 +16,17 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" ) // ServerInterface represents all server handlers. type ServerInterface interface { // (GET /engine/{uid}) - GameEngine(ctx echo.Context, uid int) error + GameEngine(ctx echo.Context, uid openapi_types.UUID) error // (POST /engine/{uid}/code) - EnterCode(ctx echo.Context, uid int) error + EnterCode(ctx echo.Context, uid openapi_types.UUID) error // (GET /games) GetGames(ctx echo.Context) error @@ -55,7 +56,7 @@ type ServerInterfaceWrapper struct { func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error { var err error // ------------- Path parameter "uid" ------------- - var uid int + var uid openapi_types.UUID err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid) if err != nil { @@ -73,7 +74,7 @@ func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error { func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error { var err error // ------------- Path parameter "uid" ------------- - var uid int + var uid openapi_types.UUID err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid) if err != nil { @@ -187,22 +188,24 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/types.go b/api/types.go index 6c2a65c..1af8dd5 100644 --- a/api/types.go +++ b/api/types.go @@ -3,6 +3,10 @@ // Code generated by github.com/deepmap/oapi-codegen/v2 version v2.0.0 DO NOT EDIT. package api +import ( + openapi_types "github.com/oapi-codegen/runtime/types" +) + const ( CookieAuthScopes = "cookieAuth.Scopes" ) @@ -13,6 +17,15 @@ const ( Virtual GameType = "virtual" ) +// Defines values for TaskViewMessage. +const ( + GameComplete TaskViewMessage = "game_complete" + InvalidCode TaskViewMessage = "invalid_code" + NextLevel TaskViewMessage = "next_level" + OkCode TaskViewMessage = "ok_code" + OldCode TaskViewMessage = "old_code" +) + // CodeEdit defines model for codeEdit. type CodeEdit struct { Code string `json:"code"` @@ -39,10 +52,14 @@ type GameType string // GameView defines model for gameView. type GameView struct { - Description string `json:"description"` - Id int `json:"id"` - Title string `json:"title"` - Type GameType `json:"type"` + Authors []UserView `json:"authors"` + CreatedAt string `json:"createdAt"` + Description string `json:"description"` + Id openapi_types.UUID `json:"id"` + Points int `json:"points"` + TaskCount int `json:"taskCount"` + Title string `json:"title"` + Type GameType `json:"type"` } // SolutionEdit defines model for solutionEdit. @@ -67,11 +84,20 @@ type TaskEdit struct { // TaskView defines model for taskView. type TaskView struct { - Codes []CodeView `json:"codes"` - Entered []CodeView `json:"entered"` - Solutions []SolutionView `json:"solutions"` - Text string `json:"text"` - Title string `json:"title"` + Codes []CodeView `json:"codes"` + Message *TaskViewMessage `json:"message,omitempty"` + Solutions []SolutionView `json:"solutions"` + Text string `json:"text"` + Title string `json:"title"` +} + +// TaskViewMessage defines model for TaskView.Message. +type TaskViewMessage string + +// UserView defines model for userView. +type UserView struct { + Id openapi_types.UUID `json:"id"` + Username string `json:"username"` } // ErrorResponse defines model for errorResponse. @@ -91,12 +117,14 @@ type TaskResponse = TaskView // UserResponse defines model for userResponse. type UserResponse struct { - Email string `json:"email"` - Experience int `json:"experience"` - Games []GameView `json:"games"` - Id int `json:"id"` - Level int `json:"level"` - Username string `json:"username"` + Email string `json:"email"` + ExpToCurrentLevel int `json:"expToCurrentLevel"` + ExpToNextLevel int `json:"expToNextLevel"` + Experience int `json:"experience"` + Games []GameView `json:"games"` + Id openapi_types.UUID `json:"id"` + Level int `json:"level"` + Username string `json:"username"` } // EnterCodeRequest defines model for enterCodeRequest. diff --git a/docker-compose.yml b/docker-compose.yml index 8b1d7ed..dd69a3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,26 +4,6 @@ volumes: postgres-data: services: - # app: - # build: - # context: . - # dockerfile: Dockerfile - # env_file: - # # Ensure that the variables in .env match the same variables in devcontainer.json - # - .env - - # volumes: - # - ../..:/workspaces:cached - - # # Overrides default command so things don't shut down after the process ends. - # command: sleep infinity - - # # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. - # network_mode: service:db - - # # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. - # # (Adding the "ports" property to this file will not forward from a Codespace.) - db: image: postgres:15-alpine3.17 restart: unless-stopped diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4308c8d..a918d9a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@neonxp/compose": "0.0.6", + "moment": "^2.30.1", "react": "^18.2.0", "react-bootstrap": "^2.9.1", "react-dom": "^18.2.0", @@ -3735,6 +3736,14 @@ "node": "*" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 84ab077..d865f88 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@neonxp/compose": "0.0.6", + "moment": "^2.30.1", "react": "^18.2.0", "react-bootstrap": "^2.9.1", "react-dom": "^18.2.0", diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 0000000..ce1dc0e Binary files /dev/null and b/frontend/public/logo.png differ diff --git a/frontend/public/logo_small.png b/frontend/public/logo_small.png new file mode 100644 index 0000000..9785f26 Binary files /dev/null and b/frontend/public/logo_small.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 00dedcf..6dba62a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,13 +6,9 @@ import Index from './pages/Index' import Login from './pages/Login' import Register from './pages/Register' import NoMatch from './pages/NoMatch' -import Team from './pages/Team' -import Teams from './pages/Teams' import { UserProvider } from './store/user' import { ajax } from './utils/fetch' -import TeamNew from './pages/TeamNew' -import Admin from './pages/Admin' -import AdminGame from './pages/AdminGame' +import Engine from './pages/Engine' const router = createBrowserRouter( createRoutesFromElements( @@ -30,20 +26,11 @@ const router = createBrowserRouter( } /> } /> } - loader={() => ajax("/api/teams")} + path="go/:gameId" + element={} + loader={({ params }) => ajax(`/api/engine/${params.gameId}`)} /> - } - /> - } - loader={({ params }) => ajax(`/api/teams/${params.teamId}`)} - /> - } loader={() => ajax(`/api/admin/games`)} @@ -55,7 +42,7 @@ const router = createBrowserRouter( title: "Новая игра", tasks: [] })} - /> + /> */} } /> ) diff --git a/frontend/src/assets/TiltNeon-Regular.ttf b/frontend/src/assets/TiltNeon-Regular.ttf deleted file mode 100644 index 0cbb167..0000000 Binary files a/frontend/src/assets/TiltNeon-Regular.ttf and /dev/null differ diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000..ce1dc0e Binary files /dev/null and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/assets/logo_small.png b/frontend/src/assets/logo_small.png new file mode 100644 index 0000000..9785f26 Binary files /dev/null and b/frontend/src/assets/logo_small.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/styles.css b/frontend/src/assets/styles.css index e18e68e..d6c46c6 100644 --- a/frontend/src/assets/styles.css +++ b/frontend/src/assets/styles.css @@ -2,12 +2,15 @@ .navbar-dark { border-bottom: 0.1px solid #333333 !important; } -@font-face { +.navbar { + background-color: rgb(16, 22, 29) !important; +} +/* @font-face { font-family: 'TiltNeon'; src: url('TiltNeon-Regular.ttf'); -} +} */ .navbar-brand { - font-family: TiltNeon, var(--bs-font-sans-serif); + /* font-family: TiltNeon, var(--bs-font-sans-serif); text-shadow: 0 0 1px rgb(255, 255, 255, 1), 0 0 2px rgb(255, 255, 255, 1), @@ -17,10 +20,14 @@ 0 1px #198754, 1px 0 #198754, 0 -1px #198754; - -webkit-text-stroke: 1px white; + -webkit-text-stroke: 1px white; */ font-size: 26px; padding-top: 0 !important; padding-bottom: 0 !important; + height: 40px; + width: 85px; + background-image: url("./logo_small.png"); + background-size: 100%; } th.thin { @@ -28,3 +35,11 @@ th.thin { white-space: nowrap; } +* { + --bs-body-bg: rgb(23, 30, 38) !important; + --bs-primary-rgb: rgb(16, 22, 29) !important; + --bs-navbar-color: rgb(249, 115, 22) !important; + --bs-btn-bg: rgb(249, 115, 22) !important; + --bs-btn-hover-bg: rgba(249, 115, 22, 0.5) !important; + --bs-btn-active-bg: rgba(249, 115, 22, 0.5) !important; +} \ No newline at end of file diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index c9e0396..c549e00 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -1,7 +1,7 @@ import { Link, Outlet, useLoaderData } from "react-router-dom"; -import { Button, Nav, Container, NavbarBrand, NavbarToggle, Navbar, NavbarCollapse, ButtonGroup } from "react-bootstrap"; +import { Button, Nav, Container, NavbarBrand, NavbarToggle, Navbar, NavbarCollapse, ButtonGroup, ProgressBar, OverlayTrigger, Tooltip, Col, Row, NavDropdown } from "react-bootstrap"; import { UserProvider } from "../store/user"; -import { useEffect } from "react"; +import { useEffect, useRef, useState } from "react"; import { ajax } from "../utils/fetch"; import { useRole } from "../utils/roles"; @@ -20,46 +20,44 @@ export default () => { }). then(() => setUser(null)) } - return (<> - + - nQuest + - + - + + diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx deleted file mode 100644 index 1f92ca8..0000000 --- a/frontend/src/pages/Admin.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Link, useLoaderData } from "react-router-dom"; -import { Button, Table } from 'react-bootstrap'; - -export default () => { - const games = useLoaderData(); - if (!games) { - return null - } - - return (<> -

Управление играми

- - - - - - - - - - {games.map(game => ( - - - - - - ))} - -
IDНазваниеСоздана
{game.id}{game.title}{team.createdAt}
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/AdminGame.jsx b/frontend/src/pages/AdminGame.jsx deleted file mode 100644 index 26d9b0b..0000000 --- a/frontend/src/pages/AdminGame.jsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useLoaderData } from "react-router-dom"; -import { Col, Row, Form, Button, Card } from 'react-bootstrap'; -import { useEffect, useState } from "react"; - -export default () => { - const loadedGame = useLoaderData(); - const [game, setGame] = useState(loadedGame); - const [error, setError] = useState(null); - - if (!game) { - return null - } - const submit = (e) => { - e.preventDefault(); - console.log(game) - } - return (<> -

Игра "{game.title}"

-
-
- {error ? (
{error}
) : null} - - - Название игры - - setGame({ ...game, title: e.target.value })} - /> - - - - Начало в - - - - - - Описание - - setGame({ ...game, description: e.target.value })} - /> - - -
-

- Задания - -

- {game.tasks.map((task, idx) => - { - const newTasks = game.tasks; - newTasks[idx] = task; - setGame({...game, tasks: newTasks}); - }} - /> - )} -
- -
-
- ); -} - -const Task = ({id, task, setTask}) => ( - - - - Задание #{id + 1} - - - Задание - - setTask({ ...task, text: e.target.value })} - /> - - - - -) \ No newline at end of file diff --git a/frontend/src/pages/Engine.jsx b/frontend/src/pages/Engine.jsx new file mode 100644 index 0000000..506bbd1 --- /dev/null +++ b/frontend/src/pages/Engine.jsx @@ -0,0 +1,75 @@ +import { Badge, Button, Col, Form, Row, Table } from "react-bootstrap"; +import { Link, useLoaderData, useParams } from "react-router-dom"; +import Markdown from "react-markdown"; +import { useState } from "react"; +import { ajax } from "../utils/fetch"; + +export default () => { + const params = useParams(); + const loadedTask = useLoaderData(); + const [task, setTask] = useState(loadedTask); + const [code, setCode] = useState(""); + const onSubmitCode = (e) => { + e.preventDefault(); + ajax(`/api/engine/${params.gameId}/code`, { + method: "POST", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ code }) + }). + then((x) => { + if (x != null) { + setTask(x); + setCode(""); + } + }).catch(e => { + console.warn(e); + }); + } + + if (task && task.message == "game_complete") { + return (
+
Вы прошли все уровни!
+ К списку игр +
); + } + if (!task) { + return (
+
Для вас не предусмотренно уровней
+ К списку игр +
); + } + + return (<> +

{task.title}

+ {task.text} + {task.message == "invalid_code" ? (
Неверный код
) : null} + {task.message == "old_code" ? (
Этот код уже вводился
) : null} + {task.message == "next_level" ? (
Переход на новый уровень
) : null} + {task.message == "ok_code" ? (
Код принят, ищите оставшиеся
) : null} +

Коды:

+
    + {task.codes.map( + (c, idx) =>
  • {c.description} {!!c.code ? (Принят {c.code}) : (Не введён)}
  • + )} +
+
+ + Код: + + setCode(e.target.value)} + /> + + + + + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Index.jsx b/frontend/src/pages/Index.jsx index 44f7952..38638c8 100644 --- a/frontend/src/pages/Index.jsx +++ b/frontend/src/pages/Index.jsx @@ -1,47 +1,72 @@ import { Button, Table } from "react-bootstrap"; import { Link, useLoaderData } from "react-router-dom"; import Markdown from "react-markdown"; +import moment from 'moment/min/moment-with-locales'; +import { UserProvider } from "../store/user"; export default () => { + moment.locale('ru'); const games = useLoaderData(); - + const { user } = UserProvider.useContainer(); return (<> -

Текущие игры

+

Доступные квесты

{games && games.map(game => ( - <> +

{game.title}

- - + + - + + + + + + + + + + - - - -
- Начало + + Тип - {game.startAt} + + {game.type} + + Опыт за квест + + {game.points}
+ + Уровней + + {game.taskCount} + + Опубликовано + + {moment(game.createdAt).fromNow()} +
+ Автор + + {game.authors.map(a => {a.username})} +
{game.description}
- + {user ? (<> + {(!!user.games.find(x => x.id === game.id)) + ? (Вы уже прошли этот квест) + : (Начать прохождение)} + ): null} +
- Участвуют: - -
    - {game.teams.map(team => (
  • {team.name}
  • ))} - {game.teams.length == 0 ?

    Никто пока не подал заявку

    : null} -
-
- +
))} {!games ? (Игр пока не анонсировано) : null} ); diff --git a/frontend/src/pages/Team.jsx b/frontend/src/pages/Team.jsx deleted file mode 100644 index 2f892aa..0000000 --- a/frontend/src/pages/Team.jsx +++ /dev/null @@ -1,175 +0,0 @@ -import { useLoaderData, useNavigate, useRouteLoaderData } from "react-router-dom"; -import { Alert, Button, ButtonGroup, Table } from "react-bootstrap"; -import { UserProvider } from "../store/user"; -import { ajax } from "../utils/fetch"; -import { useEffect, useState } from "react"; - -const userRoles = { captain: "Капитан", member: "Участник" }; - -export default () => { - const teamFromRouter = useLoaderData(); - const [team, setTeam] = useState(teamFromRouter); - const [error, setError] = useState(null); - useEffect(() => { - setTeam(teamFromRouter); - }, [teamFromRouter]); - const navigate = useNavigate(); - - const { user, setUser } = UserProvider.useContainer(); - - const [request, setRequest] = useState(false); - - useEffect(() => { - setRequest(!!team.requests.find(x => user && x.user.id == user.id)); - }, [user, team.requests]) - - if (!team) { - return null; - } - - if (!user) { - return null; - } - - const member = team.members.find(tm => tm.user.id === user.id); - const inOtherTeam = user.team && user.team.td != team.id; - const isCaptain = member && user.team.role == "captain"; - - const sendRequest = () => { - ajax(`/api/teams/${team.id}/requests`, { - method: "POST", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }).then(() => setRequest(true)).catch(setError); - }; - const updateMemebers = (members) => { - ajax(`/api/teams/${team.id}/members`, { - method: "POST", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ members }), - }).then(team => setTeam(team)).catch(setError); - }; - const approveRequest = (userID, approve) => { - ajax(`/api/teams/${team.id}/requests/${userID}`, { - method: "POST", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ approve }), - }).then(team => setTeam(team)).catch(setError); - } - const removeMember = (userID) => { - const members = team.members.map(x => x.user.id).filter(uid => uid != userID); - return updateMemebers(members) - } - const leaveTeam = () => { - ajax(`/api/teams/${team.id}/members`, { - method: "DELETE", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - }).then(team => setTeam(team)).then(() => { - setUser({ - ...user, - team: null, - }); - }).catch(setError); - }; - const deleteTeam = () => { - ajax(`/api/teams/${team.id}`, { - method: "DELETE", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - }).then(() => { - setUser({ - ...user, - team: null, - }); - navigate("/teams"); - }).catch(setError); - }; - - return (<> -

{team.name}

-

Создана: {team.createdAt}

- - {!member && !inOtherTeam && !request ? () : null} - {request ? (Заявка в команду отправлена) : null} - {error ? ({error}) : null} - {member && !isCaptain ? () : null} - -

Участники

- - - - - - - - - - - {team.members.map(tm => ( - - - - - - - ))} - -
Имя пользователяРольПрисоединился
{tm.user.username}{userRoles[tm.role]}{tm.createdAt} - { - isCaptain && tm.user.id != user.id - ? () - : null - } -
- {isCaptain - ? (<> -

Заявки

- - - - - - - - - - {team.requests.map(tm => ( - - - - - - ))} - -
Имя пользователяДата заявки
{tm.user.username}{tm.createdAt} - - - - -
- ) - : null} - {isCaptain && (team.members.length == 1) ? : null} - ); -} \ No newline at end of file diff --git a/frontend/src/pages/TeamNew.jsx b/frontend/src/pages/TeamNew.jsx deleted file mode 100644 index 7498c5c..0000000 --- a/frontend/src/pages/TeamNew.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useState } from "react"; -import { Form, Button, Row, Col } from "react-bootstrap"; -import { useNavigate } from "react-router-dom"; -import { ajax } from "../utils/fetch"; -import { UserProvider } from "../store/user"; - -export default () => { - const {user, setUser} = UserProvider.useContainer(); - const [name, setName] = useState(""); - const [error, setError] = useState(null); - const navigate = useNavigate(); - - const onCreate = (e) => { - e.preventDefault(); - ajax("/api/teams", { - method: "POST", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ name }) - }).then((team) => { - setUser({ - ...user, - team: { - role: 1, - ...team, - } - }); - navigate(`/teams/${team.id}`); - }).catch(e => setError(e.message)) - } - return (<> -

Создание команды

-
-
- {error ? (
{error}
) : null} - - Название команды - - setName(e.target.value)} - /> - - - -
-
- ) -}; \ No newline at end of file diff --git a/frontend/src/pages/Teams.jsx b/frontend/src/pages/Teams.jsx deleted file mode 100644 index 96ab72d..0000000 --- a/frontend/src/pages/Teams.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Link, useLoaderData } from "react-router-dom"; -import { Table } from 'react-bootstrap'; -import { UserProvider } from "../store/user"; - -export default () => { - const teams = useLoaderData(); - if (!teams) { - return null - } - const { user } = UserProvider.useContainer(); - - return (<> -

Команды

- {user && !user.team - ? (

Вы не состоите в командах. Создать свою команду.

) - : null} - - - - - - - - - - - {teams.map(team => ( - - - - - - ))} - -
КомандаУчастниковСоздана
{team.name}{team.members}{team.createdAt}
- ); -} \ No newline at end of file diff --git a/pkg/controller/admin.go b/pkg/controller/admin.go index fe13302..62b7ace 100644 --- a/pkg/controller/admin.go +++ b/pkg/controller/admin.go @@ -3,6 +3,7 @@ package controller import ( "net/http" + "github.com/google/uuid" "github.com/labstack/echo/v4" "gitrepo.ru/neonxp/nquest/api" "gitrepo.ru/neonxp/nquest/pkg/contextlib" @@ -31,7 +32,7 @@ func (a *Admin) CreateGame(ctx echo.Context) error { } return ctx.JSON(http.StatusCreated, api.GameResponse{ - Id: int(game.ID), + Id: game.ID, Title: game.Title, Description: game.Description, }) @@ -39,6 +40,9 @@ func (a *Admin) CreateGame(ctx echo.Context) error { func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) *models.Game { game := &models.Game{ + Model: models.Model{ + ID: uuid.New(), + }, Visible: false, Title: req.Title, Description: req.Description, @@ -49,22 +53,32 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) Tasks: make([]*models.Task, 0, len(req.Tasks)), Points: req.Points, } - for _, te := range req.Tasks { + for order, te := range req.Tasks { task := &models.Task{ + Model: models.Model{ + ID: uuid.New(), + }, Title: te.Title, Text: te.Text, MaxTime: 0, Solutions: make([]*models.Solution, 0, len(te.Solutions)), Codes: make([]*models.Code, 0, len(te.Codes)), + TaskOrder: uint(order), } for _, s := range te.Solutions { task.Solutions = append(task.Solutions, &models.Solution{ + Model: models.Model{ + ID: uuid.New(), + }, After: s.After, Text: s.Text, }) } for _, ce := range te.Codes { task.Codes = append(task.Codes, &models.Code{ + Model: models.Model{ + ID: uuid.New(), + }, Code: ce.Code, Description: ce.Description, }) diff --git a/pkg/controller/engine.go b/pkg/controller/engine.go index 7ffbf68..6b9ca0e 100644 --- a/pkg/controller/engine.go +++ b/pkg/controller/engine.go @@ -1,8 +1,10 @@ package controller import ( + "errors" "net/http" + "github.com/google/uuid" "github.com/labstack/echo/v4" "gitrepo.ru/neonxp/nquest/api" "gitrepo.ru/neonxp/nquest/pkg/contextlib" @@ -16,10 +18,10 @@ type Engine struct { } // (GET /engine/{uid}) -func (ec *Engine) GameEngine(c echo.Context, uid int) error { +func (ec *Engine) GameEngine(c echo.Context, uid uuid.UUID) error { user := contextlib.GetUser(c) - game, err := ec.GameService.GetByID(c.Request().Context(), uint(uid)) + game, err := ec.GameService.GetByID(c.Request().Context(), uid) if err != nil { return err } @@ -29,16 +31,16 @@ func (ec *Engine) GameEngine(c echo.Context, uid int) error { return err } - return c.JSON(http.StatusOK, mapCursorToTask(cursor)) + return c.JSON(http.StatusOK, mapCursorToTask(cursor, nil)) } // (POST /engine/{uid}/code) -func (ec *Engine) EnterCode(c echo.Context, uid int) error { +func (ec *Engine) EnterCode(c echo.Context, uid uuid.UUID) error { user := contextlib.GetUser(c) ctx := c.Request().Context() - game, err := ec.GameService.GetByID(ctx, uint(uid)) + game, err := ec.GameService.GetByID(ctx, uid) if err != nil { return err } @@ -49,31 +51,48 @@ func (ec *Engine) EnterCode(c echo.Context, uid int) error { } cursor, err := ec.EngineService.EnterCode(ctx, game, user, req.Code) + message := api.OkCode if err != nil { - return err + switch { + case errors.Is(err, service.ErrGameFinished): + message = api.GameComplete + case errors.Is(err, service.ErrInvalidCode): + message = api.InvalidCode + case errors.Is(err, service.ErrOldCode): + message = api.OldCode + case errors.Is(err, service.ErrNextLevel): + message = api.NextLevel + default: + return c.JSON(http.StatusBadRequest, &api.ErrorResponse{ + Code: http.StatusBadRequest, + Message: err.Error(), + }) + } } - return c.JSON(http.StatusOK, mapCursorToTask(cursor)) + return c.JSON(http.StatusOK, mapCursorToTask(cursor, &message)) } -func mapCursorToTask(cursor *models.GameCursor) *api.TaskView { +func mapCursorToTask(cursor *models.GameCursor, message *api.TaskViewMessage) *api.TaskView { resp := &api.TaskResponse{ + Message: message, Codes: make([]api.CodeView, 0, len(cursor.Task.Codes)), - Entered: make([]api.CodeView, 0, len(cursor.Codes)), Solutions: []api.SolutionView{}, Text: cursor.Task.Text, Title: cursor.Task.Title, } for _, code := range cursor.Task.Codes { - resp.Codes = append(resp.Codes, api.CodeView{ + c := api.CodeView{ Description: code.Description, - }) - } - for _, code := range cursor.Codes { - resp.Entered = append(resp.Entered, api.CodeView{ - Code: &code.Code, - Description: code.Description, - }) + } + for _, cd := range cursor.Codes { + if cd.ID == code.ID { + c.Code = &cd.Code + break + } + } + resp.Codes = append(resp.Codes, c) } + return resp } diff --git a/pkg/controller/game.go b/pkg/controller/game.go index 3e48425..6502c53 100644 --- a/pkg/controller/game.go +++ b/pkg/controller/game.go @@ -2,6 +2,7 @@ package controller import ( "net/http" + "time" "github.com/labstack/echo/v4" "gitrepo.ru/neonxp/nquest/api" @@ -21,11 +22,23 @@ func (g *Game) GetGames(ctx echo.Context) error { resp := make(api.GameListResponse, 0, len(games)) for _, game := range games { - resp = append(resp, api.GameView{ - Id: int(game.ID), + gv := api.GameView{ + Id: game.ID, Title: game.Title, Description: game.Description, - }) + Type: api.MapGameTypeReverse(game.Type), + Points: game.Points, + TaskCount: len(game.Tasks), + Authors: make([]api.UserView, 0, len(game.Authors)), + CreatedAt: game.CreatedAt.Format(time.RFC3339), + } + for _, u := range game.Authors { + gv.Authors = append(gv.Authors, api.UserView{ + Id: u.ID, + Username: u.Username, + }) + } + resp = append(resp, gv) } return ctx.JSON(http.StatusOK, resp) diff --git a/pkg/controller/user.go b/pkg/controller/user.go index e204fa2..67a00fc 100644 --- a/pkg/controller/user.go +++ b/pkg/controller/user.go @@ -9,6 +9,7 @@ import ( "gitrepo.ru/neonxp/nquest/api" "gitrepo.ru/neonxp/nquest/pkg/models" "gitrepo.ru/neonxp/nquest/pkg/service" + "gitrepo.ru/neonxp/nquest/pkg/utils" ) type User struct { @@ -116,7 +117,7 @@ func setUser(c echo.Context, user *models.User) error { MaxAge: 86400 * 7, HttpOnly: true, } - sess.Values["userID"] = user.ID + sess.Values["userID"] = user.ID.String() if err := sess.Save(c.Request(), c.Response()); err != nil { return err @@ -128,22 +129,24 @@ func setUser(c echo.Context, user *models.User) error { func mapUser(c echo.Context, user *models.User) error { games := make([]api.GameView, 0) for _, gc := range user.Games { - if gc.Status == models.TaskFinished && gc.Task.Next == nil { - games = append(games, api.GameView{ - Id: int(gc.GameID), - Title: gc.Game.Title, - Description: gc.Game.Description, - Type: api.MapGameTypeReverse(gc.Game.Type), - }) - } + games = append(games, api.GameView{ + Id: gc.GameID, + Title: gc.Game.Title, + Description: gc.Game.Description, + Type: api.MapGameTypeReverse(gc.Game.Type), + }) } + level := utils.ExpToLevel(user.Experience) + return c.JSON(http.StatusOK, &api.UserResponse{ - Id: int(user.ID), - Username: user.Username, - Email: user.Email, - Experience: user.Experience, - Level: user.Experience / 1000, - Games: games, + Id: user.ID, + Username: user.Username, + Email: user.Email, + Experience: user.Experience, + ExpToCurrentLevel: utils.LevelToExp(level), + ExpToNextLevel: utils.LevelToExp(level + 1), + Level: int(level), + Games: games, }) } diff --git a/pkg/models/cursor.go b/pkg/models/cursor.go index 63d7548..caaab88 100644 --- a/pkg/models/cursor.go +++ b/pkg/models/cursor.go @@ -1,18 +1,23 @@ package models -import "time" +import ( + "time" + + "github.com/google/uuid" +) type GameCursor struct { - User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` - UserID uint `gorm:"primaryKey"` - Game *Game `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` - GameID uint `gorm:"primaryKey"` - Task *Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` - TaskID uint `gorm:"primaryKey"` + User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` + UserID uuid.UUID `gorm:"primaryKey"` + Game *Game `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` + GameID uuid.UUID `gorm:"primaryKey"` + Task *Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` + TaskID uuid.UUID `gorm:"primaryKey"` CreatedAt time.Time FinishedAt *time.Time Status CursorStatus Codes []*Code `gorm:"many2many:passing_codes;"` + Finish bool } type CursorStatus int diff --git a/pkg/models/model.go b/pkg/models/model.go index 572add7..cb8b301 100644 --- a/pkg/models/model.go +++ b/pkg/models/model.go @@ -3,11 +3,12 @@ package models import ( "time" + "github.com/google/uuid" "gorm.io/gorm" ) type Model struct { - ID uint `gorm:"primarykey" json:"id"` + ID uuid.UUID `gorm:"primarykey" json:"id"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` diff --git a/pkg/models/task.go b/pkg/models/task.go index 2ce5de8..5a0df22 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -1,22 +1,23 @@ package models +import "github.com/google/uuid" + type Task struct { Model Title string Text string MaxTime int - GameID uint + GameID uuid.UUID Solutions []*Solution `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` - Next *Task `gorm:"foreignKey:NextID"` - NextID *uint + TaskOrder uint } type Solution struct { Model - TaskID uint + TaskID uuid.UUID After int Text string } @@ -24,7 +25,7 @@ type Solution struct { type Code struct { Model - TaskID uint + TaskID uuid.UUID Code string `gorm:"index"` Description string } diff --git a/pkg/service/engine.go b/pkg/service/engine.go index ab9e4bd..c0086b4 100644 --- a/pkg/service/engine.go +++ b/pkg/service/engine.go @@ -5,6 +5,7 @@ import ( "errors" "strings" + "github.com/google/uuid" "github.com/jackc/pgx/v5/pgconn" "gitrepo.ru/neonxp/nquest/pkg/models" "gorm.io/gorm" @@ -15,6 +16,7 @@ var ( ErrInvalidCode = errors.New("invalid code") ErrOldCode = errors.New("old code") ErrGameFinished = errors.New("game finished") + ErrNextLevel = errors.New("next level") ) type Engine struct { @@ -30,20 +32,18 @@ func NewEngine(db *gorm.DB) *Engine { func (e *Engine) GetState(ctx context.Context, game *models.Game, user *models.User) (*models.GameCursor, error) { db := e.DB.WithContext(ctx) - // Пытаемся получить GamePassing + // Пытаемся получить GameCursor cursor := &models.GameCursor{ User: user, Game: game, Task: game.Tasks[0], Status: models.TaskStarted, - Codes: []*models.Code{}, + Codes: make([]*models.Code, 0), } err := db. Where(`user_id = ? and game_id = ? and status = ?`, user.ID, game.ID, models.TaskStarted). Preload("Task"). Preload("Task.Codes"). - Preload("Task.Next"). - Preload("Task.Next.Codes"). Preload("Codes"). FirstOrCreate(cursor). Error @@ -62,62 +62,91 @@ func (e *Engine) GetState(ctx context.Context, game *models.Game, user *models.U func (e *Engine) EnterCode(ctx context.Context, game *models.Game, user *models.User, code string) (*models.GameCursor, error) { db := e.DB.WithContext(ctx) + st, err := e.GetState(ctx, game, user) if err != nil { return nil, err } - code = strings.Trim(code, " \n\t") - code = strings.ToLower(code) - var currentCode *models.Code - for _, c := range st.Task.Codes { - if c.Code == code { - currentCode = c - break + + return st, db.Transaction(func(tx *gorm.DB) error { + code = strings.Trim(code, " \n\t") + code = strings.ToLower(code) + var currentCode *models.Code + for _, c := range st.Task.Codes { + if c.Code == code { + currentCode = c + break + } } - } - if currentCode == nil { - return nil, ErrInvalidCode - } - for _, c := range st.Codes { - if c.ID == currentCode.ID { - return nil, ErrOldCode + if currentCode == nil { + return ErrInvalidCode } - } - - st.Codes = append(st.Codes, currentCode) - - if err := db.Save(st).Error; err != nil { - return nil, err - } - - if len(st.Codes) != len(st.Task.Codes) { - return st, nil - } - - // Уровень пройден. Выдаем следующий - - st.Status = models.TaskFinished - if err := db.Save(st).Error; err != nil { - return nil, err - } - - if st.Task.Next == nil { - - user.Experience += st.Game.Points - if err := db.Save(user).Error; err != nil { - return nil, err + for _, c := range st.Codes { + if c.ID == currentCode.ID { + return ErrOldCode + } } - return nil, ErrGameFinished - } + if err := db.Model(st).Association("Codes").Append(currentCode); err != nil { + return err + } - newState := &models.GameCursor{ - User: user, - Game: game, - Task: st.Task.Next, - Status: models.TaskStarted, - Codes: []*models.Code{}, - } + if len(st.Codes) != len(st.Task.Codes) { + return nil + } - return newState, db.Create(newState).Error + // Уровень пройден. Выдаем следующий + + if err := db.Model(st).UpdateColumn("Status", models.TaskFinished).Error; err != nil { + return err + } + + nextTask, err := e.GetNext(ctx, game.ID, st.Task.TaskOrder) + if err != nil { + return err + } + + if nextTask == nil { + user.Experience += st.Game.Points + if err := db.Model(user).UpdateColumn("Experience", user.Experience).Error; err != nil { + return err + } + if err := db.Model(st).UpdateColumn("Finish", true).Error; err != nil { + return err + } + + return ErrGameFinished + } + + st = &models.GameCursor{ + User: user, + Game: game, + Task: nextTask, + Status: models.TaskStarted, + Codes: []*models.Code{}, + } + if err := db.Create(st).Error; err != nil { + return err + } + + return ErrNextLevel + }) +} + +func (e *Engine) GetNext(ctx context.Context, gameID uuid.UUID, currentOrder uint) (*models.Task, error) { + var t models.Task + err := e.DB.WithContext(ctx). + Preload("Codes"). + Preload("Solutions"). + Order("task_order ASC"). + First(&t, `game_id = ? AND task_order > ?`, gameID, currentOrder). + Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + + return nil, err + } + return &t, nil } diff --git a/pkg/service/game.go b/pkg/service/game.go index 5bc8192..c28475f 100644 --- a/pkg/service/game.go +++ b/pkg/service/game.go @@ -3,6 +3,7 @@ package service import ( "context" + "github.com/google/uuid" "gitrepo.ru/neonxp/nquest/pkg/models" "gorm.io/gorm" ) @@ -18,12 +19,16 @@ func NewGame(db *gorm.DB) *Game { } } -func (gs *Game) GetByID(ctx context.Context, id uint) (*models.Game, error) { +func (gs *Game) GetByID(ctx context.Context, id uuid.UUID) (*models.Game, error) { g := &models.Game{} return g, gs.DB. WithContext(ctx). - Preload("Tasks"). + Preload("Tasks", func(db *gorm.DB) *gorm.DB { + return db.Order("tasks.task_order ASC") + }). + Preload("Tasks.Codes"). + Preload("Tasks.Solutions"). First(g, id). Error } @@ -34,15 +39,17 @@ func (gs *Game) List(ctx context.Context) ([]*models.Game, error) { return games, gs.DB. WithContext(ctx). Order("created_at DESC"). - Find(&games, "visible = true"). + Preload("Tasks"). + Preload("Authors"). + Find(&games). Limit(20). Error } -func (gs *Game) GetTaskID(ctx context.Context, id uint) (*models.Task, error) { +func (gs *Game) GetTaskID(ctx context.Context, id uuid.UUID) (*models.Task, error) { t := &models.Task{} - return t, gs.DB.WithContext(ctx).Preload("Next").First(t, id).Error + return t, gs.DB.WithContext(ctx).First(t, id).Error } func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*models.Game, error) { @@ -59,19 +66,8 @@ func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*model } func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game, error) { - return game, gs.DB.Transaction(func(tx *gorm.DB) error { - if err := tx.Create(game).Error; err != nil { - return err - } - for i, t := range game.Tasks { - if i < len(game.Tasks)-1 { - t.Next = game.Tasks[i+1] - if err := tx.Save(t).Error; err != nil { - return err - } - - } - } - return nil - }) + return game, gs.DB. + Session(&gorm.Session{FullSaveAssociations: true}). + Create(game). + Error } diff --git a/pkg/service/user.go b/pkg/service/user.go index 55c658d..d57f25e 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -7,6 +7,7 @@ import ( "net/mail" normalizer "github.com/dimuska139/go-email-normalizer" + "github.com/google/uuid" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" @@ -58,6 +59,9 @@ func (s *User) Register(ctx context.Context, username, email, password, password } u := &models.User{ + Model: models.Model{ + ID: uuid.New(), + }, Username: username, Email: normalizer.NewNormalizer().Normalize(email), Password: hex.EncodeToString(hashed), @@ -99,14 +103,12 @@ func (s *User) Login(ctx context.Context, email, password string) (*models.User, return u, nil } -func (s *User) GetUserByID(ctx context.Context, userID uint) (*models.User, error) { +func (s *User) GetUserByID(ctx context.Context, userID uuid.UUID) (*models.User, error) { u := new(models.User) return u, s.DB.WithContext(ctx). - Preload("Games"). + Preload("Games", `Finish = true`). Preload("Games.Game"). - Preload("Games.Task"). - Preload("Games.Task.Next"). First(u, userID).Error } @@ -116,12 +118,16 @@ func (s *User) GetUser(c echo.Context) *models.User { return nil } - userID, ok := sess.Values["userID"].(uint) + userID, ok := sess.Values["userID"].(string) if !ok { return nil } + uid, err := uuid.Parse(userID) + if err != nil { + return nil + } - user, err := s.GetUserByID(c.Request().Context(), userID) + user, err := s.GetUserByID(c.Request().Context(), uid) if err != nil { return nil } diff --git a/pkg/utils/exp.go b/pkg/utils/exp.go new file mode 100644 index 0000000..0ef15a6 --- /dev/null +++ b/pkg/utils/exp.go @@ -0,0 +1,11 @@ +package utils + +import "math" + +func ExpToLevel(exp int) int { + return int(math.Floor((math.Sqrt(625+100*float64(exp))-25)/50)) + 1 +} + +func LevelToExp(level int) int { + return 25*level*level - 25*level +} diff --git a/requests.http b/requests.http index 680d575..b763010 100644 --- a/requests.http +++ b/requests.http @@ -33,13 +33,14 @@ POST http://localhost:8000/api/games Content-Type: application/json { - "title": "Тестовая игра", + "title": "Тестовая игра 2", "description": "Описание тестовой игры", "type": "city", + "points": 500, "tasks": [ { "title": "Задание 1", - "text": "Текст первого задания", + "text": "Текст первого задания.\n\n*Коды: `nq1111`*", "codes": [ { "description": "1+", @@ -96,11 +97,114 @@ Content-Type: application/json ### -GET http://localhost:8000/api/engine/1 + +POST http://localhost:8000/api/games +Content-Type: application/json + +{ + "title": "Тестовая игра 3", + "description": "Описание тестовой игры", + "type": "city", + "points": 500, + "tasks": [ + { + "title": "Задание 1", + "text": "Текст первого задания.\n\n*Коды: `nq1111`*", + "codes": [ + { + "description": "1+", + "code": "nq1111" + } + ], + "solutions": [] + }, + { + "title": "Задание 2", + "text": "Текст второго задания", + "codes": [ + { + "description": "1+", + "code": "nq2211" + } + ], + "solutions": [] + }, + { + "title": "Задание 3", + "text": "Текст третьего задания", + "codes": [ + { + "description": "1+", + "code": "nq3311" + } + ], + "solutions": [] + }, + { + "title": "Задание 4", + "text": "Текст 4 задания", + "codes": [ + { + "description": "1+", + "code": "nq4411" + } + ], + "solutions": [] + }, + { + "title": "Задание 5", + "text": "Текст 5 задания", + "codes": [ + { + "description": "1+", + "code": "nq5511" + } + ], + "solutions": [] + }, + { + "title": "Задание 6", + "text": "Текст 6 задания", + "codes": [ + { + "description": "1+", + "code": "nq6611" + } + ], + "solutions": [] + }, + { + "title": "Задание 7", + "text": "Текст 7 задания", + "codes": [ + { + "description": "1+", + "code": "nq7711" + } + ], + "solutions": [] + }, + { + "title": "Задание 8", + "text": "Текст 8 задания", + "codes": [ + { + "description": "1+", + "code": "nq8811" + } + ], + "solutions": [] + } + ] +} ### -POST http://localhost:8000/api/engine/1/code +GET http://localhost:8000/api/engine/2 + +### + +POST http://localhost:8000/api/engine/2/code Content-Type: application/json { @@ -108,7 +212,7 @@ Content-Type: application/json } ### -POST http://localhost:8000/api/engine/1/code +POST http://localhost:8000/api/engine/2/code Content-Type: application/json { @@ -116,7 +220,7 @@ Content-Type: application/json } ### -POST http://localhost:8000/api/engine/1/code +POST http://localhost:8000/api/engine/2/code Content-Type: application/json { @@ -124,9 +228,9 @@ Content-Type: application/json } ### -POST http://localhost:8000/api/engine/1/code +POST http://localhost:8000/api/engine/2/code Content-Type: application/json { - "code": "NQ3322" + "code": "NQ3333" } \ No newline at end of file diff --git a/views/engine/view.gotmpl b/views/engine/view.gotmpl deleted file mode 100644 index 6ad8b6b..0000000 --- a/views/engine/view.gotmpl +++ /dev/null @@ -1,83 +0,0 @@ -{{ template "header" . }} - -{{ if .State }} -
Уровень
- - - - - - - - - - - - - - - -
- {{.State.Task.Title}} -
Выдано:{{ .State.CreatedAt.Format "15:04 02.01.2006" }}Автопереход:{{ .State.Deadline.Format "15:04 02.01.2006" }} (через {{ (.State.Deadline.Sub now) | toTime }})
{{ .State.Task.Text | markDown }}
- - - - - - - - - - -
- Ввод кода -
-
-
-
-
- -
- -
-
-
-
- -
История игры
- - - - - - - - - - - {{ range $i, $a := .History }} - - - - - - - {{ end }} - -
УровеньВремя началаВремя окончанияСтатус
{{ inc $i }}{{ $a.CreatedAt.Format "15:04 02.01.2006" }}{{ $a.Deadline.Format "15:04 02.01.2006" }} - {{ if eq $a.Status 0 }} - Текущее - {{ else if eq $a.Status 1 }} - Пройден - {{ else if eq $a.Status 2 }} - Снят - {{ else if eq $a.Status 3 }} - Автопереход - {{ end }} -
-{{ else }} -

Вам не предусмотренно следующее задание

-{{ end }} - -{{ template "footer" . }} \ No newline at end of file diff --git a/views/index.gotmpl b/views/index.gotmpl deleted file mode 100644 index 25590d3..0000000 --- a/views/index.gotmpl +++ /dev/null @@ -1,48 +0,0 @@ -{{ template "header" . }} -

Текущие игры

-{{ range .Games }} - - - - - - - - - - - - - - - - - -
- {{.Title}} -
- Начало - - {{ .StartAt.Format "15:04 02.01.2006" }} -
- {{ .Description | markDown }} - -
- Участвуют: - -
    - {{ range .Teams }} -
  • {{.Team.Name}}
  • - {{ else }} - Никто пока не подал заявку - {{ end }} -
-
-{{ else }} -

- Игр пока не анонсировано -

-{{ end }} -{{ template "footer" . }} \ No newline at end of file diff --git a/views/layout.gotmpl b/views/layout.gotmpl deleted file mode 100644 index 4f31ab0..0000000 --- a/views/layout.gotmpl +++ /dev/null @@ -1,67 +0,0 @@ -{{define "header"}} - - - - - - - nQuest - - - - - - - -
- {{end}} - -{{define "footer"}} -
- - - - -{{end}} \ No newline at end of file diff --git a/views/user/login.gotmpl b/views/user/login.gotmpl deleted file mode 100644 index 6d76dcd..0000000 --- a/views/user/login.gotmpl +++ /dev/null @@ -1,27 +0,0 @@ -{{ template "header" . }} -

Вход

-
-
- {{ if .Error }} - - {{ end }} -
- -
- -
-
-
- -
- -
-
-
-
- -
-
-
-
-{{ template "footer" . }} \ No newline at end of file diff --git a/views/user/register.gotmpl b/views/user/register.gotmpl deleted file mode 100644 index af1a783..0000000 --- a/views/user/register.gotmpl +++ /dev/null @@ -1,39 +0,0 @@ -{{ template "header" . }} -

Регистрация

-
-
- {{ if .Error }} - - {{ end }} -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
-
-
-{{ template "footer" . }} \ No newline at end of file