Antd вместо bootstrap
This commit is contained in:
parent
e7acaa92a5
commit
cb558d05fe
33 changed files with 1933 additions and 381 deletions
|
@ -89,6 +89,41 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
$ref: '#/components/responses/taskResponse'
|
$ref: '#/components/responses/taskResponse'
|
||||||
|
/file/upload:
|
||||||
|
post:
|
||||||
|
security: []
|
||||||
|
operationId: uploadFile
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: '#/components/responses/uploadResponse'
|
||||||
|
/file/{uid}:
|
||||||
|
get:
|
||||||
|
operationId: getFile
|
||||||
|
parameters:
|
||||||
|
- name: uid
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: file
|
||||||
|
content:
|
||||||
|
'application/octet-stream':
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
|
||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
|
@ -123,6 +158,9 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/userView"
|
$ref: "#/components/schemas/userView"
|
||||||
|
icon:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- title
|
- title
|
||||||
|
@ -132,6 +170,7 @@ components:
|
||||||
- taskCount
|
- taskCount
|
||||||
- createdAt
|
- createdAt
|
||||||
- authors
|
- authors
|
||||||
|
- icon
|
||||||
taskView:
|
taskView:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -193,12 +232,16 @@ components:
|
||||||
$ref: "#/components/schemas/taskEdit"
|
$ref: "#/components/schemas/taskEdit"
|
||||||
points:
|
points:
|
||||||
type: integer
|
type: integer
|
||||||
|
icon:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
required:
|
required:
|
||||||
- title
|
- title
|
||||||
- description
|
- description
|
||||||
- type
|
- type
|
||||||
- tasks
|
- tasks
|
||||||
- points
|
- points
|
||||||
|
- icon
|
||||||
taskEdit:
|
taskEdit:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -358,6 +401,18 @@ components:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/taskView"
|
$ref: "#/components/schemas/taskView"
|
||||||
|
uploadResponse:
|
||||||
|
description: ''
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
required:
|
||||||
|
- uuid
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
cookieAuth:
|
cookieAuth:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
|
|
|
@ -28,6 +28,12 @@ type ServerInterface interface {
|
||||||
// (POST /engine/{uid}/code)
|
// (POST /engine/{uid}/code)
|
||||||
EnterCode(ctx echo.Context, uid openapi_types.UUID) error
|
EnterCode(ctx echo.Context, uid openapi_types.UUID) error
|
||||||
|
|
||||||
|
// (POST /file/upload)
|
||||||
|
UploadFile(ctx echo.Context) error
|
||||||
|
|
||||||
|
// (GET /file/{uid})
|
||||||
|
GetFile(ctx echo.Context, uid openapi_types.UUID) error
|
||||||
|
|
||||||
// (GET /games)
|
// (GET /games)
|
||||||
GetGames(ctx echo.Context) error
|
GetGames(ctx echo.Context) error
|
||||||
|
|
||||||
|
@ -88,6 +94,33 @@ func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UploadFile converts echo context to params.
|
||||||
|
func (w *ServerInterfaceWrapper) UploadFile(ctx echo.Context) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Invoke the callback with all the unmarshaled arguments
|
||||||
|
err = w.Handler.UploadFile(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFile converts echo context to params.
|
||||||
|
func (w *ServerInterfaceWrapper) GetFile(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{})
|
||||||
|
|
||||||
|
// Invoke the callback with all the unmarshaled arguments
|
||||||
|
err = w.Handler.GetFile(ctx, uid)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// GetGames converts echo context to params.
|
// GetGames converts echo context to params.
|
||||||
func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
|
func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
|
@ -176,6 +209,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||||
|
|
||||||
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.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.GET(baseURL+"/user", wrapper.GetUser)
|
router.GET(baseURL+"/user", wrapper.GetUser)
|
||||||
|
@ -188,24 +223,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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",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|
23
api/types.go
23
api/types.go
|
@ -40,11 +40,12 @@ type CodeView struct {
|
||||||
|
|
||||||
// GameEdit defines model for gameEdit.
|
// GameEdit defines model for gameEdit.
|
||||||
type GameEdit struct {
|
type GameEdit struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Points int `json:"points"`
|
Icon openapi_types.UUID `json:"icon"`
|
||||||
Tasks []TaskEdit `json:"tasks"`
|
Points int `json:"points"`
|
||||||
Title string `json:"title"`
|
Tasks []TaskEdit `json:"tasks"`
|
||||||
Type GameType `json:"type"`
|
Title string `json:"title"`
|
||||||
|
Type GameType `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GameType defines model for gameType.
|
// GameType defines model for gameType.
|
||||||
|
@ -55,6 +56,7 @@ type GameView struct {
|
||||||
Authors []UserView `json:"authors"`
|
Authors []UserView `json:"authors"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Icon openapi_types.UUID `json:"icon"`
|
||||||
Id openapi_types.UUID `json:"id"`
|
Id openapi_types.UUID `json:"id"`
|
||||||
Points int `json:"points"`
|
Points int `json:"points"`
|
||||||
TaskCount int `json:"taskCount"`
|
TaskCount int `json:"taskCount"`
|
||||||
|
@ -115,6 +117,11 @@ type GameResponse = GameView
|
||||||
// TaskResponse defines model for taskResponse.
|
// TaskResponse defines model for taskResponse.
|
||||||
type TaskResponse = TaskView
|
type TaskResponse = TaskView
|
||||||
|
|
||||||
|
// UploadResponse defines model for uploadResponse.
|
||||||
|
type UploadResponse struct {
|
||||||
|
Uuid openapi_types.UUID `json:"uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
// UserResponse defines model for userResponse.
|
// UserResponse defines model for userResponse.
|
||||||
type UserResponse struct {
|
type UserResponse struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
@ -154,6 +161,9 @@ type EnterCodeJSONBody struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UploadFileMultipartBody defines parameters for UploadFile.
|
||||||
|
type UploadFileMultipartBody interface{}
|
||||||
|
|
||||||
// PostUserLoginJSONBody defines parameters for PostUserLogin.
|
// PostUserLoginJSONBody defines parameters for PostUserLogin.
|
||||||
type PostUserLoginJSONBody struct {
|
type PostUserLoginJSONBody struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
@ -171,6 +181,9 @@ type PostUserRegisterJSONBody struct {
|
||||||
// EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType.
|
// EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType.
|
||||||
type EnterCodeJSONRequestBody EnterCodeJSONBody
|
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
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>nQuest</title>
|
<title>nQuest</title>
|
||||||
</head>
|
</head>
|
||||||
<body data-bs-theme="dark">
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
1219
frontend/package-lock.json
generated
1219
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -11,12 +11,13 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neonxp/compose": "0.0.6",
|
"@neonxp/compose": "0.0.6",
|
||||||
|
"antd": "^5.12.8",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^2.9.1",
|
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^9.0.0",
|
"react-markdown": "^9.0.0",
|
||||||
"react-router-dom": "^6.17.0",
|
"react-router-dom": "^6.17.0",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
"unstated-next": "^1.1.0"
|
"unstated-next": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
BIN
frontend/public/icon.png
Normal file
BIN
frontend/public/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
frontend/public/icon150.png
Normal file
BIN
frontend/public/icon150.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
BIN
frontend/public/icon278.png
Normal file
BIN
frontend/public/icon278.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
frontend/public/icon32.png
Normal file
BIN
frontend/public/icon32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/public/icon576.png
Normal file
BIN
frontend/public/icon576.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
frontend/public/icon578.png
Normal file
BIN
frontend/public/icon578.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -9,6 +9,7 @@ import NoMatch from './pages/NoMatch'
|
||||||
import { UserProvider } from './store/user'
|
import { UserProvider } from './store/user'
|
||||||
import { ajax } from './utils/fetch'
|
import { ajax } from './utils/fetch'
|
||||||
import Engine from './pages/Engine'
|
import Engine from './pages/Engine'
|
||||||
|
import Quests from './pages/Quests'
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
|
@ -16,19 +17,24 @@ const router = createBrowserRouter(
|
||||||
path="/"
|
path="/"
|
||||||
id="root"
|
id="root"
|
||||||
element={<Layout />}
|
element={<Layout />}
|
||||||
loader={async () => ajax("/api/user")}
|
loader={async () => ajax("/api/user").catch(x => {console.log(x); return null})}
|
||||||
>
|
>
|
||||||
<Route
|
<Route
|
||||||
index
|
index
|
||||||
element={<Index />}
|
element={<Index />}
|
||||||
loader={() => ajax("/api/games")}
|
/>
|
||||||
|
<Route
|
||||||
|
id="quests"
|
||||||
|
path="/quests"
|
||||||
|
element={<Auth><Quests /></Auth>}
|
||||||
|
loader={() => ajax("/api/games").catch(x => console.log(x))}
|
||||||
/>
|
/>
|
||||||
<Route path="login" element={<Login />} />
|
<Route path="login" element={<Login />} />
|
||||||
<Route path="register" element={<Register />} />
|
<Route path="register" element={<Register />} />
|
||||||
<Route
|
<Route
|
||||||
path="go/:gameId"
|
path="go/:gameId"
|
||||||
element={<Auth><Engine /></Auth>}
|
element={<Auth><Engine /></Auth>}
|
||||||
loader={({ params }) => ajax(`/api/engine/${params.gameId}`)}
|
loader={({ params }) => ajax(`/api/engine/${params.gameId}`).catch(x => console.log(x))}
|
||||||
/>
|
/>
|
||||||
{/* <Route
|
{/* <Route
|
||||||
path="admin"
|
path="admin"
|
||||||
|
|
12
frontend/src/assets/bootstrap.min.css
vendored
12
frontend/src/assets/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -1,26 +1,4 @@
|
||||||
/* navbar */
|
|
||||||
.navbar-dark {
|
|
||||||
border-bottom: 0.1px solid #333333 !important;
|
|
||||||
}
|
|
||||||
.navbar {
|
|
||||||
background-color: rgb(16, 22, 29) !important;
|
|
||||||
}
|
|
||||||
/* @font-face {
|
|
||||||
font-family: 'TiltNeon';
|
|
||||||
src: url('TiltNeon-Regular.ttf');
|
|
||||||
} */
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
/* font-family: TiltNeon, var(--bs-font-sans-serif);
|
|
||||||
text-shadow:
|
|
||||||
0 0 1px rgb(255, 255, 255, 1),
|
|
||||||
0 0 2px rgb(255, 255, 255, 1),
|
|
||||||
0 0 3px rgb(255, 255, 255, 1),
|
|
||||||
0 0 4px rgb(255, 255, 255, 1),
|
|
||||||
-1px 0 #198754,
|
|
||||||
0 1px #198754,
|
|
||||||
1px 0 #198754,
|
|
||||||
0 -1px #198754;
|
|
||||||
-webkit-text-stroke: 1px white; */
|
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
|
@ -28,6 +6,7 @@
|
||||||
width: 85px;
|
width: 85px;
|
||||||
background-image: url("./logo_small.png");
|
background-image: url("./logo_small.png");
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
th.thin {
|
th.thin {
|
||||||
|
@ -35,11 +14,18 @@ th.thin {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
html, body, #container {
|
||||||
--bs-body-bg: rgb(23, 30, 38) !important;
|
height: 100%;
|
||||||
--bs-primary-rgb: rgb(16, 22, 29) !important;
|
margin:0;
|
||||||
--bs-navbar-color: rgb(249, 115, 22) !important;
|
background-color: #171E26;
|
||||||
--bs-btn-bg: rgb(249, 115, 22) !important;
|
}
|
||||||
--bs-btn-hover-bg: rgba(249, 115, 22, 0.5) !important;
|
|
||||||
--bs-btn-active-bg: rgba(249, 115, 22, 0.5) !important;
|
.ant-layout-header {
|
||||||
|
background-color: #26323f;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid rgba(154, 197, 247, 0.19);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-horizontal {
|
||||||
|
border-bottom: 0;
|
||||||
}
|
}
|
|
@ -1,14 +1,16 @@
|
||||||
import { Link, Outlet, useLoaderData } from "react-router-dom";
|
import { Link, Outlet, useLoaderData, useLocation, useNavigate, useNavigation } from "react-router-dom";
|
||||||
import { Button, Nav, Container, NavbarBrand, NavbarToggle, Navbar, NavbarCollapse, ButtonGroup, ProgressBar, OverlayTrigger, Tooltip, Col, Row, NavDropdown } from "react-bootstrap";
|
|
||||||
import { UserProvider } from "../store/user";
|
import { UserProvider } from "../store/user";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { ajax } from "../utils/fetch";
|
import { ajax } from "../utils/fetch";
|
||||||
import { useRole } from "../utils/roles";
|
import { useRole } from "../utils/roles";
|
||||||
|
import { Layout, Menu, Progress, Space } from "antd";
|
||||||
|
import { FireTwoTone, StarTwoTone } from '@ant-design/icons';
|
||||||
|
import { Content, Header } from "antd/es/layout/layout";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const params = useLoaderData();
|
const params = useLoaderData();
|
||||||
const { hasRole } = useRole();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUser(params)
|
setUser(params)
|
||||||
}, [params])
|
}, [params])
|
||||||
|
@ -17,53 +19,84 @@ export default () => {
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
ajax("/api/user/logout", {
|
ajax("/api/user/logout", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}).
|
})
|
||||||
then(() => setUser(null))
|
.then(() => { setUser(null); navigate("/login") })
|
||||||
|
.catch(() => { setUser(null); navigate("/login") })
|
||||||
|
}
|
||||||
|
let items = [
|
||||||
|
{ key: "login", label: "Вход", link: "/login" },
|
||||||
|
{ key: "register", label: "Регистрация", link: "/register" }
|
||||||
|
]
|
||||||
|
if (user != null) {
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
key: "quests",
|
||||||
|
label: "Квесты",
|
||||||
|
link: "/quests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "user",
|
||||||
|
label: user.username,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "group",
|
||||||
|
key: "exp",
|
||||||
|
label: (<Space>
|
||||||
|
<Space title={`${user.level} уровень`}><StarTwoTone />{user.level} уровень</Space>
|
||||||
|
<Space title={`${user.experience} опыта`}><FireTwoTone />{user.experience} опыт</Space>
|
||||||
|
</Space>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "group",
|
||||||
|
key: "progress",
|
||||||
|
label: <Progress
|
||||||
|
value={user.experience}
|
||||||
|
percent={((user.experience - user.expToCurrentLevel) / (user.expToNextLevel - user.expToCurrentLevel) * 100)}
|
||||||
|
size="small"
|
||||||
|
showInfo={false}
|
||||||
|
/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "group",
|
||||||
|
key: "nextLevel",
|
||||||
|
label: `Следующий уровень - ${user.expToNextLevel} очков опыта`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "logout",
|
||||||
|
label: "Выход",
|
||||||
|
handler: logout,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const menuHandler = (x) => {
|
||||||
|
const item = getItemByPath(items, x.keyPath);
|
||||||
|
item.link ? navigate(item.link) : null;
|
||||||
|
item.handler ? item.handler() : null;
|
||||||
}
|
}
|
||||||
return (<>
|
|
||||||
<Navbar expand="lg" data-bs-theme="dark">
|
|
||||||
<Container>
|
|
||||||
<NavbarBrand href="/"></NavbarBrand>
|
|
||||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
|
||||||
<NavbarCollapse className="justify-content-between">
|
|
||||||
<Nav className="me-auto">
|
|
||||||
{hasRole("creator") ? (
|
|
||||||
<Nav.Item>
|
|
||||||
<Nav.Link as={Link} className="nav-link" to="/admin">Админка</Nav.Link>
|
|
||||||
</Nav.Item>
|
|
||||||
) : null}
|
|
||||||
</Nav>
|
|
||||||
<Nav>
|
|
||||||
{user ? (
|
|
||||||
<>
|
|
||||||
<NavDropdown title={user.username} id="user-box">
|
|
||||||
<NavDropdown.Item>
|
|
||||||
Уровень: <b>{user.level}</b>, Опыт: <b>{user.experience}/{user.expToNextLevel}</b>
|
|
||||||
</NavDropdown.Item>
|
|
||||||
<NavDropdown.Item>
|
|
||||||
<Nav.Link onClick={logout}>Выход</Nav.Link>
|
|
||||||
</NavDropdown.Item>
|
|
||||||
</NavDropdown>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Nav.Item>
|
|
||||||
<Nav.Link as={Link} className="btn btn-success" to="login">Вход</Nav.Link>
|
|
||||||
</Nav.Item>
|
|
||||||
<Nav.Item>
|
|
||||||
<Nav.Link as={Link} className="btn btn-outline-success" to="register">Регистрация</Nav.Link>
|
|
||||||
</Nav.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Nav>
|
const getItemByPath = (items, path) => {
|
||||||
|
const x = path.pop()
|
||||||
|
const item = items.find(y => y.key === x);
|
||||||
|
if (path.length == 0) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
return getItemByPath(item.children, path)
|
||||||
|
}
|
||||||
|
|
||||||
</NavbarCollapse>
|
return (<Layout>
|
||||||
</Container>
|
<Header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
</Navbar>
|
<Link to="/" className="navbar-brand"></Link>
|
||||||
|
<Menu
|
||||||
<Container>
|
mode="horizontal"
|
||||||
|
items={items}
|
||||||
|
selectedKeys={location.pathname.replace("/", "")}
|
||||||
|
onClick={menuHandler}
|
||||||
|
/>
|
||||||
|
</Header>
|
||||||
|
<Content style={{ padding: '0 24px' }}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Container>
|
</Content>
|
||||||
</>);
|
</Layout>);
|
||||||
}
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import { Compose } from '@neonxp/compose';
|
import { Compose } from '@neonxp/compose';
|
||||||
|
import { App as AntdApp, ConfigProvider, theme } from 'antd';
|
||||||
import "./assets/bootstrap.min.css"
|
const { darkAlgorithm } = theme;
|
||||||
|
import ruRU from "antd/locale/ru_RU";
|
||||||
import "./assets/styles.css"
|
import "./assets/styles.css"
|
||||||
|
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
|
@ -11,7 +12,23 @@ import { store } from './store/provider.js';
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Compose providers={store}>
|
<Compose providers={store}>
|
||||||
<App />
|
<ConfigProvider
|
||||||
|
locale={ruRU}
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
"colorPrimary": "#fb923c",
|
||||||
|
"colorInfo": "#fb923c",
|
||||||
|
"colorSuccess": "#15803d",
|
||||||
|
"colorBgBase": "#171e26",
|
||||||
|
"borderRadius": 3,
|
||||||
|
"wireframe": false
|
||||||
|
},
|
||||||
|
algorithm: darkAlgorithm,
|
||||||
|
}}>
|
||||||
|
<AntdApp>
|
||||||
|
<App />
|
||||||
|
</AntdApp>
|
||||||
|
</ConfigProvider>
|
||||||
</Compose>
|
</Compose>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
|
@ -1,75 +1,115 @@
|
||||||
import { Badge, Button, Col, Form, Row, Table } from "react-bootstrap";
|
|
||||||
import { Link, useLoaderData, useParams } from "react-router-dom";
|
import { Link, useLoaderData, useParams } from "react-router-dom";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ajax } from "../utils/fetch";
|
import { ajax } from "../utils/fetch";
|
||||||
|
import Title from "antd/es/typography/Title";
|
||||||
|
import { Alert, App, Badge, Button, Card, Col, Form, Input, List, Row } from "antd";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const loadedTask = useLoaderData();
|
const loadedTask = useLoaderData();
|
||||||
const [task, setTask] = useState(loadedTask);
|
const [task, setTask] = useState(loadedTask);
|
||||||
const [code, setCode] = useState("");
|
const { message, notification, modal } = App.useApp();
|
||||||
const onSubmitCode = (e) => {
|
|
||||||
e.preventDefault();
|
useEffect(() => {
|
||||||
|
switch (task.message) {
|
||||||
|
case "invalid_code":
|
||||||
|
message.error("Неверный код")
|
||||||
|
break
|
||||||
|
case "old_code":
|
||||||
|
message.error("Этот код уже вводился")
|
||||||
|
break
|
||||||
|
case "next_level":
|
||||||
|
message.success("Переход на новый уровень")
|
||||||
|
break
|
||||||
|
case "ok_code":
|
||||||
|
message.success("Код принят, ищите оставшиеся")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, [task.message])
|
||||||
|
|
||||||
|
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const onFinish = ({ code }) => {
|
||||||
ajax(`/api/engine/${params.gameId}/code`, {
|
ajax(`/api/engine/${params.gameId}/code`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ code })
|
body: JSON.stringify({ code })
|
||||||
}).
|
}).
|
||||||
then((x) => {
|
then((x) => {
|
||||||
if (x != null) {
|
if (x != null) {
|
||||||
setTask(x);
|
setTask(x);
|
||||||
setCode("");
|
form.setFieldsValue({ code: '' });
|
||||||
}
|
}
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (task && task.message == "game_complete") {
|
if (task && task.message == "game_complete") {
|
||||||
return (<div>
|
return (<div style={{ padding: 8 }}>
|
||||||
<div className="alert alert-success" role="alert">Вы прошли все уровни!</div>
|
<Alert type="success" message="Вы прошли все уровни!" />
|
||||||
<Link to={"/"}>К списку игр</Link>
|
<Link to={"/"}>К списку игр</Link>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return (<div className="alert alert-default" role="alert">
|
return (<div style={{ padding: 8 }}>
|
||||||
<div>Для вас не предусмотренно уровней</div>
|
<Alert type="warning" message="Для вас не предусмотренно уровней" />
|
||||||
<Link to={"/"}>К списку игр</Link>
|
<Link to={"/"}>К списку игр</Link>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<h1 className="mb-4">{task.title}</h1>
|
<Title>{task.title}</Title>
|
||||||
<Markdown>{task.text}</Markdown>
|
<Row gutter={8}>
|
||||||
{task.message == "invalid_code" ? (<div className="alert alert-danger" role="alert">Неверный код</div>) : null}
|
<Col xs={24} sm={24} md={18}>
|
||||||
{task.message == "old_code" ? (<div className="alert alert-danger" role="alert">Этот код уже вводился</div>) : null}
|
<Card title="Текст задания" style={{ marginBottom: 8 }}>
|
||||||
{task.message == "next_level" ? (<div className="alert alert-success" role="alert">Переход на новый уровень</div>) : null}
|
<Markdown>{task.text}</Markdown>
|
||||||
{task.message == "ok_code" ? (<div className="alert alert-success" role="alert">Код принят, ищите оставшиеся</div>) : null}
|
</Card>
|
||||||
<h2>Коды:</h2>
|
<Card title="Коды на уровне" style={{ marginBottom: 8 }}>
|
||||||
<ul>
|
<List
|
||||||
{task.codes.map(
|
dataSource={task.codes}
|
||||||
(c, idx) => <li key={idx}>{c.description} {!!c.code ? (<Badge bg="success">Принят {c.code}</Badge>) : (<Badge bg="danger">Не введён</Badge>)}</li>
|
renderItem={(c, idx) => (
|
||||||
)}
|
<List.Item
|
||||||
</ul>
|
key={idx}
|
||||||
<Form onSubmit={onSubmitCode}>
|
extra={!!c.code ? (<Alert type="success" message={`Принят ${c.code}`} />) : (<Alert type="info " message="Не введён" />)}
|
||||||
<Form.Group as={Row} className="mb-3" controlId="code">
|
>
|
||||||
<Form.Label column sm="2">Код:</Form.Label>
|
{c.description}
|
||||||
<Col sm="6">
|
</List.Item>)}
|
||||||
<Form.Control
|
|
||||||
type="text"
|
|
||||||
placeholder="NQ...."
|
|
||||||
value={code}
|
|
||||||
onChange={e => setCode(e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Card>
|
||||||
<Col sm="4">
|
</Col>
|
||||||
<Form.Control type="submit" className="btn btn-primary" value="Отправить" />
|
<Col xs={24} sm={24} md={6}>
|
||||||
</Col>
|
<Card title="Ввод кода" style={{ marginBottom: 8 }}>
|
||||||
</Form.Group>
|
<Form
|
||||||
</Form>
|
style={{ marginTop: 8 }}
|
||||||
|
form={form}
|
||||||
|
name="basic"
|
||||||
|
wrapperCol={{ span: 24 }}
|
||||||
|
onFinish={onFinish}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<Form.Item name="code">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="NQ...."
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" block htmlType="submit">Отправить</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
{task.message == "invalid_code" ? (<Alert type="error" message="Неверный код" />) : null}
|
||||||
|
{task.message == "old_code" ? (<Alert type="warning" message="Этот код уже вводился" />) : null}
|
||||||
|
{task.message == "next_level" ? (<Alert type="success" message="Переход на новый уровень" />) : null}
|
||||||
|
{task.message == "ok_code" ? (<Alert type="success" message="Код принят, ищите оставшиеся" />) : null}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
|
@ -1,73 +1,22 @@
|
||||||
import { Button, Table } from "react-bootstrap";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Link, useLoaderData } from "react-router-dom";
|
|
||||||
import Markdown from "react-markdown";
|
|
||||||
import moment from 'moment/min/moment-with-locales';
|
|
||||||
import { UserProvider } from "../store/user";
|
import { UserProvider } from "../store/user";
|
||||||
|
import { Typography, Button } from 'antd';
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const { Title, Text, Paragraph } = Typography;
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
moment.locale('ru');
|
const {user} = UserProvider.useContainer();
|
||||||
const games = useLoaderData();
|
const navigate = useNavigate();
|
||||||
const { user } = UserProvider.useContainer();
|
|
||||||
return (<>
|
return (<>
|
||||||
<h1 className="mb-4">Доступные квесты</h1>
|
<Title>NQuest</Title>
|
||||||
{games && games.map(game => (
|
<Paragraph>Привет! Это платформа для ARG игр.</Paragraph>
|
||||||
<div key={game.id}>
|
<Paragraph>Если ты попал сюда случайно, то скорее всего, для тебя здесь нет ничего интересного. А если ты знаешь зачем пришёл, то добро пожаловать!</Paragraph>
|
||||||
<h3>{game.title}</h3>
|
{!user?(
|
||||||
<Table className="table table-bordered mb-4">
|
<Button.Group>
|
||||||
<tbody>
|
<Button type="primary" onClick={() => navigate("/login")}>Вход</Button>
|
||||||
<tr>
|
<Button onClick={() => navigate("/register")}>Регистрация</Button>
|
||||||
<td className="col-sm-3">
|
</Button.Group>
|
||||||
Тип
|
):(<Button onClick={() => navigate("/quests")}>К квестам</Button>)}
|
||||||
</td>
|
|
||||||
<td className="col-sm-3">
|
|
||||||
{game.type}
|
|
||||||
</td>
|
|
||||||
<td className="col-sm-3">
|
|
||||||
Опыт за квест
|
|
||||||
</td>
|
|
||||||
<td className="col-sm-3">
|
|
||||||
{game.points}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="col-sm-3">
|
|
||||||
Уровней
|
|
||||||
</td>
|
|
||||||
<td className="col-sm-3">
|
|
||||||
{game.taskCount}
|
|
||||||
</td>
|
|
||||||
<td className="col-sm-3">
|
|
||||||
Опубликовано
|
|
||||||
</td>
|
|
||||||
<td className="col-sm-3">
|
|
||||||
{moment(game.createdAt).fromNow()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="col-sm-3">
|
|
||||||
Автор
|
|
||||||
</td>
|
|
||||||
<td className="col-sm-9" colSpan={3}>
|
|
||||||
{game.authors.map(a => <Link key={a.id} to={`/user/${a.id}`}>{a.username}</Link>)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colSpan="4">
|
|
||||||
<Markdown>{game.description}</Markdown>
|
|
||||||
<div>
|
|
||||||
{user ? (<>
|
|
||||||
{(!!user.games.find(x => x.id === game.id))
|
|
||||||
? (<b>Вы уже прошли этот квест</b>)
|
|
||||||
: (<Link className="btn btn-primary" to={`go/${game.id}`}>Начать прохождение</Link>)}
|
|
||||||
</>): null}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{!games ? (<strong>Игр пока не анонсировано</strong>) : null}
|
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
|
@ -1,16 +1,15 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Form, Button, Row, Col } from "react-bootstrap";
|
|
||||||
import { UserProvider } from "../store/user";
|
import { UserProvider } from "../store/user";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { ajax } from "../utils/fetch";
|
import { ajax } from "../utils/fetch";
|
||||||
|
import { Alert, App, Button, Form, Input } from "antd";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const {user, setUser} = UserProvider.useContainer();
|
const { user, setUser } = UserProvider.useContainer();
|
||||||
const {state} = useLocation()
|
const { state } = useLocation()
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
|
@ -18,48 +17,77 @@ export default () => {
|
||||||
}
|
}
|
||||||
}, [user])
|
}, [user])
|
||||||
|
|
||||||
const onLogin = (e) => {
|
const onFinish = (values) => {
|
||||||
e.preventDefault();
|
|
||||||
ajax("/api/user/login", {
|
ajax("/api/user/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email, password })
|
body: JSON.stringify(values)
|
||||||
}).
|
}).
|
||||||
then(setUser).
|
then(setUser).
|
||||||
catch(({ message }) => setError(message));
|
catch(({ message }) => setError("Проверьте e-mail и пароль"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<h1>Вход</h1>
|
<h1>Вход</h1>
|
||||||
<Form onSubmit={onLogin}>
|
{error ? <Alert type="error" message={error} /> : null}
|
||||||
<div className="col-lg-8 px-0">
|
<Form
|
||||||
{error ? (<div className="alert alert-danger" role="alert">{error}</div>) : null}
|
form={form}
|
||||||
<Form.Group as={Row} className="mb-3" controlId="email">
|
name="login"
|
||||||
<Form.Label column sm="4">Email</Form.Label>
|
labelCol={{
|
||||||
<Col sm="8">
|
span: 8,
|
||||||
<Form.Control
|
}}
|
||||||
type="email"
|
wrapperCol={{
|
||||||
placeholder="name@mail.ru"
|
span: 16,
|
||||||
value={email}
|
}}
|
||||||
onChange={e => setEmail(e.target.value)}
|
style={{
|
||||||
/>
|
maxWidth: 600,
|
||||||
</Col>
|
}}
|
||||||
</Form.Group>
|
|
||||||
<Form.Group as={Row} className="mb-3" controlId="password">
|
onFinish={onFinish}>
|
||||||
<Form.Label column sm="4">Пароль</Form.Label>
|
<Form.Item
|
||||||
<Col sm="8">
|
label="E-mail"
|
||||||
<Form.Control
|
name="email"
|
||||||
type="password"
|
rules={[
|
||||||
placeholder="********"
|
{
|
||||||
value={password}
|
required: true,
|
||||||
onChange={e => setPassword(e.target.value)}
|
message: 'Обязательное поле',
|
||||||
/>
|
},
|
||||||
</Col>
|
{
|
||||||
</Form.Group>
|
type: 'email',
|
||||||
<Button type="submit" size="lg">Вход</Button>
|
message: 'E-mail некорректный',
|
||||||
</div>
|
},
|
||||||
</Form>
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="name@mail.ru"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Пароль"
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Обязательное поле',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
wrapperCol={{
|
||||||
|
offset: 8,
|
||||||
|
span: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Вход
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form >
|
||||||
</>)
|
</>)
|
||||||
};
|
};
|
60
frontend/src/pages/Quests.jsx
Normal file
60
frontend/src/pages/Quests.jsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { Link, useLoaderData, useNavigate } from "react-router-dom";
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import moment from 'moment/min/moment-with-locales';
|
||||||
|
import { UserProvider } from "../store/user";
|
||||||
|
import { Avatar, List, Typography, Button, Space, Table, Card } from 'antd';
|
||||||
|
import { FireTwoTone, BuildTwoTone } from '@ant-design/icons';
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
moment.locale('ru');
|
||||||
|
const games = useLoaderData();
|
||||||
|
const { user } = UserProvider.useContainer();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<Title>Квесты</Title>
|
||||||
|
{games.map(item => renderItem(user, navigate, item))}
|
||||||
|
{!games ? (<strong>Квестов пока не анонсировано</strong>) : null}
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const renderItem = (user, navigate, item) => {
|
||||||
|
const actions = [
|
||||||
|
<Space title={`${item.points} опыта за выполнение квеста`}>Оп: {item.points}</Space>,
|
||||||
|
<Space title={`${item.taskCount} уровней в квесте`}>Ур: {item.taskCount}</Space>,
|
||||||
|
<>{moment(item.createdAt).fromNow()}</>,
|
||||||
|
<>Автор(ы) {item.authors.map(a => a.username)}</>,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
let questAction = (<span>Необходимо войти</span>)
|
||||||
|
|
||||||
|
if (!!user) {
|
||||||
|
questAction = (!!user.games.find(x => x.id === item.id)
|
||||||
|
? <span>Вы уже прошли этот квест</span>
|
||||||
|
: <Button onClick={() => navigate(`/go/${item.id}`)} type="primary">Начать квест</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={item.id}
|
||||||
|
actions={actions}
|
||||||
|
title={item.title}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
>
|
||||||
|
|
||||||
|
<Avatar src={`/api/file/${item.icon}`} style={{float:'left', margin: '4px'}} />
|
||||||
|
<Markdown
|
||||||
|
remarkPlugins={[remarkGfm]}>
|
||||||
|
{item.description}
|
||||||
|
</Markdown>
|
||||||
|
<Space>{questAction}</Space>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,89 +1,121 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Form, Button, Row, Col } from "react-bootstrap";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { UserProvider } from "../store/user";
|
import { UserProvider } from "../store/user";
|
||||||
import { ajax } from "../utils/fetch";
|
import { ajax } from "../utils/fetch";
|
||||||
|
import { Alert, Button, Form, Input } from "antd";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { user, setUser } = UserProvider.useContainer();
|
const { user, setUser } = UserProvider.useContainer();
|
||||||
const [username, setUsername] = useState("");
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [password2, setPassword2] = useState("");
|
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
user ? navigate("/") : null;
|
user ? navigate("/") : null;
|
||||||
}, [user])
|
}, [user])
|
||||||
const onRegister = (e) => {
|
const onFinish = (values) => {
|
||||||
e.preventDefault();
|
|
||||||
ajax("/api/user/register", {
|
ajax("/api/user/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ username, email, password, password2 })
|
body: JSON.stringify(values)
|
||||||
}).
|
}).
|
||||||
then(setUser).
|
then(setUser).
|
||||||
catch(({ message }) => setError(message));
|
catch(({ message }) => setError("Ошибка регистрации"));
|
||||||
}
|
}
|
||||||
return (<>
|
return (<>
|
||||||
<h1>Регистрация</h1>
|
<h1>Регистрация</h1>
|
||||||
<Form onSubmit={onRegister}>
|
{error ? <Alert type="error" message={error} /> : null}
|
||||||
<div className="col-lg-8 px-0">
|
<Form
|
||||||
{error ? (<div className="alert alert-danger" role="alert">{error}</div>) : null}
|
name="register"
|
||||||
<Form.Group as={Row} className="mb-3" controlId="username">
|
labelCol={{
|
||||||
<Form.Label column sm="4">Имя пользователя</Form.Label>
|
span: 8,
|
||||||
<Col sm="8">
|
}}
|
||||||
<Form.Control
|
wrapperCol={{
|
||||||
type="text"
|
span: 16,
|
||||||
placeholder="Имя пользователя"
|
}}
|
||||||
value={username}
|
style={{
|
||||||
onChange={e => setUsername(e.target.value)}
|
maxWidth: 600,
|
||||||
/>
|
}}
|
||||||
<Form.Text>Имя пользователя для отображения другим игрокам</Form.Text>
|
|
||||||
</Col>
|
onFinish={onFinish}>
|
||||||
</Form.Group>
|
<Form.Item
|
||||||
<Form.Group as={Row} className="mb-3" controlId="email">
|
label="Имя пользователя"
|
||||||
<Form.Label column sm="4">Email</Form.Label>
|
name="username"
|
||||||
<Col sm="8">
|
rules={[
|
||||||
<Form.Control
|
{
|
||||||
type="email"
|
required: true,
|
||||||
placeholder="name@mail.ru"
|
message: 'Обязательное поле',
|
||||||
value={email}
|
},
|
||||||
onChange={e => setEmail(e.target.value)}
|
]}
|
||||||
/>
|
>
|
||||||
<Form.Text>E-mail не виден другим игрокам</Form.Text>
|
<Input />
|
||||||
</Col>
|
</Form.Item>
|
||||||
</Form.Group>
|
<Form.Item
|
||||||
<Form.Group as={Row} className="mb-3" controlId="password">
|
label="E-mail"
|
||||||
<Form.Label column sm="4">Пароль</Form.Label>
|
name="email"
|
||||||
<Col sm="8">
|
rules={[
|
||||||
<Form.Control
|
{
|
||||||
type="password"
|
required: true,
|
||||||
placeholder="********"
|
message: 'Обязательное поле',
|
||||||
value={password}
|
},
|
||||||
onChange={e => setPassword(e.target.value)}
|
{
|
||||||
/>
|
type: 'email',
|
||||||
<Form.Text>Пароль должен быть от 8 до 16 символов</Form.Text>
|
message: 'E-mail некорректный',
|
||||||
</Col>
|
},
|
||||||
</Form.Group>
|
]}
|
||||||
<Form.Group as={Row} className="mb-3" controlId="password2">
|
help="Не видно другим пользователям"
|
||||||
<Form.Label column sm="4">Повторите пароль</Form.Label>
|
>
|
||||||
<Col sm="8">
|
<Input
|
||||||
<Form.Control
|
type="email"
|
||||||
type="password"
|
placeholder="name@mail.ru"
|
||||||
placeholder="********"
|
/>
|
||||||
value={password2}
|
</Form.Item>
|
||||||
onChange={e => setPassword2(e.target.value)}
|
<Form.Item
|
||||||
/>
|
label="Пароль"
|
||||||
<Form.Text>Введите пароль заново, чтобы избежать опечаток</Form.Text>
|
name="password"
|
||||||
</Col>
|
rules={[
|
||||||
</Form.Group>
|
{
|
||||||
<Button type="submit" size="lg">Регистрация</Button>
|
required: true,
|
||||||
</div>
|
message: 'Обязательное поле',
|
||||||
</Form>
|
},
|
||||||
</>);
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Повторите пароль"
|
||||||
|
name="password2"
|
||||||
|
dependencies={['password']}
|
||||||
|
hasFeedback
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Обязательное поле',
|
||||||
|
},
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue('password') === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('Пароли отличаются!'));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
wrapperCol={{
|
||||||
|
offset: 8,
|
||||||
|
span: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Регистрация
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form >
|
||||||
|
</>)
|
||||||
}
|
}
|
|
@ -7,5 +7,4 @@ export const ajax = async (path, params) => {
|
||||||
return r
|
return r
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.catch(() => null)
|
|
||||||
}
|
}
|
||||||
|
|
6
main.go
6
main.go
|
@ -54,6 +54,7 @@ func main() {
|
||||||
&models.Task{},
|
&models.Task{},
|
||||||
&models.Solution{},
|
&models.Solution{},
|
||||||
&models.Code{},
|
&models.Code{},
|
||||||
|
&models.File{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error DB migration\n: %s", err)
|
fmt.Fprintf(os.Stderr, "Error DB migration\n: %s", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -63,6 +64,7 @@ func main() {
|
||||||
userService := service.NewUser(db)
|
userService := service.NewUser(db)
|
||||||
gameService := service.NewGame(db)
|
gameService := service.NewGame(db)
|
||||||
engineService := service.NewEngine(db)
|
engineService := service.NewEngine(db)
|
||||||
|
uploadService := service.NewFile(db)
|
||||||
|
|
||||||
// --[ HTTP server ]--
|
// --[ HTTP server ]--
|
||||||
|
|
||||||
|
@ -130,6 +132,9 @@ func main() {
|
||||||
Admin: &controller.Admin{
|
Admin: &controller.Admin{
|
||||||
GameService: gameService,
|
GameService: gameService,
|
||||||
},
|
},
|
||||||
|
File: &controller.File{
|
||||||
|
FileService: uploadService,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
codegen := e.Group("")
|
codegen := e.Group("")
|
||||||
|
|
||||||
|
@ -161,4 +166,5 @@ type serverRouter struct {
|
||||||
*controller.User
|
*controller.User
|
||||||
*controller.Engine
|
*controller.Engine
|
||||||
*controller.Admin
|
*controller.Admin
|
||||||
|
*controller.File
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
@ -32,9 +33,15 @@ func (a *Admin) CreateGame(ctx echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.JSON(http.StatusCreated, api.GameResponse{
|
return ctx.JSON(http.StatusCreated, api.GameResponse{
|
||||||
Id: game.ID,
|
Authors: make([]api.UserView, 0, len(game.Authors)),
|
||||||
Title: game.Title,
|
CreatedAt: game.CreatedAt.Format(time.RFC3339),
|
||||||
Description: game.Description,
|
Description: game.Description,
|
||||||
|
Icon: game.IconID,
|
||||||
|
Id: game.ID,
|
||||||
|
Points: game.Points,
|
||||||
|
TaskCount: len(game.Tasks),
|
||||||
|
Title: game.Title,
|
||||||
|
Type: api.MapGameTypeReverse(game.Type),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +59,7 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User)
|
||||||
Type: api.MapGameType(req.Type),
|
Type: api.MapGameType(req.Type),
|
||||||
Tasks: make([]*models.Task, 0, len(req.Tasks)),
|
Tasks: make([]*models.Task, 0, len(req.Tasks)),
|
||||||
Points: req.Points,
|
Points: req.Points,
|
||||||
|
IconID: req.Icon,
|
||||||
}
|
}
|
||||||
for order, te := range req.Tasks {
|
for order, te := range req.Tasks {
|
||||||
task := &models.Task{
|
task := &models.Task{
|
||||||
|
|
50
pkg/controller/file.go
Normal file
50
pkg/controller/file.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"gitrepo.ru/neonxp/nquest/api"
|
||||||
|
"gitrepo.ru/neonxp/nquest/pkg/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
FileService *service.File
|
||||||
|
}
|
||||||
|
|
||||||
|
// (POST /file/upload)
|
||||||
|
func (u *File) UploadFile(c echo.Context) error {
|
||||||
|
// user := contextlib.GetUser(c)
|
||||||
|
fh, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fo, err := fh.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := u.FileService.Upload(
|
||||||
|
c.Request().Context(),
|
||||||
|
fh.Filename,
|
||||||
|
fh.Header.Get("Content-Type"),
|
||||||
|
int(fh.Size),
|
||||||
|
fo,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(200, &api.UploadResponse{
|
||||||
|
Uuid: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// (GET /file/{uid})
|
||||||
|
func (u *File) GetFile(c echo.Context, uid uuid.UUID) error {
|
||||||
|
f, err := u.FileService.GetFile(c.Request().Context(), uid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Blob(200, f.ContentType, f.Body)
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ func (g *Game) GetGames(ctx echo.Context) error {
|
||||||
TaskCount: len(game.Tasks),
|
TaskCount: len(game.Tasks),
|
||||||
Authors: make([]api.UserView, 0, len(game.Authors)),
|
Authors: make([]api.UserView, 0, len(game.Authors)),
|
||||||
CreatedAt: game.CreatedAt.Format(time.RFC3339),
|
CreatedAt: game.CreatedAt.Format(time.RFC3339),
|
||||||
|
Icon: game.IconID,
|
||||||
}
|
}
|
||||||
for _, u := range game.Authors {
|
for _, u := range game.Authors {
|
||||||
gv.Authors = append(gv.Authors, api.UserView{
|
gv.Authors = append(gv.Authors, api.UserView{
|
||||||
|
|
10
pkg/models/file.go
Normal file
10
pkg/models/file.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Model
|
||||||
|
|
||||||
|
Filename string
|
||||||
|
ContentType string
|
||||||
|
Size int
|
||||||
|
Body []byte `gorm:"type:bytea"`
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
Model
|
Model
|
||||||
|
|
||||||
|
@ -10,6 +12,8 @@ type Game struct {
|
||||||
Authors []*User `gorm:"many2many:game_authors"`
|
Authors []*User `gorm:"many2many:game_authors"`
|
||||||
Type GameType
|
Type GameType
|
||||||
Points int
|
Points int
|
||||||
|
Icon *File
|
||||||
|
IconID uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
type GameType int
|
type GameType int
|
||||||
|
|
49
pkg/service/file.go
Normal file
49
pkg/service/file.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mime/multipart"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFile(db *gorm.DB) *File {
|
||||||
|
return &File{
|
||||||
|
DB: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *File) Upload(
|
||||||
|
ctx context.Context,
|
||||||
|
filename string,
|
||||||
|
contentType string,
|
||||||
|
size int,
|
||||||
|
r multipart.File,
|
||||||
|
) (uuid.UUID, error) {
|
||||||
|
buf := make([]byte, size)
|
||||||
|
if _, err := r.Read(buf); err != nil {
|
||||||
|
return uuid.UUID{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := &models.File{
|
||||||
|
Model: models.Model{ID: uuid.New()},
|
||||||
|
Filename: filename,
|
||||||
|
ContentType: contentType,
|
||||||
|
Size: size,
|
||||||
|
Body: buf,
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.ID, u.DB.WithContext(ctx).Create(file).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *File) GetFile(ctx context.Context, uid uuid.UUID) (*models.File, error) {
|
||||||
|
f := new(models.File)
|
||||||
|
|
||||||
|
return f, u.DB.WithContext(ctx).First(f, uid).Error
|
||||||
|
}
|
|
@ -85,6 +85,8 @@ func (s *User) Login(ctx context.Context, email, password string) (*models.User,
|
||||||
nemail := normalizer.NewNormalizer().Normalize(email)
|
nemail := normalizer.NewNormalizer().Normalize(email)
|
||||||
err := s.DB.
|
err := s.DB.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
|
Preload("Games", `Finish = true`).
|
||||||
|
Preload("Games.Game").
|
||||||
Where("email = ?", nemail).
|
Where("email = ?", nemail).
|
||||||
First(u).
|
First(u).
|
||||||
Error
|
Error
|
||||||
|
|
|
@ -33,14 +33,15 @@ POST http://localhost:8000/api/games
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"title": "Тестовая игра 2",
|
"icon": "f343919a-b068-4d72-ade5-bbb84db0bec9",
|
||||||
"description": "Описание тестовой игры",
|
"title": "Тестовая игра 7",
|
||||||
|
"description": "A paragraph with *emphasis* and **strong importance**.\n\n> A block quote with ~strikethrough~ and a URL: https://reactjs.org.\n\n* Lists\n* [ ] todo\n* [x] done\n\nA table:\n\n\n| a | b |\n| - | - |\n",
|
||||||
"type": "city",
|
"type": "city",
|
||||||
"points": 500,
|
"points": 500,
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"title": "Задание 1",
|
"title": "Задание 1",
|
||||||
"text": "Текст первого задания.\n\n*Коды: `nq1111`*",
|
"text": "A paragraph with *emphasis* and **strong importance**.\n\n> A block quote with ~strikethrough~ and a URL: https://reactjs.org.\n\n* Lists\n* [ ] todo\n* [x] done\n\nA table:\n\n\n| a | b |\n| - | - |\n",
|
||||||
"codes": [
|
"codes": [
|
||||||
{
|
{
|
||||||
"description": "1+",
|
"description": "1+",
|
||||||
|
@ -234,3 +235,21 @@ Content-Type: application/json
|
||||||
{
|
{
|
||||||
"code": "NQ3333"
|
"code": "NQ3333"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8000/api/file/upload
|
||||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="file"; filename="icon.jpg"
|
||||||
|
Content-Type: image/jpg
|
||||||
|
|
||||||
|
< /home/neonxp/logo.jpg
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:8000/api/file/f343919a-b068-4d72-ade5-bbb84db0bec9
|
Loading…
Reference in a new issue