diff --git a/.woodpecker.yaml b/.woodpecker.yaml index bf6b4f2..5be2cad 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -9,4 +9,6 @@ steps: repo: gitrepo.ru/neonxp/nquest tags: - latest - - ${CI_COMMIT_SHA} \ No newline at end of file + - ${CI_COMMIT_SHA} + build_args: + - VERSION=${CI_COMMIT_SHA} \ No newline at end of file diff --git a/api/openapi.yaml b/api/openapi.yaml index e916808..a3a6038 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -122,8 +122,23 @@ paths: schema: type: string 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: schemas: userView: @@ -218,15 +233,15 @@ components: type: array items: $ref: '#/components/schemas/codeView' - solutions: - type: array - items: - $ref: '#/components/schemas/solutionView' + # solutions: + # type: array + # items: + # $ref: '#/components/schemas/solutionView' required: - title - text - codes - - solutions + # - solutions codeView: type: object properties: @@ -281,15 +296,15 @@ components: type: array items: $ref: '#/components/schemas/codeEdit' - solutions: - type: array - items: - $ref: '#/components/schemas/solutionEdit' + # solutions: + # type: array + # items: + # $ref: '#/components/schemas/solutionEdit' required: - title - text - codes - - solutions + # - solutions codeEdit: type: object properties: @@ -300,16 +315,16 @@ components: required: - description - code - solutionEdit: - type: object - properties: - text: - type: string - after: - type: integer - required: - - after - - text + # solutionEdit: + # type: object + # properties: + # text: + # type: string + # after: + # type: integer + # required: + # - after + # - text gameType: type: string enum: diff --git a/api/server.go b/api/server.go index 34d1157..f0f287e 100644 --- a/api/server.go +++ b/api/server.go @@ -40,6 +40,9 @@ type ServerInterface interface { // (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 @@ -145,6 +148,24 @@ func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error { 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 @@ -219,6 +240,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL 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) @@ -229,26 +251,26 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "Ymo063X9bwAAAP//ZDhhHgMaAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/types.go b/api/types.go index 609673b..5e2e658 100644 --- a/api/types.go +++ b/api/types.go @@ -71,33 +71,19 @@ type GameView struct { 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. type TaskEdit struct { - Codes []CodeEdit `json:"codes"` - Solutions []SolutionEdit `json:"solutions"` - Text string `json:"text"` - Title string `json:"title"` + Codes []CodeEdit `json:"codes"` + Text string `json:"text"` + Title string `json:"title"` } // TaskView defines model for taskView. type TaskView struct { - Codes []CodeView `json:"codes"` - Message *TaskViewMessage `json:"message,omitempty"` - Solutions []SolutionView `json:"solutions"` - Text string `json:"text"` - Title string `json:"title"` + Codes []CodeView `json:"codes"` + Message *TaskViewMessage `json:"message,omitempty"` + Text string `json:"text"` + Title string `json:"title"` } // TaskViewMessage defines model for TaskView.Message. @@ -195,6 +181,9 @@ 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 + // PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType. type PostUserLoginJSONRequestBody PostUserLoginJSONBody diff --git a/frontend/public/assets/icons/icon-128x128.png b/frontend/public/assets/icons/icon-128x128.png index 40e5902..25417e4 100644 Binary files a/frontend/public/assets/icons/icon-128x128.png and b/frontend/public/assets/icons/icon-128x128.png differ diff --git a/frontend/public/assets/icons/icon-144x144.png b/frontend/public/assets/icons/icon-144x144.png index aaa48a2..c0b213b 100644 Binary files a/frontend/public/assets/icons/icon-144x144.png and b/frontend/public/assets/icons/icon-144x144.png differ diff --git a/frontend/public/assets/icons/icon-152x152.png b/frontend/public/assets/icons/icon-152x152.png index a58a7be..fc51728 100644 Binary files a/frontend/public/assets/icons/icon-152x152.png and b/frontend/public/assets/icons/icon-152x152.png differ diff --git a/frontend/public/assets/icons/icon-192x192.png b/frontend/public/assets/icons/icon-192x192.png index afbbee8..8fcc882 100644 Binary files a/frontend/public/assets/icons/icon-192x192.png and b/frontend/public/assets/icons/icon-192x192.png differ diff --git a/frontend/public/assets/icons/icon-384x384.png b/frontend/public/assets/icons/icon-384x384.png index c503c32..12c20f3 100644 Binary files a/frontend/public/assets/icons/icon-384x384.png and b/frontend/public/assets/icons/icon-384x384.png differ diff --git a/frontend/public/assets/icons/icon-512x512.png b/frontend/public/assets/icons/icon-512x512.png index f2e7457..cdf006c 100644 Binary files a/frontend/public/assets/icons/icon-512x512.png and b/frontend/public/assets/icons/icon-512x512.png differ diff --git a/frontend/public/assets/icons/icon-72x72.png b/frontend/public/assets/icons/icon-72x72.png index 05711f3..a1fc5ce 100644 Binary files a/frontend/public/assets/icons/icon-72x72.png and b/frontend/public/assets/icons/icon-72x72.png differ diff --git a/frontend/public/assets/icons/icon-96x96.png b/frontend/public/assets/icons/icon-96x96.png index b668560..1f4666f 100644 Binary files a/frontend/public/assets/icons/icon-96x96.png and b/frontend/public/assets/icons/icon-96x96.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 42dc117..2f8c31a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -14,6 +14,7 @@ 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' const router = createBrowserRouter( createRoutesFromElements( @@ -48,7 +49,7 @@ const router = createBrowserRouter( /> } + element={} // loader={() => ajax(`/api/admin/games`)} /> diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png index ce1dc0e..1f4666f 100644 Binary files a/frontend/src/assets/logo.png and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/assets/logo_small.png b/frontend/src/assets/logo_small.png deleted file mode 100644 index 9785f26..0000000 Binary files a/frontend/src/assets/logo_small.png and /dev/null differ diff --git a/frontend/src/assets/styles.css b/frontend/src/assets/styles.css index 21e7108..859a2db 100644 --- a/frontend/src/assets/styles.css +++ b/frontend/src/assets/styles.css @@ -2,9 +2,9 @@ font-size: 26px; padding-top: 0 !important; padding-bottom: 0 !important; - height: 40px; - width: 135px; - background-image: url("./logo_small.png"); + height: 48px; + width: 54px; + background-image: url("./logo.png"); background-size: cover; display: inline-block; margin-right: 50px; diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 23d689d..f53ae92 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -32,10 +32,10 @@ const Login = () => { return (<>

