фиксы и статистика игроков
This commit is contained in:
parent
1ad012b8fa
commit
392b6df0d5
15 changed files with 425 additions and 28 deletions
7
.env
Normal file
7
.env
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
POSTGRES_HOSTNAME=db
|
||||||
|
POSTGRES_DB=nquest
|
||||||
|
POSTGRES_USER=nquest
|
||||||
|
POSTGRES_PASSWORD=nquest
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
LISTEN=:8000
|
||||||
|
SECRET=d51a5056-c8ad-4c2a-939e-c60cff9c1214
|
|
@ -1,8 +1,7 @@
|
||||||
{
|
|
||||||
debug
|
|
||||||
}
|
|
||||||
|
|
||||||
http:// {
|
http:// {
|
||||||
|
header /* {
|
||||||
|
Cache-Control: no-cache, no-store, must-revalidate
|
||||||
|
}
|
||||||
handle_path /file/* {
|
handle_path /file/* {
|
||||||
root * /app/store
|
root * /app/store
|
||||||
file_server
|
file_server
|
||||||
|
|
|
@ -66,6 +66,19 @@ components:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/userView'
|
$ref: '#/components/schemas/userView'
|
||||||
description: ""
|
description: ""
|
||||||
|
userShortResponse:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
$ref: '#/components/schemas/userShortView'
|
||||||
|
description: ""
|
||||||
|
usersListResponse:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/userShortView'
|
||||||
|
type: array
|
||||||
|
description: ""
|
||||||
schemas:
|
schemas:
|
||||||
codeEdit:
|
codeEdit:
|
||||||
properties:
|
properties:
|
||||||
|
@ -213,6 +226,37 @@ components:
|
||||||
- text
|
- text
|
||||||
- codes
|
- codes
|
||||||
type: object
|
type: object
|
||||||
|
userShortView:
|
||||||
|
properties:
|
||||||
|
expToCurrentLevel:
|
||||||
|
type: integer
|
||||||
|
expToNextLevel:
|
||||||
|
type: integer
|
||||||
|
experience:
|
||||||
|
type: integer
|
||||||
|
games:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/gameView'
|
||||||
|
type: array
|
||||||
|
gamesFinished:
|
||||||
|
type: integer
|
||||||
|
gamesStarted:
|
||||||
|
type: integer
|
||||||
|
id:
|
||||||
|
format: uuid
|
||||||
|
type: string
|
||||||
|
level:
|
||||||
|
type: integer
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- username
|
||||||
|
- experience
|
||||||
|
- level
|
||||||
|
- gamesStarted
|
||||||
|
- gamesFinished
|
||||||
|
type: object
|
||||||
userView:
|
userView:
|
||||||
properties:
|
properties:
|
||||||
email:
|
email:
|
||||||
|
@ -457,6 +501,26 @@ paths:
|
||||||
400:
|
400:
|
||||||
$ref: '#/components/responses/errorResponse'
|
$ref: '#/components/responses/errorResponse'
|
||||||
security: []
|
security: []
|
||||||
|
/users:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: '#/components/responses/usersListResponse'
|
||||||
|
description: users list
|
||||||
|
/users/{uid}:
|
||||||
|
get:
|
||||||
|
operationId: getUserInfo
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: uid
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
format: uuid
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: '#/components/responses/userShortResponse'
|
||||||
|
description: users list
|
||||||
security:
|
security:
|
||||||
- cookieAuth: []
|
- cookieAuth: []
|
||||||
servers:
|
servers:
|
||||||
|
|
|
@ -64,3 +64,16 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/fileItem"
|
$ref: "#/components/schemas/fileItem"
|
||||||
|
usersListResponse:
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/userShortView"
|
||||||
|
userShortResponse:
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
$ref: "#/components/schemas/userShortView"
|
|
@ -1,5 +1,36 @@
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
|
userShortView:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
experience:
|
||||||
|
type: integer
|
||||||
|
level:
|
||||||
|
type: integer
|
||||||
|
gamesStarted:
|
||||||
|
type: integer
|
||||||
|
gamesFinished:
|
||||||
|
type: integer
|
||||||
|
expToCurrentLevel:
|
||||||
|
type: integer
|
||||||
|
expToNextLevel:
|
||||||
|
type: integer
|
||||||
|
games:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/gameView"
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- username
|
||||||
|
- experience
|
||||||
|
- level
|
||||||
|
- gamesStarted
|
||||||
|
- gamesFinished
|
||||||
userView:
|
userView:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
@ -55,3 +55,23 @@ paths:
|
||||||
description: "success logout"
|
description: "success logout"
|
||||||
400:
|
400:
|
||||||
$ref: "#/components/responses/errorResponse"
|
$ref: "#/components/responses/errorResponse"
|
||||||
|
/users:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: "#/components/responses/usersListResponse"
|
||||||
|
description: "users list"
|
||||||
|
/users/{uid}:
|
||||||
|
get:
|
||||||
|
operationId: getUserInfo
|
||||||
|
parameters:
|
||||||
|
- name: uid
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: "#/components/responses/userShortResponse"
|
||||||
|
description: "users list"
|
|
@ -60,6 +60,12 @@ type ServerInterface interface {
|
||||||
|
|
||||||
// (POST /user/register)
|
// (POST /user/register)
|
||||||
PostUserRegister(ctx echo.Context) error
|
PostUserRegister(ctx echo.Context) error
|
||||||
|
|
||||||
|
// (GET /users)
|
||||||
|
GetUsers(ctx echo.Context) error
|
||||||
|
|
||||||
|
// (GET /users/{uid})
|
||||||
|
GetUserInfo(ctx echo.Context, uid openapi_types.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerInterfaceWrapper converts echo contexts to parameters.
|
// ServerInterfaceWrapper converts echo contexts to parameters.
|
||||||
|
@ -248,6 +254,35 @@ func (w *ServerInterfaceWrapper) PostUserRegister(ctx echo.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsers converts echo context to params.
|
||||||
|
func (w *ServerInterfaceWrapper) GetUsers(ctx echo.Context) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx.Set(CookieAuthScopes, []string{})
|
||||||
|
|
||||||
|
// Invoke the callback with all the unmarshaled arguments
|
||||||
|
err = w.Handler.GetUsers(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo converts echo context to params.
|
||||||
|
func (w *ServerInterfaceWrapper) GetUserInfo(ctx echo.Context) error {
|
||||||
|
var err error
|
||||||
|
// ------------- Path parameter "uid" -------------
|
||||||
|
var uid openapi_types.UUID
|
||||||
|
|
||||||
|
err = runtime.BindStyledParameterWithOptions("simple", "uid", ctx.Param("uid"), &uid, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||||
|
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.GetUserInfo(ctx, uid)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// This is a simple interface which specifies echo.Route addition functions which
|
// This is a simple interface which specifies echo.Route addition functions which
|
||||||
// are present on both echo.Echo and echo.Group, since we want to allow using
|
// are present on both echo.Echo and echo.Group, since we want to allow using
|
||||||
// either of them for path registration
|
// either of them for path registration
|
||||||
|
@ -289,33 +324,36 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||||
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)
|
||||||
router.POST(baseURL+"/user/register", wrapper.PostUserRegister)
|
router.POST(baseURL+"/user/register", wrapper.PostUserRegister)
|
||||||
|
router.GET(baseURL+"/users", wrapper.GetUsers)
|
||||||
|
router.GET(baseURL+"/users/:uid", wrapper.GetUserInfo)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"H4sIAAAAAAAC/8xY30/kthP/V77yt48p2TtOqpQ3iig6FZ3aK9cXtEImGRYfiR3sCccW5X+vxk6yycb5",
|
"H4sIAAAAAAAC/9RZX2+cOBD/KiffPXJh+0c6ad9yURpFF1V3TXov0SpyYLLrBmximzR7Ed/9NDawsNjA",
|
||||||
"wZJy98SSTObHZz4znvEzi1WWKwkSDYuemQaTK2nA/gNaK/25ekIPYiURJNJPnuepiDkKJcOvRkl6ZuI7",
|
"Epq2T93CMH9+85uxZ/JMIpFmggPXiiyfiQSVCa7A/AekFPJT+QQfRIJr4Bp/0ixLWEQ1Ezz8ogTHZyra",
|
||||||
"yDj9yrXKQaNwemKV2M9xmwOLmJAIG9CsDFgGxvBN+6VBLeSGlWXANDwUQkPCoiunYie/Dmp5dfMVYmQl",
|
"QErxVyZFBlIzqycSsflcbzMgS8K4hjVIUgQkBaXouvlSacn4mhRFQCQ85ExCTJbXVsVOfhVU8uL2C0Sa",
|
||||||
"fZCAibXIyScWMdJ/K1IwF8LgQVEIhMwG8JOGWxax/4c7sEInZkIy8REhI3OVT1xrvh1yacMzOEkyIQ9y",
|
"FPhBDCqSLEOfyJKg/juWgLpgSk+KgmlITQC/SbgjS/JruAMrtGIqRBPnGlI0V/pEpaRbn0trmsJxnDI+",
|
||||||
"acwT0nyWCByz/N9iQRb+FvBtLhbIzf3iMJBS54TfaJGniicL0LooREJ/b5XOOLLIPQgmmGyF5vK3MKAX",
|
"yaU+T1Dzacx0n+VviwVa+JfB17FYaKruZ4cBlVon3EbzLBE0noHWec5i/PdOyJRqsrQPggEmG6Gx/M0V",
|
||||||
"B4iUDgNUBpWapnQtp6aKuo52T6Hn/SGgWWN90AL7wsaylH+lx0hT4j0js2IJmNJiIyRPP/HM75MR/3g7",
|
"yNkBQqW9ACmQlxshDyHKkD2jrzTqs/mNC3XPjTEMLYLSSN3GTH0NNbgq83sKHe+nEMgY6xIoMC9MbHP5",
|
||||||
"5B4MVntHWfWpD5qmHfS8nsxQ7F5MxjUz/FyJ6njpnwBUrWZ2gyFp1+L2G0zAUGDqR9c9mG5dlyRXBuxR",
|
"VziM1O2uY2RULAERkq0Zp8lHmrp9Uuw/52mxB4PR3lJWfuqCpm6NHa8HMxTZF4NxjQw/E6w8arunIXYu",
|
||||||
"GHHT0XWjVApc9hJSS9bWu+yqLNdBNkAMZeuy8hNkkTntGguesoDFAretz3ahNR23l2Re4J3S86HddYU+",
|
"NZrPKG3b/T6VA6KZTtzo2gfDbfwK5YqAPDLFblu6boVIgPJOQirJynqbXaXlKsgaCF+2rko/geep1S51",
|
||||||
"tLEGjpCc4GEF/8Z0OlWFxIHX34EjLogxelQBtb1vYx40ufQRpykJbwecn/+m03vyPzMxCE9+hgzBPoaU",
|
"ThMSkIjpbeOzXWj16dNJMs31Rkh1UKtwd4mARBKohvhYTyv4V6bTici59rz+DhyxQfTRowyo6X0T86DO",
|
||||||
"VRZUQQwFPtz6Xxb4EPFb82ldlOr+uhpGhXzkqUjqf1Xa/JTwhNcpPALVLrHmmuymgOAt4tfiNhuypsZ7",
|
"pYs4dUk4O+D4/Ned3pH/kYnR8ORmiA/2PqSMsqAMwhe4v/UfFriP+I27elWU4v6mvJgz/kgTFlf/FUn9",
|
||||||
"kEHGRer1AZ7yS3VaaA0SL2xI3sqyYp/gaUIGtAAZD2wDhJRZYNicTdh02Fet0k7aCbq6KhX94jTDe9NJ",
|
"k8OTvkngEbB2kTU3aDcBDc4ifiluoyFrXwc6uMFTdiVOcimB6wvjvbOIjNhHeBqQAcmAR54hCEFRM9yx",
|
||||||
"otJ/0vsI34gHVRY6QNVO+hLRQ72GsHLfM2UGzEBcaIHbvwjGul7UvYCTAu8s+DQHukdEZRsIM2BMq2dF",
|
"S00fGGdqA3GPsUtNpfZJjCR44g8YoeXu493F8lq8BVVlYc/l/SB9qfVkNaUscdLrp8z3y1MlRdKqaISu",
|
||||||
"jOfid6gGeyFvlQ3W0ZXJPwswxMRH0MbNle+OVkcrOwrlIHkuWMSOj94draj7cbyzboQWUrtQhc8PpKKk",
|
"argCf1EcVZ2V+oIsmyx4st1NRAf1CsLSfccwFRAFUS6Z3l4ijFUrFPcMjnO9MeDjFd8+wi5lAiEKlGoc",
|
||||||
"xxuwxUFstdPux4RFzG5QtMz8Rhue1aJ5Bgh04l1VQZDmXQgPlVO7BKAuIGgNzVMz6Tro7sfvV6shkjZy",
|
"R0tCM/YXlNMB43fCBGs7EeH/5KCwyTyCVHZkeHO0OFqYW24GnGaMLMm7ozdHCzzYqN4YN0IDqdkbhM8P",
|
||||||
"YX8FbWfBOtvG/6pPsXJNX3jQCd0qY6tZmSGUvlghwuktYbJqflXJdm9tyYoURc41hqTm54SjZ7OiIDuW",
|
"qKLAx2swfQ/Zasac85gsiVkU4Fj0gSUm4IxKmoIGvMxcl0Gg5l0ID6VTuwRomUPQmJaGxo1V0F4DvV0s",
|
||||||
"boTkeutdEzx71MuTtLcTvipDTRcbJ+55Xakvdra3x9ugRxhAx/q56zDDiVnm3qE8NKDulcjrExA+FyKZ",
|
"fCSt5cLupqWZBeNsE//rLsWKFX7hQCe0E7upZqF8KH02QojTa8Jk1Pwp4u3evJrmiWYZlTpENb/HVDsW",
|
||||||
"6B/nUMMyXRaO6m/cOxYDBuRGSJjAhLA4s4I/LiKdGyPHt05wYb32+8vhTCLoUzekvVGEh9XbrLuMeZck",
|
"CBhky9It41RunROgY11weJL2Vh8vylDdxfqJe1ZV6sHOdtZVJugeBuCN7cx2GH9i5lmvFVMDam/+Xp6A",
|
||||||
"BxalD2l3+IyTCHD2UbMgg0j8ePVL73aDaUiErmefFgqySFMb03i7rlrEco2aTNpZcsTiFzdrHnCMta8L",
|
"8Dln8UD/OIMKluGysFR/5d4xGzDA14zDACaIxakR/HERaS1GLd9awYXVRsddDqdcgzyx9+9XinBavY1a",
|
||||||
"y4B9WB1Pf9S95W+5GKZqI+RwMf2hjHX1wootxfXhTSTnxnxTOpmuhHqObr5YrCr6CK9ejnCnha87eKsC",
|
"U43bf00sShfS9vDpJxHo0UfNjAxC8XeLPzqLKyIhZrK6+zRQ4HmSmJj623XZIuZr1GjS3CV7LH62d80J",
|
||||||
"ZwFOcj3/P/Tpb4o4BmP+V6k+1OOdjxo2wqDj77iXn2vJ78qM3cv33rfzlzXPntbYbVv5sbk2Oj+sqaUa",
|
"x1hzK14E5P3i3fBH7T9mNVwME7Fm3F9MfwtlXL0wYnNx3T+JZFSpr0LGw5VQ3aPrL2arii7Ci8MRbrXw",
|
||||||
"0I910y50yiIW0sJWrst/AwAA//8rjKvDLRwAAA==",
|
"VQtvketRgKNcx//3XfqrPIpAqV9K1VM93vkoYc2Utvzt9/JTJfldmbF7+db5dvyw5pjTartNKz8F19RQ",
|
||||||
|
"/1GTG5DytTw1fHSg5XOcJn/YC0j3b4CFDdF/K1uhJQXysYollwlZkhDH4GJV/B8AAP//bSQu1mogAAA=",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|
19
api/types.go
19
api/types.go
|
@ -101,6 +101,19 @@ type TaskView struct {
|
||||||
// TaskViewMessage defines model for TaskView.Message.
|
// TaskViewMessage defines model for TaskView.Message.
|
||||||
type TaskViewMessage string
|
type TaskViewMessage string
|
||||||
|
|
||||||
|
// UserShortView defines model for userShortView.
|
||||||
|
type UserShortView struct {
|
||||||
|
ExpToCurrentLevel *int `json:"expToCurrentLevel,omitempty"`
|
||||||
|
ExpToNextLevel *int `json:"expToNextLevel,omitempty"`
|
||||||
|
Experience int `json:"experience"`
|
||||||
|
Games *[]GameView `json:"games,omitempty"`
|
||||||
|
GamesFinished int `json:"gamesFinished"`
|
||||||
|
GamesStarted int `json:"gamesStarted"`
|
||||||
|
Id openapi_types.UUID `json:"id"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
// UserView defines model for userView.
|
// UserView defines model for userView.
|
||||||
type UserView struct {
|
type UserView struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
@ -143,6 +156,12 @@ type UploadResponse struct {
|
||||||
// UserResponse defines model for userResponse.
|
// UserResponse defines model for userResponse.
|
||||||
type UserResponse = UserView
|
type UserResponse = UserView
|
||||||
|
|
||||||
|
// UserShortResponse defines model for userShortResponse.
|
||||||
|
type UserShortResponse interface{}
|
||||||
|
|
||||||
|
// UsersListResponse defines model for usersListResponse.
|
||||||
|
type UsersListResponse = []UserShortView
|
||||||
|
|
||||||
// AdminUploadFileMultipartBody defines parameters for AdminUploadFile.
|
// AdminUploadFileMultipartBody defines parameters for AdminUploadFile.
|
||||||
type AdminUploadFileMultipartBody interface{}
|
type AdminUploadFileMultipartBody interface{}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ import User from './pages/User'
|
||||||
import { useRole } from './utils/roles'
|
import { useRole } from './utils/roles'
|
||||||
import EditQuest from './pages/admin/Quest'
|
import EditQuest from './pages/admin/Quest'
|
||||||
import AdminQuest from './pages/admin/Quests'
|
import AdminQuest from './pages/admin/Quests'
|
||||||
|
import Users from './pages/Users'
|
||||||
|
import UserView from './pages/UserView'
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
|
@ -35,6 +37,18 @@ const router = createBrowserRouter(
|
||||||
element={<Auth><Quests /></Auth>}
|
element={<Auth><Quests /></Auth>}
|
||||||
loader={() => ajax('/api/games').catch(x => { console.log(x); return null })}
|
loader={() => ajax('/api/games').catch(x => { console.log(x); return null })}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
id="users"
|
||||||
|
path="/users"
|
||||||
|
element={<Auth><Users /></Auth>}
|
||||||
|
loader={() => ajax('/api/users').catch(x => { console.log(x); return null })}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
id="user_view"
|
||||||
|
path="/users/:userId"
|
||||||
|
element={<Auth><UserView /></Auth>}
|
||||||
|
loader={({ params }) => ajax(`/api/users/${params.userId}`).catch(x => { console.log(x); return null })}
|
||||||
|
/>
|
||||||
<Route path="me" element={<User />} />
|
<Route path="me" element={<User />} />
|
||||||
<Route path="login" element={<Login />} />
|
<Route path="login" element={<Login />} />
|
||||||
<Route path="register" element={<Register />} />
|
<Route path="register" element={<Register />} />
|
||||||
|
|
|
@ -35,6 +35,11 @@ const AppLayout = () => {
|
||||||
label: 'Квесты',
|
label: 'Квесты',
|
||||||
link: '/quests'
|
link: '/quests'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'users',
|
||||||
|
label: 'Игроки',
|
||||||
|
link: '/users'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'me',
|
key: 'me',
|
||||||
label: `${user.username} [${user.level}]`,
|
label: `${user.username} [${user.level}]`,
|
||||||
|
|
34
frontend/src/pages/UserView.jsx
Normal file
34
frontend/src/pages/UserView.jsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { Avatar, Popover, Progress, Space, Typography } from 'antd'
|
||||||
|
import Markdown from 'react-markdown'
|
||||||
|
import { useLoaderData } from 'react-router-dom'
|
||||||
|
|
||||||
|
const { Paragraph } = Typography
|
||||||
|
|
||||||
|
const UserView = () => {
|
||||||
|
const user = useLoaderData()
|
||||||
|
if (user == null) {
|
||||||
|
return (<Space>Загрузка...</Space>)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<h1>{user.username}</h1>
|
||||||
|
<Paragraph>Уровень: {user.level} ур</Paragraph>
|
||||||
|
<Paragraph>Очков опыта: {user.experience} ОО</Paragraph>
|
||||||
|
<Paragraph>Следующий уровень: {user.expToNextLevel} ОО</Paragraph>
|
||||||
|
<Progress
|
||||||
|
value={user.experience}
|
||||||
|
percent={((user.experience - user.expToCurrentLevel) / (user.expToNextLevel - user.expToCurrentLevel) * 100)}
|
||||||
|
size="small"
|
||||||
|
showInfo={false}
|
||||||
|
/>
|
||||||
|
{user.games.map(item => <Popover
|
||||||
|
key={item.id}
|
||||||
|
title={item.title}
|
||||||
|
content={<Markdown>{item.description}</Markdown>}
|
||||||
|
>
|
||||||
|
<Avatar size={64} style={{ marginRight: 4, marginBottom: 4 }} key={item.id} src={`/api/file/${item.icon}`} />
|
||||||
|
</Popover>)}
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserView
|
44
frontend/src/pages/Users.jsx
Normal file
44
frontend/src/pages/Users.jsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Table, Typography } from 'antd'
|
||||||
|
import { Link, useLoaderData } from 'react-router-dom'
|
||||||
|
|
||||||
|
const { Title } = Typography
|
||||||
|
|
||||||
|
const Users = () => {
|
||||||
|
const users = useLoaderData()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>Статистика игроков</Title>
|
||||||
|
<Table
|
||||||
|
dataSource={users}
|
||||||
|
rowKey={'id'}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: 'Ник в игре',
|
||||||
|
dataIndex: 'username',
|
||||||
|
key: 'username',
|
||||||
|
render: (username, q) => <Link to={`/users/${q.id}`}>{username}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ОО',
|
||||||
|
dataIndex: 'experience',
|
||||||
|
key: 'experience'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Уровень',
|
||||||
|
dataIndex: 'level',
|
||||||
|
key: 'level'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Квесты начатые / Пройденные',
|
||||||
|
dataIndex: 'gamesStarted',
|
||||||
|
key: 'gamesStarted',
|
||||||
|
render: (gamesStarted, q) => (<>{gamesStarted} / {q.gamesFinished}</>)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default Users
|
|
@ -11,6 +11,7 @@ const { Title } = Typography
|
||||||
const Quest = () => {
|
const Quest = () => {
|
||||||
let { quest, files } = useLoaderData()
|
let { quest, files } = useLoaderData()
|
||||||
const [error, setError] = useState()
|
const [error, setError] = useState()
|
||||||
|
const [info, setInfo] = useState()
|
||||||
if (!quest) {
|
if (!quest) {
|
||||||
quest = {
|
quest = {
|
||||||
type: 'city',
|
type: 'city',
|
||||||
|
@ -45,6 +46,21 @@ const Quest = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFinish = (values) => {
|
const onFinish = (values) => {
|
||||||
|
values.tasks = values.tasks.map(task => {
|
||||||
|
if (!task.id) {
|
||||||
|
task.id = uuidv4()
|
||||||
|
}
|
||||||
|
task.codes = task.codes.map(code => {
|
||||||
|
if (!code.id) {
|
||||||
|
code.id = uuidv4()
|
||||||
|
}
|
||||||
|
|
||||||
|
return code
|
||||||
|
})
|
||||||
|
|
||||||
|
return task
|
||||||
|
})
|
||||||
|
console.log(values)
|
||||||
ajax('/api/admin/games', {
|
ajax('/api/admin/games', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -53,14 +69,24 @@ const Quest = () => {
|
||||||
},
|
},
|
||||||
body: JSON.stringify(values)
|
body: JSON.stringify(values)
|
||||||
})
|
})
|
||||||
.then(g => navigate(`/admin/quests/${g.id}/`))
|
.then(g => {
|
||||||
.catch(({ message }) => setError('Ошибка создания'))
|
navigate(`/admin/quests/${g.id}/`)
|
||||||
|
setError('')
|
||||||
|
setInfo('Сохранено')
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.catch(({ message }) => {
|
||||||
|
setInfo('')
|
||||||
|
setError('Ошибка создания:' + message)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>{quest.title ? (quest.title) : ('Новый квест')}</Title>
|
<Title>{quest.title ? (quest.title) : ('Новый квест')}</Title>
|
||||||
{error ? <Alert type="error" message={error} /> : null}
|
{error ? <Alert type="error" message={error} /> : null}
|
||||||
|
{info ? <Alert type="info" message={info} /> : null}
|
||||||
<Row gutter={8}>
|
<Row gutter={8}>
|
||||||
<Col xs={24} sm={16} md={16}>
|
<Col xs={24} sm={16} md={16}>
|
||||||
<Form
|
<Form
|
||||||
|
@ -146,6 +172,7 @@ const Quest = () => {
|
||||||
footer={<Button onClick={() => setPreview(false)}>Закрыть</Button>}
|
footer={<Button onClick={() => setPreview(false)}>Закрыть</Button>}
|
||||||
width={'80%'}
|
width={'80%'}
|
||||||
centered
|
centered
|
||||||
|
onCancel={() => setPreview(false)}
|
||||||
>
|
>
|
||||||
<List dataSource={fields.tasks} renderItem={(task) => (
|
<List dataSource={fields.tasks} renderItem={(task) => (
|
||||||
<List.Item key={task.id}>
|
<List.Item key={task.id}>
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/labstack/echo-contrib/session"
|
"github.com/labstack/echo-contrib/session"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
@ -94,6 +96,74 @@ func (u *User) GetUser(c echo.Context) error {
|
||||||
return mapUser(c, user)
|
return mapUser(c, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (GET /users)
|
||||||
|
func (u *User) GetUsers(c echo.Context) error {
|
||||||
|
list, err := u.UserService.List(c.Request().Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(api.UsersListResponse, 0, len(list))
|
||||||
|
|
||||||
|
for _, u := range list {
|
||||||
|
level := utils.ExpToLevel(u.Experience)
|
||||||
|
finished := 0
|
||||||
|
for _, g := range u.Games {
|
||||||
|
if g.Finish {
|
||||||
|
finished++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, api.UserShortView{
|
||||||
|
Experience: u.Experience,
|
||||||
|
GamesFinished: finished,
|
||||||
|
GamesStarted: len(u.Games),
|
||||||
|
Id: u.ID,
|
||||||
|
Level: level,
|
||||||
|
Username: u.Username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(200, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// (GET /users/{uid})
|
||||||
|
func (s *User) GetUserInfo(c echo.Context, uid uuid.UUID) error {
|
||||||
|
user, err := s.UserService.GetUserByID(c.Request().Context(), uid)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return echo.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
level := utils.ExpToLevel(user.Experience)
|
||||||
|
expToCur := utils.LevelToExp(level)
|
||||||
|
expToNext := utils.LevelToExp(level + 1)
|
||||||
|
|
||||||
|
games := make([]api.GameView, 0, len(user.Games))
|
||||||
|
for _, gc := range user.Games {
|
||||||
|
games = append(games, api.GameView{
|
||||||
|
Id: gc.GameID,
|
||||||
|
Title: gc.Game.Title,
|
||||||
|
Description: gc.Game.Description,
|
||||||
|
Type: api.MapGameTypeReverse(gc.Game.Type),
|
||||||
|
Icon: gc.Game.IconID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &api.UserShortView{
|
||||||
|
ExpToCurrentLevel: &expToCur,
|
||||||
|
ExpToNextLevel: &expToNext,
|
||||||
|
Experience: user.Experience,
|
||||||
|
Games: &games,
|
||||||
|
GamesFinished: 0,
|
||||||
|
GamesStarted: 0,
|
||||||
|
Id: user.ID,
|
||||||
|
Level: level,
|
||||||
|
Username: user.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(200, res)
|
||||||
|
}
|
||||||
|
|
||||||
func setUser(c echo.Context, user *models.User) error {
|
func setUser(c echo.Context, user *models.User) error {
|
||||||
sess, err := session.Get("session", c)
|
sess, err := session.Get("session", c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -134,6 +204,7 @@ func mapUser(c echo.Context, user *models.User) error {
|
||||||
Title: gc.Game.Title,
|
Title: gc.Game.Title,
|
||||||
Description: gc.Game.Description,
|
Description: gc.Game.Description,
|
||||||
Type: api.MapGameTypeReverse(gc.Game.Type),
|
Type: api.MapGameTypeReverse(gc.Game.Type),
|
||||||
|
Icon: gc.Game.IconID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -135,6 +135,17 @@ func (s *User) GetUser(c echo.Context) *models.User {
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *User) List(ctx context.Context) ([]models.User, error) {
|
||||||
|
res := make([]models.User, 0)
|
||||||
|
|
||||||
|
return res, s.DB.WithContext(ctx).
|
||||||
|
Preload("Games").
|
||||||
|
Order("experience DESC").
|
||||||
|
Limit(100).
|
||||||
|
Find(&res).
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
||||||
func (s *User) Update(ctx context.Context, user *models.User) error {
|
func (s *User) Update(ctx context.Context, user *models.User) error {
|
||||||
return s.DB.WithContext(ctx).Session(&gorm.Session{FullSaveAssociations: true}).Save(user).Error
|
return s.DB.WithContext(ctx).Session(&gorm.Session{FullSaveAssociations: true}).Save(user).Error
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue