Почти рабочая админка

This commit is contained in:
Александр Кирюхин 2024-01-25 01:45:09 +03:00
parent 10a355d0f1
commit fac2df0bc1
11 changed files with 478 additions and 204 deletions

View file

@ -8,57 +8,49 @@ servers:
- url: /api - url: /api
paths: paths:
# User routes # User routes
/user: /user:
get: get:
responses: responses:
200: 200:
$ref: '#/components/responses/userResponse' $ref: "#/components/responses/userResponse"
403: 403:
$ref: '#/components/responses/errorResponse' $ref: "#/components/responses/errorResponse"
/user/login: /user/login:
post: post:
security: [] security: []
requestBody: requestBody:
$ref: '#/components/requestBodies/login' $ref: "#/components/requestBodies/login"
responses: responses:
200: 200:
$ref: '#/components/responses/userResponse' $ref: "#/components/responses/userResponse"
400: 400:
$ref: '#/components/responses/errorResponse' $ref: "#/components/responses/errorResponse"
/user/register: /user/register:
post: post:
security: [] security: []
requestBody: requestBody:
$ref: '#/components/requestBodies/register' $ref: "#/components/requestBodies/register"
responses: responses:
200: 200:
$ref: '#/components/responses/userResponse' $ref: "#/components/responses/userResponse"
400: 400:
$ref: '#/components/responses/errorResponse' $ref: "#/components/responses/errorResponse"
/user/logout: /user/logout:
post: post:
responses: responses:
204: 204:
description: "success logout" description: "success logout"
400: 400:
$ref: '#/components/responses/errorResponse' $ref: "#/components/responses/errorResponse"
# Game routes # Game routes
/games: /games:
get: get:
responses: responses:
200: 200:
$ref: '#/components/responses/gameListResponse' $ref: "#/components/responses/gameListResponse"
post:
operationId: createGame
security:
- cookieAuth: [creator, admin]
requestBody:
$ref: "#/components/requestBodies/gameEditRequest"
responses:
200:
$ref: "#/components/responses/gameResponse"
/engine/{uid}: /engine/{uid}:
get: get:
operationId: gameEngine operationId: gameEngine
@ -71,7 +63,7 @@ paths:
format: uuid format: uuid
responses: responses:
200: 200:
$ref: '#/components/responses/taskResponse' $ref: "#/components/responses/taskResponse"
/engine/{uid}/code: /engine/{uid}/code:
post: post:
operationId: enterCode operationId: enterCode
@ -86,7 +78,7 @@ paths:
$ref: "#/components/requestBodies/enterCodeRequest" $ref: "#/components/requestBodies/enterCodeRequest"
responses: responses:
200: 200:
$ref: '#/components/responses/taskResponse' $ref: "#/components/responses/taskResponse"
/file/upload: /file/upload:
post: post:
operationId: uploadFile operationId: uploadFile
@ -103,7 +95,7 @@ paths:
format: binary format: binary
responses: responses:
200: 200:
$ref: '#/components/responses/uploadResponse' $ref: "#/components/responses/uploadResponse"
/file/{uid}: /file/{uid}:
get: get:
operationId: getFile operationId: getFile
@ -118,11 +110,40 @@ paths:
200: 200:
description: file description: file
content: content:
'application/octet-stream': "application/octet-stream":
schema: schema:
type: string type: string
format: binary format: binary
/games/{uid}: /admin/games:
get:
operationId: listGamesByAdmin
responses:
200:
$ref: "#/components/responses/gameListResponse"
post:
operationId: createGame
security:
- cookieAuth: [creator, admin]
requestBody:
$ref: "#/components/requestBodies/gameEditRequest"
responses:
200:
$ref: "#/components/responses/gameAdminResponse"
/admin/games/{uid}:
get:
operationId: getGameByAdmin
parameters:
- name: uid
in: path
required: true
schema:
type: string
format: uuid
security:
- cookieAuth: [creator, admin]
responses:
200:
$ref: "#/components/responses/gameAdminResponse"
post: post:
operationId: editGame operationId: editGame
parameters: parameters:
@ -138,7 +159,7 @@ paths:
$ref: "#/components/requestBodies/gameEditRequest" $ref: "#/components/requestBodies/gameEditRequest"
responses: responses:
200: 200:
$ref: "#/components/responses/gameResponse" $ref: "#/components/responses/gameAdminResponse"
components: components:
schemas: schemas:
userView: userView:
@ -232,7 +253,7 @@ components:
codes: codes:
type: array type: array
items: items:
$ref: '#/components/schemas/codeView' $ref: "#/components/schemas/codeView"
# solutions: # solutions:
# type: array # type: array
# items: # items:
@ -263,6 +284,11 @@ components:
gameEdit: gameEdit:
type: object type: object
properties: properties:
id:
type: string
format: uuid
visible:
type: boolean
title: title:
type: string type: string
description: description:
@ -279,6 +305,7 @@ components:
type: string type: string
format: uuid format: uuid
required: required:
- visible
- title - title
- description - description
- type - type
@ -288,6 +315,9 @@ components:
taskEdit: taskEdit:
type: object type: object
properties: properties:
id:
type: string
format: uuid
title: title:
type: string type: string
text: text:
@ -295,7 +325,7 @@ components:
codes: codes:
type: array type: array
items: items:
$ref: '#/components/schemas/codeEdit' $ref: "#/components/schemas/codeEdit"
# solutions: # solutions:
# type: array # type: array
# items: # items:
@ -308,6 +338,9 @@ components:
codeEdit: codeEdit:
type: object type: object
properties: properties:
id:
type: string
format: uuid
description: description:
type: string type: string
code: code:
@ -334,7 +367,7 @@ components:
login: login:
required: true required: true
content: content:
'application/json': "application/json":
schema: schema:
type: object type: object
properties: properties:
@ -342,11 +375,11 @@ components:
type: string type: string
password: password:
type: string type: string
required: [ email, password ] required: [email, password]
register: register:
required: true required: true
content: content:
'application/json': "application/json":
schema: schema:
type: object type: object
properties: properties:
@ -358,17 +391,17 @@ components:
type: string type: string
password2: password2:
type: string type: string
required: [ username, email, password, password2 ] required: [username, email, password, password2]
gameEditRequest: gameEditRequest:
required: true required: true
content: content:
'application/json': "application/json":
schema: schema:
$ref: '#/components/schemas/gameEdit' $ref: "#/components/schemas/gameEdit"
enterCodeRequest: enterCodeRequest:
required: true required: true
content: content:
'application/json': "application/json":
schema: schema:
type: object type: object
properties: properties:
@ -378,15 +411,15 @@ components:
- code - code
responses: responses:
userResponse: userResponse:
description: '' description: ""
content: content:
'application/json': "application/json":
schema: schema:
$ref: "#/components/schemas/userView" $ref: "#/components/schemas/userView"
errorResponse: errorResponse:
description: '' description: ""
content: content:
'application/json': "application/json":
schema: schema:
type: object type: object
properties: properties:
@ -394,31 +427,37 @@ components:
type: integer type: integer
message: message:
type: string type: string
required: [ code, message ] required: [code, message]
gameListResponse: gameListResponse:
description: '' description: ""
content: content:
'application/json': "application/json":
schema: schema:
type: array type: array
items: items:
$ref: "#/components/schemas/gameView" $ref: "#/components/schemas/gameView"
gameResponse: gameResponse:
description: '' description: ""
content: content:
'application/json': "application/json":
schema: schema:
$ref: "#/components/schemas/gameView" $ref: "#/components/schemas/gameView"
taskResponse: gameAdminResponse:
description: '' description: ""
content: content:
'application/json': "application/json":
schema:
$ref: "#/components/schemas/gameEdit"
taskResponse:
description: ""
content:
"application/json":
schema: schema:
$ref: "#/components/schemas/taskView" $ref: "#/components/schemas/taskView"
uploadResponse: uploadResponse:
description: '' description: ""
content: content:
'application/json': "application/json":
schema: schema:
type: object type: object
properties: properties:

View file

@ -22,6 +22,18 @@ import (
// ServerInterface represents all server handlers. // ServerInterface represents all server handlers.
type ServerInterface interface { type ServerInterface interface {
// (GET /admin/games)
ListGamesByAdmin(ctx echo.Context) error
// (POST /admin/games)
CreateGame(ctx echo.Context) error
// (GET /admin/games/{uid})
GetGameByAdmin(ctx echo.Context, uid openapi_types.UUID) error
// (POST /admin/games/{uid})
EditGame(ctx echo.Context, uid openapi_types.UUID) error
// (GET /engine/{uid}) // (GET /engine/{uid})
GameEngine(ctx echo.Context, uid openapi_types.UUID) error GameEngine(ctx echo.Context, uid openapi_types.UUID) error
@ -37,12 +49,6 @@ type ServerInterface interface {
// (GET /games) // (GET /games)
GetGames(ctx echo.Context) error GetGames(ctx echo.Context) error
// (POST /games)
CreateGame(ctx echo.Context) error
// (POST /games/{uid})
EditGame(ctx echo.Context, uid openapi_types.UUID) error
// (GET /user) // (GET /user)
GetUser(ctx echo.Context) error GetUser(ctx echo.Context) error
@ -61,6 +67,64 @@ type ServerInterfaceWrapper struct {
Handler ServerInterface Handler ServerInterface
} }
// ListGamesByAdmin converts echo context to params.
func (w *ServerInterfaceWrapper) ListGamesByAdmin(ctx echo.Context) error {
var err error
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.ListGamesByAdmin(ctx)
return err
}
// CreateGame converts echo context to params.
func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error {
var err error
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.CreateGame(ctx)
return err
}
// GetGameByAdmin converts echo context to params.
func (w *ServerInterfaceWrapper) GetGameByAdmin(ctx echo.Context) error {
var err error
// ------------- Path parameter "uid" -------------
var uid openapi_types.UUID
err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter uid: %s", err))
}
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetGameByAdmin(ctx, uid)
return err
}
// EditGame converts echo context to params.
func (w *ServerInterfaceWrapper) EditGame(ctx echo.Context) error {
var err error
// ------------- Path parameter "uid" -------------
var uid openapi_types.UUID
err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter uid: %s", err))
}
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.EditGame(ctx, uid)
return err
}
// GameEngine converts echo context to params. // GameEngine converts echo context to params.
func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error { func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error {
var err error var err error
@ -137,35 +201,6 @@ func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
return err return err
} }
// CreateGame converts echo context to params.
func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error {
var err error
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.CreateGame(ctx)
return err
}
// EditGame converts echo context to params.
func (w *ServerInterfaceWrapper) EditGame(ctx echo.Context) error {
var err error
// ------------- Path parameter "uid" -------------
var uid openapi_types.UUID
err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter uid: %s", err))
}
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.EditGame(ctx, uid)
return err
}
// GetUser converts echo context to params. // GetUser converts echo context to params.
func (w *ServerInterfaceWrapper) GetUser(ctx echo.Context) error { func (w *ServerInterfaceWrapper) GetUser(ctx echo.Context) error {
var err error var err error
@ -234,13 +269,15 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
Handler: si, Handler: si,
} }
router.GET(baseURL+"/admin/games", wrapper.ListGamesByAdmin)
router.POST(baseURL+"/admin/games", wrapper.CreateGame)
router.GET(baseURL+"/admin/games/:uid", wrapper.GetGameByAdmin)
router.POST(baseURL+"/admin/games/:uid", wrapper.EditGame)
router.GET(baseURL+"/engine/:uid", wrapper.GameEngine) router.GET(baseURL+"/engine/:uid", wrapper.GameEngine)
router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode) router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode)
router.POST(baseURL+"/file/upload", wrapper.UploadFile) router.POST(baseURL+"/file/upload", wrapper.UploadFile)
router.GET(baseURL+"/file/:uid", wrapper.GetFile) router.GET(baseURL+"/file/:uid", wrapper.GetFile)
router.GET(baseURL+"/games", wrapper.GetGames) router.GET(baseURL+"/games", wrapper.GetGames)
router.POST(baseURL+"/games", wrapper.CreateGame)
router.POST(baseURL+"/games/:uid", wrapper.EditGame)
router.GET(baseURL+"/user", wrapper.GetUser) router.GET(baseURL+"/user", wrapper.GetUser)
router.POST(baseURL+"/user/login", wrapper.PostUserLogin) router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
router.POST(baseURL+"/user/logout", wrapper.PostUserLogout) router.POST(baseURL+"/user/logout", wrapper.PostUserLogout)
@ -251,26 +288,27 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
// Base64 encoded, gzipped, json marshaled Swagger object // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"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",
"Ymo063X9bwAAAP//ZDhhHgMaAAA=", "NBhuMVPEMRjzQ3X0UotbG7s/iRy38rKWXBChRsvbCdLR2relqmJAP9Z1q9Api1hI3X+5Lf8NAAD//0cV",
"3sY5HAAA",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