Вход

- {error ? : null} + {error ? : null}
{ onFinish={onFinish}> { ]} > { span: 16 }} > - diff --git a/frontend/src/pages/admin/Quest.jsx b/frontend/src/pages/admin/Quest.jsx index 36eda13..8c0b3e0 100644 --- a/frontend/src/pages/admin/Quest.jsx +++ b/frontend/src/pages/admin/Quest.jsx @@ -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 ( + <> + {quest.title ? (quest.title) : ('Новый квест')} + {error ? : null} + + + + + + + + + + + + + + + + + + Полевой + Виртуальный + + + + + + + {(tasks, { add, remove }) => ( + <> + {tasks.map(renderTaskForm(remove))} + + + + + )} + + + + ) } +// eslint-disable-next-line react/display-name +const renderTaskForm = remove => task => ( + remove(task.name)} + okText='Да' + cancelText='Нет' + > + + + ]} + > + + + + + + + + {(codes, codesOpts) => ( + <> + {codes.map(renderCodeForm(codesOpts.remove))} + + + + + )} + + +) + +// eslint-disable-next-line react/display-name +const renderCodeForm = remove => code => ( + remove(code.name)} + okText='Да' + cancelText='Нет' + > + + + ]} + > + + + + + + + +) + export default Quest diff --git a/pkg/controller/admin.go b/pkg/controller/admin.go index 5cd67bc..605a471 100644 --- a/pkg/controller/admin.go +++ b/pkg/controller/admin.go @@ -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 { game := &models.Game{ Model: models.Model{ @@ -66,22 +95,22 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) Model: models.Model{ ID: uuid.New(), }, - Title: te.Title, - Text: te.Text, - MaxTime: 0, - Solutions: make([]*models.Solution, 0, len(te.Solutions)), + 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 _, 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{ diff --git a/pkg/controller/engine.go b/pkg/controller/engine.go index 6b9ca0e..1b03442 100644 --- a/pkg/controller/engine.go +++ b/pkg/controller/engine.go @@ -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 { resp := &api.TaskResponse{ - Message: message, - Codes: make([]api.CodeView, 0, len(cursor.Task.Codes)), - Solutions: []api.SolutionView{}, - Text: cursor.Task.Text, - Title: cursor.Task.Title, + Message: message, + Codes: make([]api.CodeView, 0, len(cursor.Task.Codes)), + // Solutions: []api.SolutionView{}, + Text: cursor.Task.Text, + Title: cursor.Task.Title, } for _, code := range cursor.Task.Codes { c := api.CodeView{ diff --git a/pkg/service/game.go b/pkg/service/game.go index c28475f..42ee0ca 100644 --- a/pkg/service/game.go +++ b/pkg/service/game.go @@ -71,3 +71,12 @@ func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game Create(game). 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 +}