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

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

View file

@ -22,6 +22,18 @@ import (
// ServerInterface represents all server handlers.
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})
GameEngine(ctx echo.Context, uid openapi_types.UUID) error
@ -37,12 +49,6 @@ type ServerInterface interface {
// (GET /games)
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)
GetUser(ctx echo.Context) error
@ -61,6 +67,64 @@ type ServerInterfaceWrapper struct {
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.
func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error {
var err error
@ -137,35 +201,6 @@ func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
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.
func (w *ServerInterfaceWrapper) GetUser(ctx echo.Context) error {
var err error
@ -234,13 +269,15 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
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.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode)
router.POST(baseURL+"/file/upload", wrapper.UploadFile)
router.GET(baseURL+"/file/:uid", wrapper.GetFile)
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.POST(baseURL+"/user/login", wrapper.PostUserLogin)
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
var swaggerSpec = []string{
"H4sIAAAAAAAC/8xYTW/jNhD9KwXbozbyfpx02wbboGhQtGm2l8AIuNLE4UYiVXKUjRHovxdDUl8WZcle",
"N80pDjki37yZNxzymaWqKJUEiYYlz0zDPxUY/FllAuwASAR9rjK4cjM0liqJIO1PXpa5SDkKJeOvRkka",
"M+k9FJx+lVqVoNEvlaoM6C9uS2AJM6iF3LC6juyuQkPGkhtntY4aK/XlK6TI6qEZ6grqiG14AZ8ygcdg",
"+0nDHUvYj3FHQOxmTdysO7FtrjZCfgcRUHCRB5iIWMmN+aZ0Nk+TW6P3xULKNGyEQdAvDb+bfBecrQxo",
"yYsFCdJaRmMS+rssIsSOmFJJ433TWukrP3K6XBcSYQOaHC3AGL5ZKoTOPuxOBibVoiRMLGHMa+JSGDzK",
"CYFQmCXq+FvAN9rNQ+Ja8+0+REehWQYivCly83DyTWnRfZtWZa54doL0qSphRXSndMGRJW4gmlMGGS3N",
"E5LRyQmiRacJqiO/TCsRW2SXHhQ7C87pp28cTR0rbsJCfgEYof3bs2a0//5tIiZSNzGTJBErlfDH+7gc",
"UUqbxdIna3cw7ko/YigwD/PlBub1fE12u/y5ZaOdcNolG/Sth56TKZqvPQ6QVUFLPwqNFacDJBW47X3W",
"QW/LzCg6vMJ7pZdT10ljTF2qgSNkH/GIdDsgDxbVlPl0OVeVxInp/yYHHM59idAmQAewT2vUhmtPirTJ",
"HawEyyPdFraQSOApHOQp5ibUYNeJPLApZ6bL2mHOTKVtr5VpJKUebn3fIuQjz0XW/Kvy9qeEJ7zN4RFI",
"eRTzW9o3B4SgBF+MslahB3S88FReq/NKa5B4aV0K6sKa/Q5PMzagBch0onEkpswJmrTFdSCfxqpVPgg7",
"UdcITtEvnhVCBsO5vNG3qALdfo+oBmQoECPWGwo9/ECjFDEDaaUFbv8iGhu9qAcBHyu8t+RTK+OGKJWt",
"I8yAMb1ylDBeit/AN8RC3inrrEtXJv+0d9WIPYI2rjV6e7Y6WxE5qgTJS8ES9v7s7dnK3mfw3sKIQW6E",
"hPi5EllNAxuwsqA8ta3arxlL2AV1E9bQfqt5AQh0St146LReB9xFfngninq93lz/ud65Pr1braYSs7WL",
"B615bTkaOBc3jVepTMDFT82LxMt52LyJbKed6z2bxKM3k/pUNN2JHGJ3y5gm6LOd/0XYqrcDvtflF1WO",
"ouQaY+LgTcYxcBGhDQc0fRGS623wKhK8bR/q9c4Vqq9JG+K+Gm/GBadedzzNKAXQU/S/yWTiyqVSBHxj",
"UAMvhlev+SiMbl02gD572hNkipCLpj4eHLbRq4PdMZyf57Ytu3BF/WBx7b751cfC/d4cs2x2STZRrDKB",
"3tPXWateDZ22hdiTm59di3FERek/dNQR+7B6P//R8B3Q64dWitvH33DA/1DGQr20ZkcExK1fn8bT1eGe",
"DoK3HvitKlzkONmN8H8YPWkwU6UpGPODX/pYxB3G/uP2fpRXjeUREWp3eT1B2iu5NRUGA/qxKT2VzlnC",
"Ymo063X9bwAAAP//ZDhhHgMaAAA=",
"H4sIAAAAAAAC/9RZzW7kNgx+lULt0RvP/px8ywZtUDQo2jTbSzAIFJuZaGNLrkRnMwj87gUl/47lseMa",
"2fQUR6JF8uNHmuI8s1hluZIg0bDomWn4pwCDn1UiwC6ARNBnKoFLt0NrsZII0j7yPE9FzFEoGX41StKa",
"ie8h4/SUa5WDxuqoWCVAf3GfA4uYQS3kjpVlYLUKDQmLrp3UNqil1O1XiJGVfTHUBZQB2/EMfk4ELrHt",
"Jw13LGI/hi0Aods1YX3uiNpU7YT8D0BAxkXqQSJgOTfmm9LJNEzujM4bMyHTsBMGQb+2+e3mB+9uYUBL",
"ns0gSCMZDEHoapkFiF0xuZKm8k1rpS+rlfW4LiTCDjQ5moExfDc3EVp5vzsJmFiLnGxiEWNVTpwmmZCL",
"vJifFX7NF8LgIsUCITNzLPhbwDfSVoHBteb7MYuQm4fVYaBDnRF+pUWeKp6swKGiEDaT7pTOOLLILQRT",
"6UFCc8lCubQ6QHToOEBlUB3T5Inl1NyvxcGBnv0loHXPDMY+QW7DeraWtUfM8OlvMnCgfxKU2G1MwDIT",
"vYDlSlQdw7DCUYKY2TlN0q6qHOZ0wFBg6ofVLUxXiyuSKwP2KIy47Z11q1QKXA5iUEvW2oMDctjXaycb",
"ICqEx4J2VZkLssicEo0Fp09XLHDfea31sKl1g1jzAu+Vno9wm49DhGMNHCE5xWWp9sqsOlOFxJHtlahy",
"wAZn5zEiNARoDezCGjThOkKRJge8dWV+pJtq6on0zBAgPPm5MAbwAWA1VvacoLJ/zOfxWvoyn8fY3em1",
"6sxTDzdVYyXkI09FUv+r0uZRwhPepPAIlKBEjRvSmwKCN1NfDbImkV/QksNTfqXOCq1B4oV1yZs+Vux3",
"eJqQAS1AxiOdLSFlVujlZnM1HbdVq7QXdoKuzktFT5xaZG84599ErFWe60gHqNpIXyAGqNcQVuZ7mriA",
"GYgLLXD/F8FY54t6EHBa4L0Fn9ost0RUto4wA8Z0qlbEeC5+g6pvFvJOWWcdXZn8016mA/YI2ri27f3J",
"5mRD4KgcJM8Fi9jHk/cnG3vhwntrRmghDRsW7MBmBdHUdpG/JixidD84J4nPe3tJYQf3rw+bzRhxGrlw",
"cNGwbuTKeBSe2Up87gLUTjf241o6A5DwcMBQLjW3fyPrBpJF1/0QXg9ZWm7pjS7A4XMhknIU5nOwKLcg",
"51zzDBCobbiuSEKRaynicqx/PQ46Hf9UQ739btCMhZ4CVwX+ldz/X7ML5E5ImCIWmWwF3y6perf+shw4",
"F9aXtRHa1BPPN8ubwUy2XAumO5FC6AYY4wB9sfu/iNRTUjsDhKxIUeRcY0gYvEs4emYcpLAH062QXO+9",
"F3bvNO+lXh9MZxbmisVpsgRXEH23NBmZ5qgYAd8Z1MCz/lRnOgqDgY4NYMWe45/+6ptkVvvkk0rb1x3R",
"+MX1fQt40p2MlQH7tPk4/VJ/etwxMWx+MvCn1B/KWFMvrNiCkuDOL9fxdPNyT3tZtO35rQqc5TjJDez/",
"NBhuMVPEMRjzQ3X0UotbG7s/iRy38rKWXBChRsvbCdLR2relqmJAP9Z1q9Api1hI3X+5Lf8NAAD//0cV",
"3sY5HAAA",
}
// GetSwagger returns the content of the embedded swagger specification file

View file

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

View file

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

View file

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

View file

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

View file

@ -16,7 +16,7 @@ const Quests = () => {
return (<>
<Title>Квесты</Title>
{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 { 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 { ajax } from '../../utils/fetch'
import { useState } from 'react'
@ -30,7 +30,11 @@ const Quest = () => {
}
const onFinish = (values) => {
ajax('/api/games', {
let url = '/api/admin/games'
if (quest.id) {
url = `/api/admin/games/${quest.id}`
}
ajax(url, {
method: 'POST',
headers: {
Accept: 'application/json',
@ -38,7 +42,7 @@ const Quest = () => {
},
body: JSON.stringify(values)
})
.then(g => navigate(`/quest/${g.id}/edit`))
.then(g => navigate(`/admin/quests/${g.id}/`))
.catch(({ message }) => setError('Ошибка создания'))
}
@ -64,6 +68,9 @@ const Quest = () => {
Сохранить квест
</Button>
</Form.Item>
<Form.Item label='Опубликован?' name='visible'>
<Switch />
</Form.Item>
<Form.Item label='Название' name='title'>
<Input />
</Form.Item>
@ -75,6 +82,7 @@ const Quest = () => {
label='Иконка'
getValueFromEvent={normFile}
>
{quest.icon ? <Avatar src={`/api/file/${quest.icon}`} /> : null}
<Upload name='file' action='/api/file/upload' listType='picture' maxCount={1}>
<Button icon={<UploadOutlined />}>Загрузка</Button>
</Upload>
@ -108,7 +116,7 @@ const Quest = () => {
// eslint-disable-next-line react/display-name
const renderTaskForm = remove => task => (
<Card
key={task.key}
key={task.id}
title={`Уровень ${task.key}`}
style={{ marginBottom: 8 }}
actions={[
@ -125,6 +133,9 @@ const renderTaskForm = remove => task => (
</Popconfirm>
]}
>
<Form.Item name={[task.name, 'id']} hidden>
<Input />
</Form.Item>
<Form.Item name={[task.name, 'title']} label='Название уровня' help='ВИДНО игрокам'>
<Input />
</Form.Item>
@ -152,19 +163,22 @@ const renderCodeForm = remove => code => (
key={code.key}
style={{ marginBottom: 8 }}
actions={[
<Popconfirm
key='delete'
title='Удалить код?'
onConfirm={() => remove(code.name)}
okText='Да'
cancelText='Нет'
>
<Button danger>
// <Popconfirm
// key='delete'
// title='Удалить код?'
// onConfirm={() => remove(code.name)}
// okText='Да'
// cancelText='Нет'
// >
<Button key="delete" danger onClick={() => remove(code.name)}>
<CloseOutlined/> Удалить код
</Button>
</Popconfirm>
// </Popconfirm>
]}
>
<Form.Item name={[code.name, 'id']} hidden>
<Input />
</Form.Item>
<Form.Item name={[code.name, 'code']} label='Код'>
<Input />
</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 ctx.JSON(http.StatusCreated, api.GameResponse{
Authors: make([]api.UserView, 0, len(game.Authors)),
CreatedAt: game.CreatedAt.Format(time.RFC3339),
tasks := make([]api.TaskEdit, 0, len(game.Tasks))
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,
Icon: game.IconID,
Id: game.ID,
Id: &game.ID,
Points: game.Points,
TaskCount: len(game.Tasks),
Tasks: tasks,
Title: game.Title,
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 {
user := contextlib.GetUser(ctx)
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
}
game = a.mapCreateGameRequest(req, user)
game := a.mapCreateGameRequest(req, user)
var err error
game, err = a.GameService.UpdateGame(ctx.Request().Context(), uid, game)
if err != nil {
return err
}
return ctx.JSON(http.StatusCreated, api.GameResponse{
Authors: make([]api.UserView, 0, len(game.Authors)),
CreatedAt: game.CreatedAt.Format(time.RFC3339),
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,
Id: &game.ID,
Points: game.Points,
TaskCount: len(game.Tasks),
Tasks: tasks,
Title: game.Title,
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 {
game := &models.Game{
Model: models.Model{
ID: uuid.New(),
},
Visible: false,
Visible: req.Visible,
Title: req.Title,
Description: req.Description,
Authors: []*models.User{
@ -91,30 +219,28 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User)
IconID: req.Icon,
}
for order, te := range req.Tasks {
if te.Id == nil {
u := uuid.New()
te.Id = &u
}
task := &models.Task{
Model: models.Model{
ID: uuid.New(),
ID: *te.Id,
},
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 {
if ce.Id == nil {
u := uuid.New()
ce.Id = &u
}
task.Codes = append(task.Codes, &models.Code{
Model: models.Model{
ID: uuid.New(),
ID: *ce.Id,
},
Code: ce.Code,
Description: ce.Description,

View file

@ -41,8 +41,7 @@ func (gs *Game) List(ctx context.Context) ([]*models.Game, error) {
Order("created_at DESC").
Preload("Tasks").
Preload("Authors").
Find(&games).
Limit(20).
Find(&games, "visible = true").
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) {
games := make([]*models.Game, 0)
return games, gs.DB.
model := gs.DB.
WithContext(ctx).
Model(&models.Game{}).
Preload("Authors", gs.DB.Where("id = ?", author.ID)).
Model(&models.Game{})
if author.Role == models.RoleCreator {
model.Preload("Authors", gs.DB.Where("id = ?", author.ID))
}
return games,
model.
Order("created_at DESC").
Find(&games).
Limit(20).
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) {
game.ID = uid
return game, gs.DB.
Session(&gorm.Session{FullSaveAssociations: true}).
db := gs.DB
return game, db.Debug().
Session(&gorm.Session{FullSaveAssociations: true}).Omit("created_at").
Save(game).
Error
}