Первичная имплементация админки
|
@ -10,3 +10,5 @@ steps:
|
||||||
tags:
|
tags:
|
||||||
- latest
|
- latest
|
||||||
- ${CI_COMMIT_SHA}
|
- ${CI_COMMIT_SHA}
|
||||||
|
build_args:
|
||||||
|
- VERSION=${CI_COMMIT_SHA}
|
|
@ -122,8 +122,23 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
|
/games/{uid}:
|
||||||
|
post:
|
||||||
|
operationId: editGame
|
||||||
|
parameters:
|
||||||
|
- name: uid
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
security:
|
||||||
|
- cookieAuth: [creator, admin]
|
||||||
|
requestBody:
|
||||||
|
$ref: "#/components/requestBodies/gameEditRequest"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: "#/components/responses/gameResponse"
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
userView:
|
userView:
|
||||||
|
@ -218,15 +233,15 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/codeView'
|
$ref: '#/components/schemas/codeView'
|
||||||
solutions:
|
# solutions:
|
||||||
type: array
|
# type: array
|
||||||
items:
|
# items:
|
||||||
$ref: '#/components/schemas/solutionView'
|
# $ref: '#/components/schemas/solutionView'
|
||||||
required:
|
required:
|
||||||
- title
|
- title
|
||||||
- text
|
- text
|
||||||
- codes
|
- codes
|
||||||
- solutions
|
# - solutions
|
||||||
codeView:
|
codeView:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -281,15 +296,15 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/codeEdit'
|
$ref: '#/components/schemas/codeEdit'
|
||||||
solutions:
|
# solutions:
|
||||||
type: array
|
# type: array
|
||||||
items:
|
# items:
|
||||||
$ref: '#/components/schemas/solutionEdit'
|
# $ref: '#/components/schemas/solutionEdit'
|
||||||
required:
|
required:
|
||||||
- title
|
- title
|
||||||
- text
|
- text
|
||||||
- codes
|
- codes
|
||||||
- solutions
|
# - solutions
|
||||||
codeEdit:
|
codeEdit:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -300,16 +315,16 @@ components:
|
||||||
required:
|
required:
|
||||||
- description
|
- description
|
||||||
- code
|
- code
|
||||||
solutionEdit:
|
# solutionEdit:
|
||||||
type: object
|
# type: object
|
||||||
properties:
|
# properties:
|
||||||
text:
|
# text:
|
||||||
type: string
|
# type: string
|
||||||
after:
|
# after:
|
||||||
type: integer
|
# type: integer
|
||||||
required:
|
# required:
|
||||||
- after
|
# - after
|
||||||
- text
|
# - text
|
||||||
gameType:
|
gameType:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
|
|
|
@ -40,6 +40,9 @@ type ServerInterface interface {
|
||||||
// (POST /games)
|
// (POST /games)
|
||||||
CreateGame(ctx echo.Context) error
|
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
|
||||||
|
|
||||||
|
@ -145,6 +148,24 @@ func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error {
|
||||||
return err
|
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
|
||||||
|
@ -219,6 +240,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||||
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", 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)
|
||||||
|
@ -229,26 +251,26 @@ 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=",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|
31
api/types.go
|
@ -71,33 +71,19 @@ type GameView struct {
|
||||||
Type GameType `json:"type"`
|
Type GameType `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SolutionEdit defines model for solutionEdit.
|
|
||||||
type SolutionEdit struct {
|
|
||||||
After int `json:"after"`
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SolutionView defines model for solutionView.
|
|
||||||
type SolutionView struct {
|
|
||||||
After int `json:"after"`
|
|
||||||
Text *string `json:"text,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TaskEdit defines model for taskEdit.
|
// TaskEdit defines model for taskEdit.
|
||||||
type TaskEdit struct {
|
type TaskEdit struct {
|
||||||
Codes []CodeEdit `json:"codes"`
|
Codes []CodeEdit `json:"codes"`
|
||||||
Solutions []SolutionEdit `json:"solutions"`
|
Text string `json:"text"`
|
||||||
Text string `json:"text"`
|
Title string `json:"title"`
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskView defines model for taskView.
|
// TaskView defines model for taskView.
|
||||||
type TaskView struct {
|
type TaskView struct {
|
||||||
Codes []CodeView `json:"codes"`
|
Codes []CodeView `json:"codes"`
|
||||||
Message *TaskViewMessage `json:"message,omitempty"`
|
Message *TaskViewMessage `json:"message,omitempty"`
|
||||||
Solutions []SolutionView `json:"solutions"`
|
Text string `json:"text"`
|
||||||
Text string `json:"text"`
|
Title string `json:"title"`
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskViewMessage defines model for TaskView.Message.
|
// TaskViewMessage defines model for TaskView.Message.
|
||||||
|
@ -195,6 +181,9 @@ 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.
|
||||||
|
type EditGameJSONRequestBody = GameEdit
|
||||||
|
|
||||||
// PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType.
|
// PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType.
|
||||||
type PostUserLoginJSONRequestBody PostUserLoginJSONBody
|
type PostUserLoginJSONRequestBody PostUserLoginJSONBody
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 6.5 KiB |
|
@ -14,6 +14,7 @@ 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'
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
|
@ -48,7 +49,7 @@ const router = createBrowserRouter(
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="quest/new"
|
path="quest/new"
|
||||||
element={<Auth role="creator"><NoMatch /></Auth>}
|
element={<Auth role="creator"><Quest /></Auth>}
|
||||||
// loader={() => ajax(`/api/admin/games`)}
|
// loader={() => ajax(`/api/admin/games`)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 4.3 KiB |
|
@ -2,9 +2,9 @@
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
height: 40px;
|
height: 48px;
|
||||||
width: 135px;
|
width: 54px;
|
||||||
background-image: url("./logo_small.png");
|
background-image: url("./logo.png");
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 50px;
|
margin-right: 50px;
|
||||||
|
|
|
@ -32,10 +32,10 @@ const Login = () => {
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<h1>Вход</h1>
|
<h1>Вход</h1>
|
||||||
{error ? <Alert type="error" message={error} /> : null}
|
{error ? <Alert type='error' message={error} /> : null}
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
name="login"
|
name='login'
|
||||||
labelCol={{
|
labelCol={{
|
||||||
span: 8
|
span: 8
|
||||||
}}
|
}}
|
||||||
|
@ -48,8 +48,8 @@ const Login = () => {
|
||||||
|
|
||||||
onFinish={onFinish}>
|
onFinish={onFinish}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="E-mail"
|
label='E-mail'
|
||||||
name="email"
|
name='email'
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -62,13 +62,13 @@ const Login = () => {
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type='email'
|
||||||
placeholder="name@mail.ru"
|
placeholder='name@mail.ru'
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Пароль"
|
label='Пароль'
|
||||||
name="password"
|
name='password'
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -84,7 +84,7 @@ const Login = () => {
|
||||||
span: 16
|
span: 16
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button type="primary" htmlType="submit">
|
<Button type='primary' htmlType='submit'>
|
||||||
Вход
|
Вход
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
@ -1,5 +1,177 @@
|
||||||
const Quest = () => {
|
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||||||
|
import { Alert, Button, Card, Form, Input, InputNumber, Popconfirm, Radio, Typography, Upload } from 'antd'
|
||||||
|
import { UploadOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons'
|
||||||
|
import { ajax } from '../../utils/fetch'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const { Title } = Typography
|
||||||
|
|
||||||
|
const Quest = () => {
|
||||||
|
let quest = useLoaderData()
|
||||||
|
const [error, setError] = useState()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const normFile = (e) => {
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (e.file.response) {
|
||||||
|
return e.file.response.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
const formItemLayout = {
|
||||||
|
labelCol: { span: 6 },
|
||||||
|
wrapperCol: { span: 14 }
|
||||||
|
}
|
||||||
|
const buttonLayout = {
|
||||||
|
offset: 6,
|
||||||
|
span: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFinish = (values) => {
|
||||||
|
ajax('/api/games', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(values)
|
||||||
|
})
|
||||||
|
.then(g => navigate(`/quest/${g.id}/edit`))
|
||||||
|
.catch(({ message }) => setError('Ошибка создания'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!quest) {
|
||||||
|
quest = {
|
||||||
|
type: 'city',
|
||||||
|
points: 10,
|
||||||
|
tasks: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>{quest.title ? (quest.title) : ('Новый квест')}</Title>
|
||||||
|
{error ? <Alert type="error" message={error} /> : null}
|
||||||
|
<Form
|
||||||
|
initialValues={quest}
|
||||||
|
onFinish={onFinish}
|
||||||
|
{...formItemLayout}
|
||||||
|
>
|
||||||
|
<Form.Item wrapperCol={buttonLayout}>
|
||||||
|
<Button type='primary' htmlType='submit' block>
|
||||||
|
Сохранить квест
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label='Название' name='title'>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label='Описание' name='description' help='Поддерживается Markdown'>
|
||||||
|
<Input.TextArea />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name='icon'
|
||||||
|
label='Иконка'
|
||||||
|
getValueFromEvent={normFile}
|
||||||
|
>
|
||||||
|
<Upload name='file' action='/api/file/upload' listType='picture' maxCount={1}>
|
||||||
|
<Button icon={<UploadOutlined />}>Загрузка</Button>
|
||||||
|
</Upload>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label='Тип квеста' name='type'>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio.Button value='city'>Полевой</Radio.Button>
|
||||||
|
<Radio.Button value='virtual'>Виртуальный</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label='Очков опыта за квест' name='points'>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.List name='tasks'>
|
||||||
|
{(tasks, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{tasks.map(renderTaskForm(remove))}
|
||||||
|
<Form.Item wrapperCol={buttonLayout}>
|
||||||
|
<Button type='primary' onClick={() => add()} block>
|
||||||
|
<PlusOutlined/> Добавить уровень
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
const renderTaskForm = remove => task => (
|
||||||
|
<Card
|
||||||
|
key={task.key}
|
||||||
|
title={`Уровень ${task.key}`}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
actions={[
|
||||||
|
<Popconfirm
|
||||||
|
key={'removeLevel'}
|
||||||
|
title='Удалить уровень?'
|
||||||
|
onConfirm={() => remove(task.name)}
|
||||||
|
okText='Да'
|
||||||
|
cancelText='Нет'
|
||||||
|
>
|
||||||
|
<Button danger>
|
||||||
|
<CloseOutlined/> Удалить уровень
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Form.Item name={[task.name, 'title']} label='Название уровня' help='ВИДНО игрокам'>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={[task.name, 'text']} label='Текст задания' help='Поддерживается Markdown'>
|
||||||
|
<Input.TextArea />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.List name={[task.name, 'codes']}>
|
||||||
|
{(codes, codesOpts) => (
|
||||||
|
<>
|
||||||
|
{codes.map(renderCodeForm(codesOpts.remove))}
|
||||||
|
<Form.Item wrapperCol={{ offset: 6, span: 14 }}>
|
||||||
|
<Button key='addCode' type='primary' onClick={() => codesOpts.add()} block>
|
||||||
|
<PlusOutlined/> Добавить код
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
const renderCodeForm = remove => code => (
|
||||||
|
<Card
|
||||||
|
key={code.key}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
actions={[
|
||||||
|
<Popconfirm
|
||||||
|
key='delete'
|
||||||
|
title='Удалить код?'
|
||||||
|
onConfirm={() => remove(code.name)}
|
||||||
|
okText='Да'
|
||||||
|
cancelText='Нет'
|
||||||
|
>
|
||||||
|
<Button danger>
|
||||||
|
<CloseOutlined/> Удалить код
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Form.Item name={[code.name, 'code']} label='Код'>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={[code.name, 'description']} label='Описание кода' help='Видно игрокам всегда'>
|
||||||
|
<Input.TextArea />
|
||||||
|
</Form.Item>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
|
||||||
export default Quest
|
export default Quest
|
||||||
|
|
|
@ -45,6 +45,35 @@ func (a *Admin) CreateGame(ctx echo.Context) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (POST /games/{uid})
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
Description: game.Description,
|
||||||
|
Icon: game.IconID,
|
||||||
|
Id: game.ID,
|
||||||
|
Points: game.Points,
|
||||||
|
TaskCount: len(game.Tasks),
|
||||||
|
Title: game.Title,
|
||||||
|
Type: api.MapGameTypeReverse(game.Type),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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{
|
||||||
|
@ -66,22 +95,22 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User)
|
||||||
Model: models.Model{
|
Model: models.Model{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
},
|
},
|
||||||
Title: te.Title,
|
Title: te.Title,
|
||||||
Text: te.Text,
|
Text: te.Text,
|
||||||
MaxTime: 0,
|
MaxTime: 0,
|
||||||
Solutions: make([]*models.Solution, 0, len(te.Solutions)),
|
// 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 {
|
// for _, s := range te.Solutions {
|
||||||
task.Solutions = append(task.Solutions, &models.Solution{
|
// task.Solutions = append(task.Solutions, &models.Solution{
|
||||||
Model: models.Model{
|
// Model: models.Model{
|
||||||
ID: uuid.New(),
|
// ID: uuid.New(),
|
||||||
},
|
// },
|
||||||
After: s.After,
|
// After: s.After,
|
||||||
Text: s.Text,
|
// Text: s.Text,
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
for _, ce := range te.Codes {
|
for _, ce := range te.Codes {
|
||||||
task.Codes = append(task.Codes, &models.Code{
|
task.Codes = append(task.Codes, &models.Code{
|
||||||
Model: models.Model{
|
Model: models.Model{
|
||||||
|
|
|
@ -75,11 +75,11 @@ func (ec *Engine) EnterCode(c echo.Context, uid uuid.UUID) error {
|
||||||
|
|
||||||
func mapCursorToTask(cursor *models.GameCursor, message *api.TaskViewMessage) *api.TaskView {
|
func mapCursorToTask(cursor *models.GameCursor, message *api.TaskViewMessage) *api.TaskView {
|
||||||
resp := &api.TaskResponse{
|
resp := &api.TaskResponse{
|
||||||
Message: message,
|
Message: message,
|
||||||
Codes: make([]api.CodeView, 0, len(cursor.Task.Codes)),
|
Codes: make([]api.CodeView, 0, len(cursor.Task.Codes)),
|
||||||
Solutions: []api.SolutionView{},
|
// Solutions: []api.SolutionView{},
|
||||||
Text: cursor.Task.Text,
|
Text: cursor.Task.Text,
|
||||||
Title: cursor.Task.Title,
|
Title: cursor.Task.Title,
|
||||||
}
|
}
|
||||||
for _, code := range cursor.Task.Codes {
|
for _, code := range cursor.Task.Codes {
|
||||||
c := api.CodeView{
|
c := api.CodeView{
|
||||||
|
|
|
@ -71,3 +71,12 @@ func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game
|
||||||
Create(game).
|
Create(game).
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}).
|
||||||
|
Save(game).
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|