View file

@ -37,6 +37,7 @@ const (
type CodeEdit struct { type CodeEdit struct {
Code string `json:"code"` Code string `json:"code"`
Description string `json:"description"` Description string `json:"description"`
Id *openapi_types.UUID `json:"id,omitempty"`
} }
// CodeView defines model for codeView. // CodeView defines model for codeView.
@ -49,10 +50,12 @@ type CodeView struct {
type GameEdit struct { type GameEdit struct {
Description string `json:"description"` Description string `json:"description"`
Icon openapi_types.UUID `json:"icon"` Icon openapi_types.UUID `json:"icon"`
Id *openapi_types.UUID `json:"id,omitempty"`
Points int `json:"points"` Points int `json:"points"`
Tasks []TaskEdit `json:"tasks"` Tasks []TaskEdit `json:"tasks"`
Title string `json:"title"` Title string `json:"title"`
Type GameType `json:"type"` Type GameType `json:"type"`
Visible bool `json:"visible"`
} }
// GameType defines model for gameType. // GameType defines model for gameType.
@ -74,6 +77,7 @@ type GameView struct {
// TaskEdit defines model for taskEdit. // TaskEdit defines model for taskEdit.
type TaskEdit struct { type TaskEdit struct {
Codes []CodeEdit `json:"codes"` Codes []CodeEdit `json:"codes"`
Id *openapi_types.UUID `json:"id,omitempty"`
Text string `json:"text"` Text string `json:"text"`
Title string `json:"title"` Title string `json:"title"`
} }
@ -111,12 +115,12 @@ type ErrorResponse struct {
Message string `json:"message"` Message string `json:"message"`
} }
// GameAdminResponse defines model for gameAdminResponse.
type GameAdminResponse = GameEdit
// GameListResponse defines model for gameListResponse. // GameListResponse defines model for gameListResponse.
type GameListResponse = []GameView type GameListResponse = []GameView
// GameResponse defines model for gameResponse.
type GameResponse = GameView
// TaskResponse defines model for taskResponse. // TaskResponse defines model for taskResponse.
type TaskResponse = TaskView type TaskResponse = TaskView
@ -172,18 +176,18 @@ type PostUserRegisterJSONBody struct {
Username string `json:"username"` Username string `json:"username"`
} }
// EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType.
type EnterCodeJSONRequestBody EnterCodeJSONBody
// UploadFileMultipartRequestBody defines body for UploadFile for multipart/form-data ContentType.
type UploadFileMultipartRequestBody UploadFileMultipartBody
// CreateGameJSONRequestBody defines body for CreateGame for application/json ContentType. // CreateGameJSONRequestBody defines body for CreateGame for application/json ContentType.
type CreateGameJSONRequestBody = GameEdit type CreateGameJSONRequestBody = GameEdit
// EditGameJSONRequestBody defines body for EditGame for application/json ContentType. // EditGameJSONRequestBody defines body for EditGame for application/json ContentType.
type EditGameJSONRequestBody = GameEdit type EditGameJSONRequestBody = GameEdit
// EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType.
type EnterCodeJSONRequestBody EnterCodeJSONBody
// UploadFileMultipartRequestBody defines body for UploadFile for multipart/form-data ContentType.
type UploadFileMultipartRequestBody UploadFileMultipartBody
// PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType. // PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType.
type PostUserLoginJSONRequestBody PostUserLoginJSONBody type PostUserLoginJSONRequestBody PostUserLoginJSONBody

View file

@ -14,7 +14,8 @@ import Engine from './pages/Engine'
import Quests from './pages/Quests' import Quests from './pages/Quests'
import User from './pages/User' import User from './pages/User'
import { useRole } from './utils/roles' import { useRole } from './utils/roles'
import Quest from './pages/admin/Quest' import EditQuest from './pages/admin/Quest'
import AdminQuest from './pages/admin/Quests'
const router = createBrowserRouter( const router = createBrowserRouter(
createRoutesFromElements( createRoutesFromElements(
@ -43,14 +44,19 @@ const router = createBrowserRouter(
loader={({ params }) => ajax(`/api/engine/${params.gameId}`).catch(x => { console.log(x); return null })} loader={({ params }) => ajax(`/api/engine/${params.gameId}`).catch(x => { console.log(x); return null })}
/> />
<Route <Route
path="admin" path="/admin/quests/new"
element={<Auth role="admin"><NoMatch /></Auth>} element={<Auth role="creator"><EditQuest /></Auth>}
// loader={() => ajax(`/api/admin/games`)} // loader={() => ajax(`/api/admin/games`)}
/> />
<Route <Route
path="quest/new" path="/admin/quests/:gameId"
element={<Auth role="creator"><Quest /></Auth>} element={<Auth role="creator"><EditQuest /></Auth>}
// loader={() => ajax(`/api/admin/games`)} loader={({ params }) => ajax(`/api/admin/games/${params.gameId}`)}
/>
<Route
path="/admin/quests"
element={<Auth role="creator"><AdminQuest /></Auth>}
loader={() => ajax('/api/admin/games')}
/> />
<Route path="*" element={<NoMatch />} /> <Route path="*" element={<NoMatch />} />

View file

@ -49,9 +49,9 @@ const AppLayout = () => {
if (hasRole('creator')) { if (hasRole('creator')) {
items.push({ items.push({
key: 'quest/new', key: 'admin/quests',
label: 'Создать квест', label: 'Управление квестами',
link: '/quest/new' link: '/admin/quests'
}) })
} }

View file

@ -12,6 +12,9 @@ const Engine = () => {
const { message } = App.useApp() const { message } = App.useApp()
useEffect(() => { useEffect(() => {
if (!task) {
return
}
switch (task.message) { switch (task.message) {
case 'invalid_code': case 'invalid_code':
message.error('Неверный код') message.error('Неверный код')
@ -26,7 +29,7 @@ const Engine = () => {
message.success('Код принят, ищите оставшиеся') message.success('Код принят, ищите оставшиеся')
break break
} }
}, [task.message]) }, [task])
const [form] = Form.useForm() const [form] = Form.useForm()
const onFinish = ({ code }) => { const onFinish = ({ code }) => {

View file

@ -16,7 +16,7 @@ const Quests = () => {
return (<> return (<>
<Title>Квесты</Title> <Title>Квесты</Title>
{games.map(item => renderItem(user, navigate, item))} {games.map(item => renderItem(user, navigate, item))}
{!games ? (<strong>Квестов пока не анонсировано</strong>) : null} {games.length === 0 ? (<strong>Квестов пока не анонсировано</strong>) : null}
</>) </>)
} }

View file

@ -1,5 +1,5 @@
import { useLoaderData, useNavigate } from 'react-router-dom' import { useLoaderData, useNavigate } from 'react-router-dom'
import { Alert, Button, Card, Form, Input, InputNumber, Popconfirm, Radio, Typography, Upload } from 'antd' import { Alert, Avatar, Button, Card, Form, Input, InputNumber, Popconfirm, Radio, Switch, Typography, Upload } from 'antd'
import { UploadOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons' import { UploadOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons'
import { ajax } from '../../utils/fetch' import { ajax } from '../../utils/fetch'
import { useState } from 'react' import { useState } from 'react'
@ -30,7 +30,11 @@ const Quest = () => {
} }
const onFinish = (values) => { const onFinish = (values) => {
ajax('/api/games', { let url = '/api/admin/games'
if (quest.id) {
url = `/api/admin/games/${quest.id}`
}
ajax(url, {
method: 'POST', method: 'POST',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
@ -38,7 +42,7 @@ const Quest = () => {
}, },
body: JSON.stringify(values) body: JSON.stringify(values)
}) })
.then(g => navigate(`/quest/${g.id}/edit`)) .then(g => navigate(`/admin/quests/${g.id}/`))
.catch(({ message }) => setError('Ошибка создания')) .catch(({ message }) => setError('Ошибка создания'))
} }
@ -64,6 +68,9 @@ const Quest = () => {
Сохранить квест Сохранить квест
</Button> </Button>
</Form.Item> </Form.Item>
<Form.Item label='Опубликован?' name='visible'>
<Switch />
</Form.Item>
<Form.Item label='Название' name='title'> <Form.Item label='Название' name='title'>
<Input /> <Input />
</Form.Item> </Form.Item>
@ -75,6 +82,7 @@ const Quest = () => {
label='Иконка' label='Иконка'
getValueFromEvent={normFile} getValueFromEvent={normFile}
> >
{quest.icon ? <Avatar src={`/api/file/${quest.icon}`} /> : null}
<Upload name='file' action='/api/file/upload' listType='picture' maxCount={1}> <Upload name='file' action='/api/file/upload' listType='picture' maxCount={1}>
<Button icon={<UploadOutlined />}>Загрузка</Button> <Button icon={<UploadOutlined />}>Загрузка</Button>
</Upload> </Upload>
@ -108,7 +116,7 @@ const Quest = () => {
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
const renderTaskForm = remove => task => ( const renderTaskForm = remove => task => (
<Card <Card
key={task.key} key={task.id}
title={`Уровень ${task.key}`} title={`Уровень ${task.key}`}
style={{ marginBottom: 8 }} style={{ marginBottom: 8 }}
actions={[ actions={[
@ -125,6 +133,9 @@ const renderTaskForm = remove => task => (
</Popconfirm> </Popconfirm>
]} ]}
> >
<Form.Item name={[task.name, 'id']} hidden>
<Input />
</Form.Item>
<Form.Item name={[task.name, 'title']} label='Название уровня' help='ВИДНО игрокам'> <Form.Item name={[task.name, 'title']} label='Название уровня' help='ВИДНО игрокам'>
<Input /> <Input />
</Form.Item> </Form.Item>
@ -152,19 +163,22 @@ const renderCodeForm = remove => code => (
key={code.key} key={code.key}
style={{ marginBottom: 8 }} style={{ marginBottom: 8 }}
actions={[ actions={[
<Popconfirm // <Popconfirm
key='delete' // key='delete'
title='Удалить код?' // title='Удалить код?'
onConfirm={() => remove(code.name)} // onConfirm={() => remove(code.name)}
okText='Да' // okText='Да'
cancelText='Нет' // cancelText='Нет'
> // >
<Button danger> <Button key="delete" danger onClick={() => remove(code.name)}>
<CloseOutlined/> Удалить код <CloseOutlined/> Удалить код
</Button> </Button>
</Popconfirm> // </Popconfirm>
]} ]}
> >
<Form.Item name={[code.name, 'id']} hidden>
<Input />
</Form.Item>
<Form.Item name={[code.name, 'code']} label='Код'> <Form.Item name={[code.name, 'code']} label='Код'>
<Input /> <Input />
</Form.Item> </Form.Item>

View file

@ -0,0 +1,40 @@
import { Table, Typography } from 'antd'
import { Link, useLoaderData } from 'react-router-dom'
const { Title } = Typography
const Quests = () => {
const quests = useLoaderData()
return (
<>
<Title>Управление своими квестами</Title>
<Link to="/admin/quests/new">Создать новый квест</Link>
<Table
dataSource={quests}
columns={[
{
title: 'UUID',
dataIndex: 'id',
key: 'id',
render: uid => <Link to={`/admin/quests/${uid}`}>${uid}</Link>
},
{
title: 'Название',
dataIndex: 'title',
key: 'title'
},
{
title: 'Тип',
dataIndex: 'type',
key: 'type',
render: type => type === 'virtual' ? 'Виртуальный' : 'Полевой'
}
]}
/>
</>
)
}
export default Quests

View file

@ -32,16 +32,32 @@ func (a *Admin) CreateGame(ctx echo.Context) error {
return err return err
} }
return ctx.JSON(http.StatusCreated, api.GameResponse{ tasks := make([]api.TaskEdit, 0, len(game.Tasks))
Authors: make([]api.UserView, 0, len(game.Authors)),
CreatedAt: game.CreatedAt.Format(time.RFC3339), for _, t := range game.Tasks {
codes := make([]api.CodeEdit, 0, len(t.Codes))
for _, c := range t.Codes {
codes = append(codes, api.CodeEdit{
Code: c.Code,
Description: c.Description,
})
}
tasks = append(tasks, api.TaskEdit{
Codes: codes,
Text: t.Text,
Title: t.Title,
})
}
return ctx.JSON(http.StatusOK, api.GameAdminResponse{
Description: game.Description, Description: game.Description,
Icon: game.IconID, Icon: game.IconID,
Id: game.ID, Id: &game.ID,
Points: game.Points, Points: game.Points,
TaskCount: len(game.Tasks), Tasks: tasks,
Title: game.Title, Title: game.Title,
Type: api.MapGameTypeReverse(game.Type), Type: api.MapGameTypeReverse(game.Type),
Visible: game.Visible,
}) })
} }
@ -49,37 +65,149 @@ func (a *Admin) CreateGame(ctx echo.Context) error {
func (a *Admin) EditGame(ctx echo.Context, uid uuid.UUID) error { func (a *Admin) EditGame(ctx echo.Context, uid uuid.UUID) error {
user := contextlib.GetUser(ctx) user := contextlib.GetUser(ctx)
req := &api.GameEditRequest{} req := &api.GameEditRequest{}
if err := ctx.Bind(req); err != nil {
game, err := a.GameService.GetByID(ctx.Request().Context(), uid)
if err != nil {
return echo.ErrNotFound
}
if user.Role != models.RoleAdmin {
isAuthor := false
for _, u := range game.Authors {
if u.ID == user.ID {
isAuthor = true
break
}
}
if !isAuthor {
return echo.ErrForbidden
}
}
if err = ctx.Bind(req); err != nil {
return err return err
} }
game = a.mapCreateGameRequest(req, user)
game := a.mapCreateGameRequest(req, user)
var err error
game, err = a.GameService.UpdateGame(ctx.Request().Context(), uid, game) game, err = a.GameService.UpdateGame(ctx.Request().Context(), uid, game)
if err != nil { if err != nil {
return err return err
} }
return ctx.JSON(http.StatusCreated, api.GameResponse{ tasks := make([]api.TaskEdit, 0, len(game.Tasks))
Authors: make([]api.UserView, 0, len(game.Authors)),
CreatedAt: game.CreatedAt.Format(time.RFC3339), for _, t := range game.Tasks {
t := t
codes := make([]api.CodeEdit, 0, len(t.Codes))
for _, c := range t.Codes {
codes = append(codes, api.CodeEdit{
Id: &c.ID,
Code: c.Code,
Description: c.Description,
})
}
tasks = append(tasks, api.TaskEdit{
Id: &t.ID,
Codes: codes,
Text: t.Text,
Title: t.Title,
})
}
return ctx.JSON(http.StatusOK, api.GameAdminResponse{
Description: game.Description, Description: game.Description,
Icon: game.IconID, Icon: game.IconID,
Id: game.ID, Id: &game.ID,
Points: game.Points, Points: game.Points,
TaskCount: len(game.Tasks), Tasks: tasks,
Title: game.Title, Title: game.Title,
Type: api.MapGameTypeReverse(game.Type), Type: api.MapGameTypeReverse(game.Type),
Visible: game.Visible,
}) })
} }
// (GET /games/{uid})
func (a *Admin) GetGameByAdmin(ctx echo.Context, uid uuid.UUID) error {
user := contextlib.GetUser(ctx)
game, err := a.GameService.GetByID(ctx.Request().Context(), uid)
if err != nil {
return echo.ErrNotFound
}
if user.Role != models.RoleAdmin {
isAuthor := false
for _, u := range game.Authors {
if u.ID == user.ID {
isAuthor = true
break
}
}
if !isAuthor {
return echo.ErrForbidden
}
}
tasks := make([]api.TaskEdit, 0, len(game.Tasks))
for _, t := range game.Tasks {
t := t
codes := make([]api.CodeEdit, 0, len(t.Codes))
for _, c := range t.Codes {
codes = append(codes, api.CodeEdit{
Id: &c.ID,
Code: c.Code,
Description: c.Description,
})
}
tasks = append(tasks, api.TaskEdit{
Id: &t.ID,
Codes: codes,
Text: t.Text,
Title: t.Title,
})
}
return ctx.JSON(http.StatusOK, api.GameAdminResponse{
Description: game.Description,
Icon: game.IconID,
Id: &game.ID,
Points: game.Points,
Tasks: tasks,
Title: game.Title,
Type: api.MapGameTypeReverse(game.Type),
Visible: game.Visible,
})
}
func (a *Admin) ListGamesByAdmin(ctx echo.Context) error {
user := contextlib.GetUser(ctx)
games, err := a.GameService.ListByAuthor(ctx.Request().Context(), user)
if err != nil {
return echo.ErrNotFound
}
resp := make(api.GameListResponse, 0, len(games))
for _, game := range games {
gv := api.GameView{
Id: game.ID,
Title: game.Title,
Type: api.MapGameTypeReverse(game.Type),
Points: game.Points,
TaskCount: len(game.Tasks),
CreatedAt: game.CreatedAt.Format(time.RFC3339),
Icon: game.IconID,
}
resp = append(resp, gv)
}
return ctx.JSON(http.StatusOK, resp)
}
func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) *models.Game { func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) *models.Game {
game := &models.Game{ game := &models.Game{
Model: models.Model{ Model: models.Model{
ID: uuid.New(), ID: uuid.New(),
}, },
Visible: false, Visible: req.Visible,
Title: req.Title, Title: req.Title,
Description: req.Description, Description: req.Description,
Authors: []*models.User{ Authors: []*models.User{
@ -91,30 +219,28 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User)
IconID: req.Icon, IconID: req.Icon,
} }
for order, te := range req.Tasks { for order, te := range req.Tasks {
if te.Id == nil {
u := uuid.New()
te.Id = &u
}
task := &models.Task{ task := &models.Task{
Model: models.Model{ Model: models.Model{
ID: uuid.New(), ID: *te.Id,
}, },
Title: te.Title, Title: te.Title,
Text: te.Text, Text: te.Text,
MaxTime: 0,
// Solutions: make([]*models.Solution, 0, len(te.Solutions)),
Codes: make([]*models.Code, 0, len(te.Codes)), Codes: make([]*models.Code, 0, len(te.Codes)),
TaskOrder: uint(order), 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 { for _, ce := range te.Codes {
if ce.Id == nil {
u := uuid.New()
ce.Id = &u
}
task.Codes = append(task.Codes, &models.Code{ task.Codes = append(task.Codes, &models.Code{
Model: models.Model{ Model: models.Model{
ID: uuid.New(), ID: *ce.Id,
}, },
Code: ce.Code, Code: ce.Code,
Description: ce.Description, Description: ce.Description,

View file

@ -41,8 +41,7 @@ func (gs *Game) List(ctx context.Context) ([]*models.Game, error) {
Order("created_at DESC"). Order("created_at DESC").
Preload("Tasks"). Preload("Tasks").
Preload("Authors"). Preload("Authors").
Find(&games). Find(&games, "visible = true").
Limit(20).
Error Error
} }
@ -54,14 +53,17 @@ func (gs *Game) GetTaskID(ctx context.Context, id uuid.UUID) (*models.Task, erro
func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*models.Game, error) { func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*models.Game, error) {
games := make([]*models.Game, 0) games := make([]*models.Game, 0)
model := gs.DB.
return games, gs.DB.
WithContext(ctx). WithContext(ctx).
Model(&models.Game{}). Model(&models.Game{})
Preload("Authors", gs.DB.Where("id = ?", author.ID)). if author.Role == models.RoleCreator {
model.Preload("Authors", gs.DB.Where("id = ?", author.ID))
}
return games,
model.
Order("created_at DESC"). Order("created_at DESC").
Find(&games). Find(&games).
Limit(20).
Error Error
} }
@ -75,8 +77,10 @@ func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game
func (gs *Game) UpdateGame(ctx context.Context, uid uuid.UUID, game *models.Game) (*models.Game, error) { func (gs *Game) UpdateGame(ctx context.Context, uid uuid.UUID, game *models.Game) (*models.Game, error) {
game.ID = uid game.ID = uid
return game, gs.DB. db := gs.DB
Session(&gorm.Session{FullSaveAssociations: true}).
return game, db.Debug().
Session(&gorm.Session{FullSaveAssociations: true}).Omit("created_at").
Save(game). Save(game).
Error Error
} }