Доработан фронт и бек до состояния близкого к MVP
This commit is contained in:
parent
2b9a69309d
commit
e7acaa92a5
39 changed files with 626 additions and 937 deletions
|
@ -69,7 +69,8 @@ paths:
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: string
|
||||||
|
format: uuid
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
$ref: '#/components/responses/taskResponse'
|
$ref: '#/components/responses/taskResponse'
|
||||||
|
@ -81,7 +82,8 @@ paths:
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: string
|
||||||
|
format: uuid
|
||||||
requestBody:
|
requestBody:
|
||||||
$ref: "#/components/requestBodies/enterCodeRequest"
|
$ref: "#/components/requestBodies/enterCodeRequest"
|
||||||
responses:
|
responses:
|
||||||
|
@ -94,7 +96,8 @@ components:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: string
|
||||||
|
format: uuid
|
||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
required: [ id, username ]
|
required: [ id, username ]
|
||||||
|
@ -102,21 +105,44 @@ components:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: string
|
||||||
|
format: uuid
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
type:
|
type:
|
||||||
$ref: "#/components/schemas/gameType"
|
$ref: "#/components/schemas/gameType"
|
||||||
|
points:
|
||||||
|
type: integer
|
||||||
|
taskCount:
|
||||||
|
type: integer
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
authors:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/userView"
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- title
|
- title
|
||||||
- description
|
- description
|
||||||
- type
|
- type
|
||||||
|
- points
|
||||||
|
- taskCount
|
||||||
|
- createdAt
|
||||||
|
- authors
|
||||||
taskView:
|
taskView:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- ok_code
|
||||||
|
- invalid_code
|
||||||
|
- old_code
|
||||||
|
- next_level
|
||||||
|
- game_complete
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
text:
|
text:
|
||||||
|
@ -125,10 +151,6 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/codeView'
|
$ref: '#/components/schemas/codeView'
|
||||||
entered:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/codeView'
|
|
||||||
solutions:
|
solutions:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
@ -137,7 +159,6 @@ components:
|
||||||
- title
|
- title
|
||||||
- text
|
- text
|
||||||
- codes
|
- codes
|
||||||
- entered
|
|
||||||
- solutions
|
- solutions
|
||||||
codeView:
|
codeView:
|
||||||
type: object
|
type: object
|
||||||
|
@ -278,7 +299,8 @@ components:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: string
|
||||||
|
format: uuid
|
||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
email:
|
email:
|
||||||
|
@ -287,6 +309,10 @@ components:
|
||||||
type: integer
|
type: integer
|
||||||
level:
|
level:
|
||||||
type: integer
|
type: integer
|
||||||
|
expToCurrentLevel:
|
||||||
|
type: integer
|
||||||
|
expToNextLevel:
|
||||||
|
type: integer
|
||||||
games:
|
games:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
@ -297,6 +323,8 @@ components:
|
||||||
- email
|
- email
|
||||||
- experience
|
- experience
|
||||||
- level
|
- level
|
||||||
|
- expToCurrentLevel
|
||||||
|
- expToNextLevel
|
||||||
- games
|
- games
|
||||||
errorResponse:
|
errorResponse:
|
||||||
description: ''
|
description: ''
|
||||||
|
|
|
@ -16,16 +16,17 @@ import (
|
||||||
"github.com/getkin/kin-openapi/openapi3"
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/oapi-codegen/runtime"
|
"github.com/oapi-codegen/runtime"
|
||||||
|
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerInterface represents all server handlers.
|
// ServerInterface represents all server handlers.
|
||||||
type ServerInterface interface {
|
type ServerInterface interface {
|
||||||
|
|
||||||
// (GET /engine/{uid})
|
// (GET /engine/{uid})
|
||||||
GameEngine(ctx echo.Context, uid int) error
|
GameEngine(ctx echo.Context, uid openapi_types.UUID) error
|
||||||
|
|
||||||
// (POST /engine/{uid}/code)
|
// (POST /engine/{uid}/code)
|
||||||
EnterCode(ctx echo.Context, uid int) error
|
EnterCode(ctx echo.Context, uid openapi_types.UUID) error
|
||||||
|
|
||||||
// (GET /games)
|
// (GET /games)
|
||||||
GetGames(ctx echo.Context) error
|
GetGames(ctx echo.Context) error
|
||||||
|
@ -55,7 +56,7 @@ type ServerInterfaceWrapper struct {
|
||||||
func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error {
|
func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
// ------------- Path parameter "uid" -------------
|
// ------------- Path parameter "uid" -------------
|
||||||
var uid int
|
var uid openapi_types.UUID
|
||||||
|
|
||||||
err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid)
|
err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -73,7 +74,7 @@ func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error {
|
||||||
func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error {
|
func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
// ------------- Path parameter "uid" -------------
|
// ------------- Path parameter "uid" -------------
|
||||||
var uid int
|
var uid openapi_types.UUID
|
||||||
|
|
||||||
err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid)
|
err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -187,22 +188,24 @@ 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{
|
||||||
|
|
||||||
"H4sIAAAAAAAC/8xYTY+kRgz9K5GTIxp6P07cktVqFGUPyWSTS6sPFfAwtQtVpFz0bmvEf49cfDQ0BU0T",
|
"H4sIAAAAAAAC/8xYS2/jNhD+K8W0RyHyPk66bYNFUDQo2jTtJTACVpo43EikSo68MQL992Koh2WLtBVV",
|
||||||
"ZrWnGRXGfu/Zxq5+hljnhVaoLEH0DAb/LZHsLzqR6A5QWTTvdIIP9RM+i7WyqNy/oigyGQsrtQo/kVZ8",
|
"TfcUhxzNfPPNg0O+QKqLUitUZCF5AYN/V2jpR51JdAuoCM2lzvCm2eG1VCtC5X6KssxlKkhqFX+xWvGa",
|
||||||
"RvET5oL/K4wu0NjGVawT5L/2VCBEQNZIlUJVBS6qNJhAtK+tDkFrpf/5hLGFamhmTYlVAKnI8X0i7Rps",
|
"TR+xEPyrNLpEQ62qVGfIf2lXIiRgyUi1gbqOnFVpMIPkrpFaR52U/usLpgT1oRiZCusINqLAz5mkOdh+",
|
||||||
"Pxl8hAh+DM8ChPVTClu/E2EznUr1P4TAXMjMo0QAhSD6ok1yXabaR++NhZIZTCVZNN8a/vnha+/TktAo",
|
"MPgACXwf7wmIm10bd3oDZnO9kepfEIGFkLmHiQhKYe1XbbLzNDU6Bl9MpMzgRlpC89bw95vvvbuVRaNE",
|
||||||
"kS8okM4yGIvQj7JIEHdChVbUcDNGm4fmZLtal8piioaJ5kgk0qWNcLb300mQYiMLxgQRQNMTHyTZVSSk",
|
"MSFBesloTMLQyiRC3IottbKtb8Zoc9OuLJfrUhFu0LCjBVorNlMLYS/vdydDmxpZMiZIANqauJaWZjkh",
|
||||||
"xZyWdMffEr9wtAaSMEac5hCtQrMMhD+oFfR586DsdC4ol+YGxTPdYPi1QCNRxRPFxarQBokMQCb+CBke",
|
"CQs7pTr+lPiVrbWQhDFidwrRLDTTQPiNkrBPixtlpaeMcmoukDzhAsPn8lZfVsagomvcYu7PMSf2Cz6f",
|
||||||
"MfM/Wt7CkjvV08c9em2kltOy8q+CRtSuCd1nfOkounB4jUbfOJgaXPUDJ/M3gOGL302zUfz5MAEUWjZ7",
|
"kUEjUaWBPGWC7QI5EYF0/eBBm0IQJFBVkgt15Fsexjq9OTjVng4x8Laz5GNzRF3Hw7Tqq6M2pn0PcKfI",
|
||||||
"wTjb3AvLS42t64k6LjUrbeaXoT64XsQf2e5SltptcJEl57JF3zGcku1jAwBVmbPPozS2FFyVsbSn3mtn",
|
"1JPwSOE5X4fCUejcbDZcaN4Ahs9+f5iO7J82E0GpZTuWjFOCS3F6erJ0c6CP05Mk5X4amoXziX/Lcse0",
|
||||||
"zF1T3az2VMu9kD6uCWdE8mlCOivZxl9O4rFZLDwc8Ku9Xsm1g8Z6Lr5f4I3i+wJ39evt4eV90H2SPH3Q",
|
"NGqjoyg5lR363sMQbbctAFRVwTq30lAlOCtTSbvBZ3vMfSGO2BYVPWoznTOuolBJpwYFYfaJZqTP5IZw",
|
||||||
"klvubZAOX2f5WU+X1EQLOT9BQ7UPdEqq6c/dbVJNTSd3K8FkE1/rZZ/y+EKyt6TnE8CMMC6NtKc/GW8r",
|
"LvyXulIU2P5vYtrgPBXYFvMQ4JCtqI+CL+RW5xWr81eLeGjHNo+7+EznC7VR0Eqfsh/In2Xs+wz35elt",
|
||||||
"vf4s8efSPjmWPC3rIwigntlASNRr/whEIX/DZquT6lE74DUDUH+4C1cARzRUT99Xd7u7HbPUBSpRSIjg",
|
"UdNTtu+4npTtnJuu7SAcvsbh9zqcfYEO4fREratDoCGqwt38dVSFqnswonadRz/dt/OoVFuRy6z7V+f9",
|
||||||
"zd2ru51byu2TgxGiSqXC8LmUScUHKTqluErcbvRrAhHc88Byhu5dI3K0aAiifQOd/Z2Bl+5LNlzsg95y",
|
"T4XPdJ8Pjs17tpsjobdTzQ9ICPf/EpC+TY4CMrHNzR1uPCNJBBbTykja/c6UdXmhnyR+qujRgeJJpVni",
|
||||||
"dfk1qA4XS//r3W4q5Z1dOFgoKyfKgE3YDvNCk4fT+/Ye/YKU2qv7aZpN73Yfjq721Va6dGuoP71o753B",
|
"kDm7YNHaQRtLQJTyZ2wHeqketMPWkAjqN3fXjmCLxjaTz7uL1cWKfdElKlFKSODDxbuLlbuP0aODEaPa",
|
||||||
"mnCj602/4CHaH9za4svAO4PC4n29cd4s1uVPDdVa8FPAhz26h5jhap6GIsmlgkN1cNry2jwn7V/kRujt",
|
"SIXxSyWzmhc26ILFjLmx+KcMErjiYcEJum+NKJCQD627Fjrr2wNvKD2800WDufoM+fX66Pr3frUKJWEv",
|
||||||
"4AaXlyqAt7s3118a3peb9LOnsPuRxJ+N3zU5qB+c2YqE1P6rbZjubmd6UXV93rq0i4iz3Qj/29GqCFTG",
|
"Fx9cLWrH0YFzcTdXldp6XPzcvai8nYfdm84u7Nzg2ScevfnUS9HUXyr8wUe6cgJzzI3uvcNygORu7SYK",
|
||||||
"MRL90LjeGnH/J6F5zA+t5Yp8dVG+n5TNNuCBv6mE5th+pkuTQQQhT7bqUP0XAAD//zkXlqI5FQAA",
|
"X0Au3fF81VwYXk3W8RtUPRd8CPhhBd8104Tmg1xkhVSwrteOW24Mp6j9w7rT//XgDm61dQQfVx/Of3T4",
|
||||||
|
"kNKGnzXF/euZPxq/auugXjuxGQFp9NfLeLp6vadHWTf0W1c0yXGWG+H/OLoyga3SFK39rlW9NOLhW+Fp",
|
||||||
|
"zDed5Ix49Va+nZCdLMA191SLZtt17crkkEDM5169rv8JAAD//ytDNwRSFwAA",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|
34
api/types.go
34
api/types.go
|
@ -3,6 +3,10 @@
|
||||||
// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.0.0 DO NOT EDIT.
|
// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.0.0 DO NOT EDIT.
|
||||||
package api
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CookieAuthScopes = "cookieAuth.Scopes"
|
CookieAuthScopes = "cookieAuth.Scopes"
|
||||||
)
|
)
|
||||||
|
@ -13,6 +17,15 @@ const (
|
||||||
Virtual GameType = "virtual"
|
Virtual GameType = "virtual"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Defines values for TaskViewMessage.
|
||||||
|
const (
|
||||||
|
GameComplete TaskViewMessage = "game_complete"
|
||||||
|
InvalidCode TaskViewMessage = "invalid_code"
|
||||||
|
NextLevel TaskViewMessage = "next_level"
|
||||||
|
OkCode TaskViewMessage = "ok_code"
|
||||||
|
OldCode TaskViewMessage = "old_code"
|
||||||
|
)
|
||||||
|
|
||||||
// CodeEdit defines model for codeEdit.
|
// CodeEdit defines model for codeEdit.
|
||||||
type CodeEdit struct {
|
type CodeEdit struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
|
@ -39,8 +52,12 @@ type GameType string
|
||||||
|
|
||||||
// GameView defines model for gameView.
|
// GameView defines model for gameView.
|
||||||
type GameView struct {
|
type GameView struct {
|
||||||
|
Authors []UserView `json:"authors"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Id int `json:"id"`
|
Id openapi_types.UUID `json:"id"`
|
||||||
|
Points int `json:"points"`
|
||||||
|
TaskCount int `json:"taskCount"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Type GameType `json:"type"`
|
Type GameType `json:"type"`
|
||||||
}
|
}
|
||||||
|
@ -68,12 +85,21 @@ type TaskEdit struct {
|
||||||
// TaskView defines model for taskView.
|
// TaskView defines model for taskView.
|
||||||
type TaskView struct {
|
type TaskView struct {
|
||||||
Codes []CodeView `json:"codes"`
|
Codes []CodeView `json:"codes"`
|
||||||
Entered []CodeView `json:"entered"`
|
Message *TaskViewMessage `json:"message,omitempty"`
|
||||||
Solutions []SolutionView `json:"solutions"`
|
Solutions []SolutionView `json:"solutions"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TaskViewMessage defines model for TaskView.Message.
|
||||||
|
type TaskViewMessage string
|
||||||
|
|
||||||
|
// UserView defines model for userView.
|
||||||
|
type UserView struct {
|
||||||
|
Id openapi_types.UUID `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
// ErrorResponse defines model for errorResponse.
|
// ErrorResponse defines model for errorResponse.
|
||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
|
@ -92,9 +118,11 @@ type TaskResponse = TaskView
|
||||||
// UserResponse defines model for userResponse.
|
// UserResponse defines model for userResponse.
|
||||||
type UserResponse struct {
|
type UserResponse struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
ExpToCurrentLevel int `json:"expToCurrentLevel"`
|
||||||
|
ExpToNextLevel int `json:"expToNextLevel"`
|
||||||
Experience int `json:"experience"`
|
Experience int `json:"experience"`
|
||||||
Games []GameView `json:"games"`
|
Games []GameView `json:"games"`
|
||||||
Id int `json:"id"`
|
Id openapi_types.UUID `json:"id"`
|
||||||
Level int `json:"level"`
|
Level int `json:"level"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,26 +4,6 @@ volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# app:
|
|
||||||
# build:
|
|
||||||
# context: .
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
# env_file:
|
|
||||||
# # Ensure that the variables in .env match the same variables in devcontainer.json
|
|
||||||
# - .env
|
|
||||||
|
|
||||||
# volumes:
|
|
||||||
# - ../..:/workspaces:cached
|
|
||||||
|
|
||||||
# # Overrides default command so things don't shut down after the process ends.
|
|
||||||
# command: sleep infinity
|
|
||||||
|
|
||||||
# # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
|
||||||
# network_mode: service:db
|
|
||||||
|
|
||||||
# # Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
|
||||||
# # (Adding the "ports" property to this file will not forward from a Codespace.)
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:15-alpine3.17
|
image: postgres:15-alpine3.17
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
9
frontend/package-lock.json
generated
9
frontend/package-lock.json
generated
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neonxp/compose": "0.0.6",
|
"@neonxp/compose": "0.0.6",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^2.9.1",
|
"react-bootstrap": "^2.9.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -3735,6 +3736,14 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
|
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neonxp/compose": "0.0.6",
|
"@neonxp/compose": "0.0.6",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^2.9.1",
|
"react-bootstrap": "^2.9.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
BIN
frontend/public/logo.png
Normal file
BIN
frontend/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
frontend/public/logo_small.png
Normal file
BIN
frontend/public/logo_small.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
|
@ -6,13 +6,9 @@ import Index from './pages/Index'
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
import Register from './pages/Register'
|
import Register from './pages/Register'
|
||||||
import NoMatch from './pages/NoMatch'
|
import NoMatch from './pages/NoMatch'
|
||||||
import Team from './pages/Team'
|
|
||||||
import Teams from './pages/Teams'
|
|
||||||
import { UserProvider } from './store/user'
|
import { UserProvider } from './store/user'
|
||||||
import { ajax } from './utils/fetch'
|
import { ajax } from './utils/fetch'
|
||||||
import TeamNew from './pages/TeamNew'
|
import Engine from './pages/Engine'
|
||||||
import Admin from './pages/Admin'
|
|
||||||
import AdminGame from './pages/AdminGame'
|
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
|
@ -30,20 +26,11 @@ const router = createBrowserRouter(
|
||||||
<Route path="login" element={<Login />} />
|
<Route path="login" element={<Login />} />
|
||||||
<Route path="register" element={<Register />} />
|
<Route path="register" element={<Register />} />
|
||||||
<Route
|
<Route
|
||||||
path="teams"
|
path="go/:gameId"
|
||||||
element={<Auth><Teams /></Auth>}
|
element={<Auth><Engine /></Auth>}
|
||||||
loader={() => ajax("/api/teams")}
|
loader={({ params }) => ajax(`/api/engine/${params.gameId}`)}
|
||||||
/>
|
/>
|
||||||
<Route
|
{/* <Route
|
||||||
path="teams/new"
|
|
||||||
element={<Auth><TeamNew /></Auth>}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="teams/:teamId"
|
|
||||||
element={<Auth><Team /></Auth>}
|
|
||||||
loader={({ params }) => ajax(`/api/teams/${params.teamId}`)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="admin"
|
path="admin"
|
||||||
element={<Auth role="creator"><Admin /></Auth>}
|
element={<Auth role="creator"><Admin /></Auth>}
|
||||||
loader={() => ajax(`/api/admin/games`)}
|
loader={() => ajax(`/api/admin/games`)}
|
||||||
|
@ -55,7 +42,7 @@ const router = createBrowserRouter(
|
||||||
title: "Новая игра",
|
title: "Новая игра",
|
||||||
tasks: []
|
tasks: []
|
||||||
})}
|
})}
|
||||||
/>
|
/> */}
|
||||||
<Route path="*" element={<NoMatch />} />
|
<Route path="*" element={<NoMatch />} />
|
||||||
</Route>
|
</Route>
|
||||||
)
|
)
|
||||||
|
|
Binary file not shown.
BIN
frontend/src/assets/logo.png
Normal file
BIN
frontend/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
frontend/src/assets/logo_small.png
Normal file
BIN
frontend/src/assets/logo_small.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
Before Width: | Height: | Size: 4 KiB |
|
@ -2,12 +2,15 @@
|
||||||
.navbar-dark {
|
.navbar-dark {
|
||||||
border-bottom: 0.1px solid #333333 !important;
|
border-bottom: 0.1px solid #333333 !important;
|
||||||
}
|
}
|
||||||
@font-face {
|
.navbar {
|
||||||
|
background-color: rgb(16, 22, 29) !important;
|
||||||
|
}
|
||||||
|
/* @font-face {
|
||||||
font-family: 'TiltNeon';
|
font-family: 'TiltNeon';
|
||||||
src: url('TiltNeon-Regular.ttf');
|
src: url('TiltNeon-Regular.ttf');
|
||||||
}
|
} */
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-family: TiltNeon, var(--bs-font-sans-serif);
|
/* font-family: TiltNeon, var(--bs-font-sans-serif);
|
||||||
text-shadow:
|
text-shadow:
|
||||||
0 0 1px rgb(255, 255, 255, 1),
|
0 0 1px rgb(255, 255, 255, 1),
|
||||||
0 0 2px rgb(255, 255, 255, 1),
|
0 0 2px rgb(255, 255, 255, 1),
|
||||||
|
@ -17,10 +20,14 @@
|
||||||
0 1px #198754,
|
0 1px #198754,
|
||||||
1px 0 #198754,
|
1px 0 #198754,
|
||||||
0 -1px #198754;
|
0 -1px #198754;
|
||||||
-webkit-text-stroke: 1px white;
|
-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;
|
||||||
|
height: 40px;
|
||||||
|
width: 85px;
|
||||||
|
background-image: url("./logo_small.png");
|
||||||
|
background-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
th.thin {
|
th.thin {
|
||||||
|
@ -28,3 +35,11 @@ th.thin {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
--bs-body-bg: rgb(23, 30, 38) !important;
|
||||||
|
--bs-primary-rgb: rgb(16, 22, 29) !important;
|
||||||
|
--bs-navbar-color: rgb(249, 115, 22) !important;
|
||||||
|
--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;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { Link, Outlet, useLoaderData } from "react-router-dom";
|
import { Link, Outlet, useLoaderData } from "react-router-dom";
|
||||||
import { Button, Nav, Container, NavbarBrand, NavbarToggle, Navbar, NavbarCollapse, ButtonGroup } from "react-bootstrap";
|
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 } 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";
|
||||||
|
|
||||||
|
@ -20,46 +20,44 @@ export default () => {
|
||||||
}).
|
}).
|
||||||
then(() => setUser(null))
|
then(() => setUser(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<Navbar expand="lg" className="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
|
<Navbar expand="lg" data-bs-theme="dark">
|
||||||
<Container>
|
<Container>
|
||||||
<NavbarBrand href="https://nquest.ru/">nQuest</NavbarBrand>
|
<NavbarBrand href="/"></NavbarBrand>
|
||||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||||
<NavbarCollapse id="basic-navbar-nav" className="justify-content-end">
|
<NavbarCollapse className="justify-content-between">
|
||||||
<Nav className="me-auto">
|
<Nav className="me-auto">
|
||||||
<Nav.Item>
|
|
||||||
<Nav.Link as={Link} className="nav-link" to="/">Игры</Nav.Link>
|
|
||||||
</Nav.Item>
|
|
||||||
{hasRole("user") ? (<>
|
|
||||||
<Nav.Item>
|
|
||||||
<Nav.Link as={Link} className="nav-link" to="/teams">Команды</Nav.Link>
|
|
||||||
</Nav.Item>
|
|
||||||
{hasRole("creator") ? (
|
{hasRole("creator") ? (
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link as={Link} className="nav-link" to="/admin">Админка</Nav.Link>
|
<Nav.Link as={Link} className="nav-link" to="/admin">Админка</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
) : null}
|
) : null}
|
||||||
</>) : null}
|
|
||||||
</Nav>
|
</Nav>
|
||||||
<Navbar.Text>
|
<Nav>
|
||||||
{user ? (
|
{user ? (
|
||||||
<>
|
<>
|
||||||
{user.username}
|
<NavDropdown title={user.username} id="user-box">
|
||||||
{user.team ? (
|
<NavDropdown.Item>
|
||||||
<>(<Link to={`teams/${user.team.id}`}>{user.team.name}</Link>)</>
|
Уровень: <b>{user.level}</b>, Опыт: <b>{user.experience}/{user.expToNextLevel}</b>
|
||||||
) : (
|
</NavDropdown.Item>
|
||||||
<>(без команды)</>
|
<NavDropdown.Item>
|
||||||
)}
|
<Nav.Link onClick={logout}>Выход</Nav.Link>
|
||||||
<Button type="button" variant="outline-success" onClick={logout}>Выход</Button>
|
</NavDropdown.Item>
|
||||||
|
</NavDropdown>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ButtonGroup>
|
<>
|
||||||
<Link className="btn btn-success" to="login">Вход</Link>
|
<Nav.Item>
|
||||||
<Link className="btn btn-outline-success" to="register">Регистрация</Link>
|
<Nav.Link as={Link} className="btn btn-success" to="login">Вход</Nav.Link>
|
||||||
</ButtonGroup>
|
</Nav.Item>
|
||||||
|
<Nav.Item>
|
||||||
|
<Nav.Link as={Link} className="btn btn-outline-success" to="register">Регистрация</Nav.Link>
|
||||||
|
</Nav.Item>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Navbar.Text>
|
|
||||||
|
</Nav>
|
||||||
|
|
||||||
</NavbarCollapse>
|
</NavbarCollapse>
|
||||||
</Container>
|
</Container>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { Link, useLoaderData } from "react-router-dom";
|
|
||||||
import { Button, Table } from 'react-bootstrap';
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const games = useLoaderData();
|
|
||||||
if (!games) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
<h1>Управление играми <Button to="/admin/games/new" as={Link}>Создать игру</Button> </h1>
|
|
||||||
<Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Название</th>
|
|
||||||
<th>Создана</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{games.map(game => (
|
|
||||||
<tr key={game.id}>
|
|
||||||
<td>{game.id}</td>
|
|
||||||
<td>{game.title}</td>
|
|
||||||
<td>{team.createdAt}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</>);
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
import { useLoaderData } from "react-router-dom";
|
|
||||||
import { Col, Row, Form, Button, Card } from 'react-bootstrap';
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const loadedGame = useLoaderData();
|
|
||||||
const [game, setGame] = useState(loadedGame);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
if (!game) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const submit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
console.log(game)
|
|
||||||
}
|
|
||||||
return (<>
|
|
||||||
<h1>Игра "{game.title}"</h1>
|
|
||||||
<Form onSubmit={submit}>
|
|
||||||
<div className="col-lg-10 px-0">
|
|
||||||
{error ? (<div className="alert alert-danger" role="alert">{error}</div>) : null}
|
|
||||||
|
|
||||||
<Form.Group as={Row} className="mb-3" controlId="title">
|
|
||||||
<Form.Label column sm="4">Название игры</Form.Label>
|
|
||||||
<Col sm="8">
|
|
||||||
<Form.Control
|
|
||||||
name="title"
|
|
||||||
type="text"
|
|
||||||
value={game.title}
|
|
||||||
onChange={e => setGame({ ...game, title: e.target.value })}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group as={Row} className="mb-3" controlId="startAt">
|
|
||||||
<Form.Label column sm="4">Начало в</Form.Label>
|
|
||||||
<Col sm="8">
|
|
||||||
<Form.Control
|
|
||||||
name="startAt"
|
|
||||||
type="datetime-local"
|
|
||||||
value={game.startAt}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group as={Row} className="mb-3" controlId="description">
|
|
||||||
<Form.Label column sm="4">Описание</Form.Label>
|
|
||||||
<Col sm="8">
|
|
||||||
<Form.Control
|
|
||||||
name="description"
|
|
||||||
as="textarea"
|
|
||||||
rows={5}
|
|
||||||
value={game.description}
|
|
||||||
onChange={e => setGame({ ...game, description: e.target.value })}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<div>
|
|
||||||
<h2>
|
|
||||||
Задания
|
|
||||||
<Button onClick={() => setGame({ ...game, tasks: [...game.tasks, { title: "Задание" }] })}>
|
|
||||||
Добавить задание
|
|
||||||
</Button>
|
|
||||||
</h2>
|
|
||||||
{game.tasks.map((task, idx) =>
|
|
||||||
<Task
|
|
||||||
key={idx}
|
|
||||||
id={idx}
|
|
||||||
task={task}
|
|
||||||
setTask={task => {
|
|
||||||
const newTasks = game.tasks;
|
|
||||||
newTasks[idx] = task;
|
|
||||||
setGame({...game, tasks: newTasks});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button type="submit" size="lg">Сохранить</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Task = ({id, task, setTask}) => (
|
|
||||||
<Card>
|
|
||||||
<Card.Body>
|
|
||||||
<Card.Title>
|
|
||||||
Задание #{id + 1}
|
|
||||||
</Card.Title>
|
|
||||||
<Form.Group as={Row} className="mb-3" controlId={`task[${id}].text`}>
|
|
||||||
<Form.Label column sm="4">Задание</Form.Label>
|
|
||||||
<Col sm="8">
|
|
||||||
<Form.Control
|
|
||||||
name={`task[${id}].text`}
|
|
||||||
as="textarea"
|
|
||||||
rows={5}
|
|
||||||
value={task.text}
|
|
||||||
onChange={e => setTask({ ...task, text: e.target.value })}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
)
|
|
75
frontend/src/pages/Engine.jsx
Normal file
75
frontend/src/pages/Engine.jsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { Badge, Button, Col, Form, Row, Table } from "react-bootstrap";
|
||||||
|
import { Link, useLoaderData, useParams } from "react-router-dom";
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ajax } from "../utils/fetch";
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const params = useParams();
|
||||||
|
const loadedTask = useLoaderData();
|
||||||
|
const [task, setTask] = useState(loadedTask);
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
const onSubmitCode = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
ajax(`/api/engine/${params.gameId}/code`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ code })
|
||||||
|
}).
|
||||||
|
then((x) => {
|
||||||
|
if (x != null) {
|
||||||
|
setTask(x);
|
||||||
|
setCode("");
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
console.warn(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task && task.message == "game_complete") {
|
||||||
|
return (<div>
|
||||||
|
<div className="alert alert-success" role="alert">Вы прошли все уровни!</div>
|
||||||
|
<Link to={"/"}>К списку игр</Link>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
if (!task) {
|
||||||
|
return (<div className="alert alert-default" role="alert">
|
||||||
|
<div>Для вас не предусмотренно уровней</div>
|
||||||
|
<Link to={"/"}>К списку игр</Link>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<h1 className="mb-4">{task.title}</h1>
|
||||||
|
<Markdown>{task.text}</Markdown>
|
||||||
|
{task.message == "invalid_code" ? (<div className="alert alert-danger" role="alert">Неверный код</div>) : null}
|
||||||
|
{task.message == "old_code" ? (<div className="alert alert-danger" role="alert">Этот код уже вводился</div>) : null}
|
||||||
|
{task.message == "next_level" ? (<div className="alert alert-success" role="alert">Переход на новый уровень</div>) : null}
|
||||||
|
{task.message == "ok_code" ? (<div className="alert alert-success" role="alert">Код принят, ищите оставшиеся</div>) : null}
|
||||||
|
<h2>Коды:</h2>
|
||||||
|
<ul>
|
||||||
|
{task.codes.map(
|
||||||
|
(c, idx) => <li key={idx}>{c.description} {!!c.code ? (<Badge bg="success">Принят {c.code}</Badge>) : (<Badge bg="danger">Не введён</Badge>)}</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
<Form onSubmit={onSubmitCode}>
|
||||||
|
<Form.Group as={Row} className="mb-3" controlId="code">
|
||||||
|
<Form.Label column sm="2">Код:</Form.Label>
|
||||||
|
<Col sm="6">
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="NQ...."
|
||||||
|
value={code}
|
||||||
|
onChange={e => setCode(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col sm="4">
|
||||||
|
<Form.Control type="submit" className="btn btn-primary" value="Отправить" />
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
</Form>
|
||||||
|
</>);
|
||||||
|
}
|
|
@ -1,47 +1,72 @@
|
||||||
import { Button, Table } from "react-bootstrap";
|
import { Button, Table } from "react-bootstrap";
|
||||||
import { Link, useLoaderData } from "react-router-dom";
|
import { Link, useLoaderData } from "react-router-dom";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
|
import moment from 'moment/min/moment-with-locales';
|
||||||
|
import { UserProvider } from "../store/user";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
moment.locale('ru');
|
||||||
const games = useLoaderData();
|
const games = useLoaderData();
|
||||||
|
const { user } = UserProvider.useContainer();
|
||||||
return (<>
|
return (<>
|
||||||
<h1 className="mb-4">Текущие игры</h1>
|
<h1 className="mb-4">Доступные квесты</h1>
|
||||||
{games && games.map(game => (
|
{games && games.map(game => (
|
||||||
<>
|
<div key={game.id}>
|
||||||
<h3>{game.title}</h3>
|
<h3>{game.title}</h3>
|
||||||
<Table className="table table-bordered mb-4">
|
<Table className="table table-bordered mb-4">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td className="col-sm-3">
|
||||||
Начало
|
Тип
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="col-sm-3">
|
||||||
{game.startAt}
|
{game.type}
|
||||||
|
</td>
|
||||||
|
<td className="col-sm-3">
|
||||||
|
Опыт за квест
|
||||||
|
</td>
|
||||||
|
<td className="col-sm-3">
|
||||||
|
{game.points}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<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>
|
<Markdown>{game.description}</Markdown>
|
||||||
<div>
|
<div>
|
||||||
<Button>Войти в игру</Button>
|
{user ? (<>
|
||||||
|
{(!!user.games.find(x => x.id === game.id))
|
||||||
|
? (<b>Вы уже прошли этот квест</b>)
|
||||||
|
: (<Link className="btn btn-primary" to={`go/${game.id}`}>Начать прохождение</Link>)}
|
||||||
|
</>): null}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
Участвуют:
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<ul>
|
|
||||||
{game.teams.map(team => (<li><Link to={`/team/${team.id}`}>{team.name}</Link></li>))}
|
|
||||||
{game.teams.length == 0 ? <p>Никто пока не подал заявку</p> : null}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!games ? (<strong>Игр пока не анонсировано</strong>) : null}
|
{!games ? (<strong>Игр пока не анонсировано</strong>) : null}
|
||||||
</>);
|
</>);
|
||||||
|
|
|
@ -1,175 +0,0 @@
|
||||||
import { useLoaderData, useNavigate, useRouteLoaderData } from "react-router-dom";
|
|
||||||
import { Alert, Button, ButtonGroup, Table } from "react-bootstrap";
|
|
||||||
import { UserProvider } from "../store/user";
|
|
||||||
import { ajax } from "../utils/fetch";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
const userRoles = { captain: "Капитан", member: "Участник" };
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const teamFromRouter = useLoaderData();
|
|
||||||
const [team, setTeam] = useState(teamFromRouter);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
useEffect(() => {
|
|
||||||
setTeam(teamFromRouter);
|
|
||||||
}, [teamFromRouter]);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { user, setUser } = UserProvider.useContainer();
|
|
||||||
|
|
||||||
const [request, setRequest] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setRequest(!!team.requests.find(x => user && x.user.id == user.id));
|
|
||||||
}, [user, team.requests])
|
|
||||||
|
|
||||||
if (!team) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const member = team.members.find(tm => tm.user.id === user.id);
|
|
||||||
const inOtherTeam = user.team && user.team.td != team.id;
|
|
||||||
const isCaptain = member && user.team.role == "captain";
|
|
||||||
|
|
||||||
const sendRequest = () => {
|
|
||||||
ajax(`/api/teams/${team.id}/requests`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}).then(() => setRequest(true)).catch(setError);
|
|
||||||
};
|
|
||||||
const updateMemebers = (members) => {
|
|
||||||
ajax(`/api/teams/${team.id}/members`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ members }),
|
|
||||||
}).then(team => setTeam(team)).catch(setError);
|
|
||||||
};
|
|
||||||
const approveRequest = (userID, approve) => {
|
|
||||||
ajax(`/api/teams/${team.id}/requests/${userID}`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ approve }),
|
|
||||||
}).then(team => setTeam(team)).catch(setError);
|
|
||||||
}
|
|
||||||
const removeMember = (userID) => {
|
|
||||||
const members = team.members.map(x => x.user.id).filter(uid => uid != userID);
|
|
||||||
return updateMemebers(members)
|
|
||||||
}
|
|
||||||
const leaveTeam = () => {
|
|
||||||
ajax(`/api/teams/${team.id}/members`, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
}).then(team => setTeam(team)).then(() => {
|
|
||||||
setUser({
|
|
||||||
...user,
|
|
||||||
team: null,
|
|
||||||
});
|
|
||||||
}).catch(setError);
|
|
||||||
};
|
|
||||||
const deleteTeam = () => {
|
|
||||||
ajax(`/api/teams/${team.id}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
}).then(() => {
|
|
||||||
setUser({
|
|
||||||
...user,
|
|
||||||
team: null,
|
|
||||||
});
|
|
||||||
navigate("/teams");
|
|
||||||
}).catch(setError);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
<h1>{team.name}</h1>
|
|
||||||
<p>Создана: {team.createdAt}</p>
|
|
||||||
|
|
||||||
{!member && !inOtherTeam && !request ? (<Button onClick={sendRequest}>Отправить заявку в команду</Button>) : null}
|
|
||||||
{request ? (<Alert variant="success">Заявка в команду отправлена</Alert>) : null}
|
|
||||||
{error ? (<Alert variant="danger">{error}</Alert>) : null}
|
|
||||||
{member && !isCaptain ? (<Button onClick={leaveTeam}>Выйти из команды</Button>) : null}
|
|
||||||
|
|
||||||
<h2>Участники</h2>
|
|
||||||
<Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Имя пользователя</th>
|
|
||||||
<th>Роль</th>
|
|
||||||
<th>Присоединился</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{team.members.map(tm => (
|
|
||||||
<tr key={tm.user.id}>
|
|
||||||
<td>{tm.user.username}</td>
|
|
||||||
<td>{userRoles[tm.role]}</td>
|
|
||||||
<td>{tm.createdAt}</td>
|
|
||||||
<td>
|
|
||||||
{
|
|
||||||
isCaptain && tm.user.id != user.id
|
|
||||||
? (<Button variant="outline-danger" onClick={() => removeMember(tm.user.id)}>Выгнать</Button>)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
{isCaptain
|
|
||||||
? (<>
|
|
||||||
<h2>Заявки</h2>
|
|
||||||
<Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Имя пользователя</th>
|
|
||||||
<th>Дата заявки</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{team.requests.map(tm => (
|
|
||||||
<tr key={tm.user.id}>
|
|
||||||
<td>{tm.user.username}</td>
|
|
||||||
<td>{tm.createdAt}</td>
|
|
||||||
<td>
|
|
||||||
<ButtonGroup>
|
|
||||||
<Button
|
|
||||||
variant="outline-success"
|
|
||||||
onClick={() => approveRequest(tm.user.id, true)}>
|
|
||||||
Принять
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline-danger"
|
|
||||||
onClick={() => approveRequest(tm.user.id, false)}>
|
|
||||||
Отказать
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</>)
|
|
||||||
: null}
|
|
||||||
{isCaptain && (team.members.length == 1) ? <Button variant="outline-danger" onClick={deleteTeam}>Удалить команду</Button> : null}
|
|
||||||
</>);
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import { Form, Button, Row, Col } from "react-bootstrap";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { ajax } from "../utils/fetch";
|
|
||||||
import { UserProvider } from "../store/user";
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const {user, setUser} = UserProvider.useContainer();
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onCreate = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
ajax("/api/teams", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ name })
|
|
||||||
}).then((team) => {
|
|
||||||
setUser({
|
|
||||||
...user,
|
|
||||||
team: {
|
|
||||||
role: 1,
|
|
||||||
...team,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
navigate(`/teams/${team.id}`);
|
|
||||||
}).catch(e => setError(e.message))
|
|
||||||
}
|
|
||||||
return (<>
|
|
||||||
<h1>Создание команды</h1>
|
|
||||||
<Form onSubmit={onCreate}>
|
|
||||||
<div className="col-lg-8 px-0">
|
|
||||||
{error ? (<div className="alert alert-danger" role="alert">{error}</div>) : null}
|
|
||||||
<Form.Group as={Row} className="mb-3" controlId="name">
|
|
||||||
<Form.Label column sm="4">Название команды</Form.Label>
|
|
||||||
<Col sm="8">
|
|
||||||
<Form.Control
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
onChange={e => setName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Button type="submit" size="lg">Создать</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</>)
|
|
||||||
};
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { Link, useLoaderData } from "react-router-dom";
|
|
||||||
import { Table } from 'react-bootstrap';
|
|
||||||
import { UserProvider } from "../store/user";
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const teams = useLoaderData();
|
|
||||||
if (!teams) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const { user } = UserProvider.useContainer();
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
<h1>Команды</h1>
|
|
||||||
{user && !user.team
|
|
||||||
? (<p>Вы не состоите в командах. <Link to="/teams/new">Создать свою команду.</Link></p>)
|
|
||||||
: null}
|
|
||||||
|
|
||||||
<Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Команда</th>
|
|
||||||
<th>Участников</th>
|
|
||||||
<th>Создана</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{teams.map(team => (
|
|
||||||
<tr key={team.id} className={team.currentTeam?"table-active":null}>
|
|
||||||
<td><Link to={`/teams/${team.id}`}>{team.name}</Link></td>
|
|
||||||
<td>{team.members}</td>
|
|
||||||
<td>{team.createdAt}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</>);
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ package controller
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"gitrepo.ru/neonxp/nquest/api"
|
"gitrepo.ru/neonxp/nquest/api"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
||||||
|
@ -31,7 +32,7 @@ func (a *Admin) CreateGame(ctx echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.JSON(http.StatusCreated, api.GameResponse{
|
return ctx.JSON(http.StatusCreated, api.GameResponse{
|
||||||
Id: int(game.ID),
|
Id: game.ID,
|
||||||
Title: game.Title,
|
Title: game.Title,
|
||||||
Description: game.Description,
|
Description: game.Description,
|
||||||
})
|
})
|
||||||
|
@ -39,6 +40,9 @@ func (a *Admin) CreateGame(ctx echo.Context) error {
|
||||||
|
|
||||||
func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) *models.Game {
|
func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) *models.Game {
|
||||||
game := &models.Game{
|
game := &models.Game{
|
||||||
|
Model: models.Model{
|
||||||
|
ID: uuid.New(),
|
||||||
|
},
|
||||||
Visible: false,
|
Visible: false,
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
|
@ -49,22 +53,32 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User)
|
||||||
Tasks: make([]*models.Task, 0, len(req.Tasks)),
|
Tasks: make([]*models.Task, 0, len(req.Tasks)),
|
||||||
Points: req.Points,
|
Points: req.Points,
|
||||||
}
|
}
|
||||||
for _, te := range req.Tasks {
|
for order, te := range req.Tasks {
|
||||||
task := &models.Task{
|
task := &models.Task{
|
||||||
|
Model: models.Model{
|
||||||
|
ID: uuid.New(),
|
||||||
|
},
|
||||||
Title: te.Title,
|
Title: te.Title,
|
||||||
Text: te.Text,
|
Text: te.Text,
|
||||||
MaxTime: 0,
|
MaxTime: 0,
|
||||||
Solutions: make([]*models.Solution, 0, len(te.Solutions)),
|
Solutions: make([]*models.Solution, 0, len(te.Solutions)),
|
||||||
Codes: make([]*models.Code, 0, len(te.Codes)),
|
Codes: make([]*models.Code, 0, len(te.Codes)),
|
||||||
|
TaskOrder: uint(order),
|
||||||
}
|
}
|
||||||
for _, s := range te.Solutions {
|
for _, s := range te.Solutions {
|
||||||
task.Solutions = append(task.Solutions, &models.Solution{
|
task.Solutions = append(task.Solutions, &models.Solution{
|
||||||
|
Model: models.Model{
|
||||||
|
ID: uuid.New(),
|
||||||
|
},
|
||||||
After: s.After,
|
After: s.After,
|
||||||
Text: s.Text,
|
Text: s.Text,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, ce := range te.Codes {
|
for _, ce := range te.Codes {
|
||||||
task.Codes = append(task.Codes, &models.Code{
|
task.Codes = append(task.Codes, &models.Code{
|
||||||
|
Model: models.Model{
|
||||||
|
ID: uuid.New(),
|
||||||
|
},
|
||||||
Code: ce.Code,
|
Code: ce.Code,
|
||||||
Description: ce.Description,
|
Description: ce.Description,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"gitrepo.ru/neonxp/nquest/api"
|
"gitrepo.ru/neonxp/nquest/api"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
||||||
|
@ -16,10 +18,10 @@ type Engine struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// (GET /engine/{uid})
|
// (GET /engine/{uid})
|
||||||
func (ec *Engine) GameEngine(c echo.Context, uid int) error {
|
func (ec *Engine) GameEngine(c echo.Context, uid uuid.UUID) error {
|
||||||
user := contextlib.GetUser(c)
|
user := contextlib.GetUser(c)
|
||||||
|
|
||||||
game, err := ec.GameService.GetByID(c.Request().Context(), uint(uid))
|
game, err := ec.GameService.GetByID(c.Request().Context(), uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -29,16 +31,16 @@ func (ec *Engine) GameEngine(c echo.Context, uid int) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, mapCursorToTask(cursor))
|
return c.JSON(http.StatusOK, mapCursorToTask(cursor, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
// (POST /engine/{uid}/code)
|
// (POST /engine/{uid}/code)
|
||||||
func (ec *Engine) EnterCode(c echo.Context, uid int) error {
|
func (ec *Engine) EnterCode(c echo.Context, uid uuid.UUID) error {
|
||||||
user := contextlib.GetUser(c)
|
user := contextlib.GetUser(c)
|
||||||
|
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
game, err := ec.GameService.GetByID(ctx, uint(uid))
|
game, err := ec.GameService.GetByID(ctx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -49,31 +51,48 @@ func (ec *Engine) EnterCode(c echo.Context, uid int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor, err := ec.EngineService.EnterCode(ctx, game, user, req.Code)
|
cursor, err := ec.EngineService.EnterCode(ctx, game, user, req.Code)
|
||||||
|
message := api.OkCode
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
switch {
|
||||||
|
case errors.Is(err, service.ErrGameFinished):
|
||||||
|
message = api.GameComplete
|
||||||
|
case errors.Is(err, service.ErrInvalidCode):
|
||||||
|
message = api.InvalidCode
|
||||||
|
case errors.Is(err, service.ErrOldCode):
|
||||||
|
message = api.OldCode
|
||||||
|
case errors.Is(err, service.ErrNextLevel):
|
||||||
|
message = api.NextLevel
|
||||||
|
default:
|
||||||
|
return c.JSON(http.StatusBadRequest, &api.ErrorResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, mapCursorToTask(cursor))
|
return c.JSON(http.StatusOK, mapCursorToTask(cursor, &message))
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapCursorToTask(cursor *models.GameCursor) *api.TaskView {
|
func mapCursorToTask(cursor *models.GameCursor, message *api.TaskViewMessage) *api.TaskView {
|
||||||
resp := &api.TaskResponse{
|
resp := &api.TaskResponse{
|
||||||
|
Message: message,
|
||||||
Codes: make([]api.CodeView, 0, len(cursor.Task.Codes)),
|
Codes: make([]api.CodeView, 0, len(cursor.Task.Codes)),
|
||||||
Entered: make([]api.CodeView, 0, len(cursor.Codes)),
|
|
||||||
Solutions: []api.SolutionView{},
|
Solutions: []api.SolutionView{},
|
||||||
Text: cursor.Task.Text,
|
Text: cursor.Task.Text,
|
||||||
Title: cursor.Task.Title,
|
Title: cursor.Task.Title,
|
||||||
}
|
}
|
||||||
for _, code := range cursor.Task.Codes {
|
for _, code := range cursor.Task.Codes {
|
||||||
resp.Codes = append(resp.Codes, api.CodeView{
|
c := api.CodeView{
|
||||||
Description: code.Description,
|
Description: code.Description,
|
||||||
})
|
|
||||||
}
|
}
|
||||||
for _, code := range cursor.Codes {
|
for _, cd := range cursor.Codes {
|
||||||
resp.Entered = append(resp.Entered, api.CodeView{
|
if cd.ID == code.ID {
|
||||||
Code: &code.Code,
|
c.Code = &cd.Code
|
||||||
Description: code.Description,
|
break
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
resp.Codes = append(resp.Codes, c)
|
||||||
|
}
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"gitrepo.ru/neonxp/nquest/api"
|
"gitrepo.ru/neonxp/nquest/api"
|
||||||
|
@ -21,12 +22,24 @@ func (g *Game) GetGames(ctx echo.Context) error {
|
||||||
|
|
||||||
resp := make(api.GameListResponse, 0, len(games))
|
resp := make(api.GameListResponse, 0, len(games))
|
||||||
for _, game := range games {
|
for _, game := range games {
|
||||||
resp = append(resp, api.GameView{
|
gv := api.GameView{
|
||||||
Id: int(game.ID),
|
Id: game.ID,
|
||||||
Title: game.Title,
|
Title: game.Title,
|
||||||
Description: game.Description,
|
Description: game.Description,
|
||||||
|
Type: api.MapGameTypeReverse(game.Type),
|
||||||
|
Points: game.Points,
|
||||||
|
TaskCount: len(game.Tasks),
|
||||||
|
Authors: make([]api.UserView, 0, len(game.Authors)),
|
||||||
|
CreatedAt: game.CreatedAt.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
for _, u := range game.Authors {
|
||||||
|
gv.Authors = append(gv.Authors, api.UserView{
|
||||||
|
Id: u.ID,
|
||||||
|
Username: u.Username,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
resp = append(resp, gv)
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.JSON(http.StatusOK, resp)
|
return ctx.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"gitrepo.ru/neonxp/nquest/api"
|
"gitrepo.ru/neonxp/nquest/api"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/models"
|
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/service"
|
"gitrepo.ru/neonxp/nquest/pkg/service"
|
||||||
|
"gitrepo.ru/neonxp/nquest/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
|
@ -116,7 +117,7 @@ func setUser(c echo.Context, user *models.User) error {
|
||||||
MaxAge: 86400 * 7,
|
MaxAge: 86400 * 7,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
}
|
}
|
||||||
sess.Values["userID"] = user.ID
|
sess.Values["userID"] = user.ID.String()
|
||||||
|
|
||||||
if err := sess.Save(c.Request(), c.Response()); err != nil {
|
if err := sess.Save(c.Request(), c.Response()); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -128,22 +129,24 @@ func setUser(c echo.Context, user *models.User) error {
|
||||||
func mapUser(c echo.Context, user *models.User) error {
|
func mapUser(c echo.Context, user *models.User) error {
|
||||||
games := make([]api.GameView, 0)
|
games := make([]api.GameView, 0)
|
||||||
for _, gc := range user.Games {
|
for _, gc := range user.Games {
|
||||||
if gc.Status == models.TaskFinished && gc.Task.Next == nil {
|
|
||||||
games = append(games, api.GameView{
|
games = append(games, api.GameView{
|
||||||
Id: int(gc.GameID),
|
Id: gc.GameID,
|
||||||
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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
level := utils.ExpToLevel(user.Experience)
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, &api.UserResponse{
|
return c.JSON(http.StatusOK, &api.UserResponse{
|
||||||
Id: int(user.ID),
|
Id: user.ID,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Experience: user.Experience,
|
Experience: user.Experience,
|
||||||
Level: user.Experience / 1000,
|
ExpToCurrentLevel: utils.LevelToExp(level),
|
||||||
|
ExpToNextLevel: utils.LevelToExp(level + 1),
|
||||||
|
Level: int(level),
|
||||||
Games: games,
|
Games: games,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
type GameCursor struct {
|
type GameCursor struct {
|
||||||
User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
||||||
UserID uint `gorm:"primaryKey"`
|
UserID uuid.UUID `gorm:"primaryKey"`
|
||||||
Game *Game `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
Game *Game `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
||||||
GameID uint `gorm:"primaryKey"`
|
GameID uuid.UUID `gorm:"primaryKey"`
|
||||||
Task *Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
Task *Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
||||||
TaskID uint `gorm:"primaryKey"`
|
TaskID uuid.UUID `gorm:"primaryKey"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
FinishedAt *time.Time
|
FinishedAt *time.Time
|
||||||
Status CursorStatus
|
Status CursorStatus
|
||||||
Codes []*Code `gorm:"many2many:passing_codes;"`
|
Codes []*Code `gorm:"many2many:passing_codes;"`
|
||||||
|
Finish bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type CursorStatus int
|
type CursorStatus int
|
||||||
|
|
|
@ -3,11 +3,12 @@ package models
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uuid.UUID `gorm:"primarykey" json:"id"`
|
||||||
CreatedAt time.Time `json:"-"`
|
CreatedAt time.Time `json:"-"`
|
||||||
UpdatedAt time.Time `json:"-"`
|
UpdatedAt time.Time `json:"-"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
Model
|
Model
|
||||||
|
|
||||||
Title string
|
Title string
|
||||||
Text string
|
Text string
|
||||||
MaxTime int
|
MaxTime int
|
||||||
GameID uint
|
GameID uuid.UUID
|
||||||
Solutions []*Solution `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
Solutions []*Solution `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
Next *Task `gorm:"foreignKey:NextID"`
|
TaskOrder uint
|
||||||
NextID *uint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Solution struct {
|
type Solution struct {
|
||||||
Model
|
Model
|
||||||
|
|
||||||
TaskID uint
|
TaskID uuid.UUID
|
||||||
After int
|
After int
|
||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
@ -24,7 +25,7 @@ type Solution struct {
|
||||||
type Code struct {
|
type Code struct {
|
||||||
Model
|
Model
|
||||||
|
|
||||||
TaskID uint
|
TaskID uuid.UUID
|
||||||
Code string `gorm:"index"`
|
Code string `gorm:"index"`
|
||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/models"
|
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
@ -15,6 +16,7 @@ var (
|
||||||
ErrInvalidCode = errors.New("invalid code")
|
ErrInvalidCode = errors.New("invalid code")
|
||||||
ErrOldCode = errors.New("old code")
|
ErrOldCode = errors.New("old code")
|
||||||
ErrGameFinished = errors.New("game finished")
|
ErrGameFinished = errors.New("game finished")
|
||||||
|
ErrNextLevel = errors.New("next level")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
|
@ -30,20 +32,18 @@ func NewEngine(db *gorm.DB) *Engine {
|
||||||
func (e *Engine) GetState(ctx context.Context, game *models.Game, user *models.User) (*models.GameCursor, error) {
|
func (e *Engine) GetState(ctx context.Context, game *models.Game, user *models.User) (*models.GameCursor, error) {
|
||||||
db := e.DB.WithContext(ctx)
|
db := e.DB.WithContext(ctx)
|
||||||
|
|
||||||
// Пытаемся получить GamePassing
|
// Пытаемся получить GameCursor
|
||||||
cursor := &models.GameCursor{
|
cursor := &models.GameCursor{
|
||||||
User: user,
|
User: user,
|
||||||
Game: game,
|
Game: game,
|
||||||
Task: game.Tasks[0],
|
Task: game.Tasks[0],
|
||||||
Status: models.TaskStarted,
|
Status: models.TaskStarted,
|
||||||
Codes: []*models.Code{},
|
Codes: make([]*models.Code, 0),
|
||||||
}
|
}
|
||||||
err := db.
|
err := db.
|
||||||
Where(`user_id = ? and game_id = ? and status = ?`, user.ID, game.ID, models.TaskStarted).
|
Where(`user_id = ? and game_id = ? and status = ?`, user.ID, game.ID, models.TaskStarted).
|
||||||
Preload("Task").
|
Preload("Task").
|
||||||
Preload("Task.Codes").
|
Preload("Task.Codes").
|
||||||
Preload("Task.Next").
|
|
||||||
Preload("Task.Next.Codes").
|
|
||||||
Preload("Codes").
|
Preload("Codes").
|
||||||
FirstOrCreate(cursor).
|
FirstOrCreate(cursor).
|
||||||
Error
|
Error
|
||||||
|
@ -62,10 +62,13 @@ func (e *Engine) GetState(ctx context.Context, game *models.Game, user *models.U
|
||||||
|
|
||||||
func (e *Engine) EnterCode(ctx context.Context, game *models.Game, user *models.User, code string) (*models.GameCursor, error) {
|
func (e *Engine) EnterCode(ctx context.Context, game *models.Game, user *models.User, code string) (*models.GameCursor, error) {
|
||||||
db := e.DB.WithContext(ctx)
|
db := e.DB.WithContext(ctx)
|
||||||
|
|
||||||
st, err := e.GetState(ctx, game, user)
|
st, err := e.GetState(ctx, game, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return st, db.Transaction(func(tx *gorm.DB) error {
|
||||||
code = strings.Trim(code, " \n\t")
|
code = strings.Trim(code, " \n\t")
|
||||||
code = strings.ToLower(code)
|
code = strings.ToLower(code)
|
||||||
var currentCode *models.Code
|
var currentCode *models.Code
|
||||||
|
@ -76,48 +79,74 @@ func (e *Engine) EnterCode(ctx context.Context, game *models.Game, user *models.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if currentCode == nil {
|
if currentCode == nil {
|
||||||
return nil, ErrInvalidCode
|
return ErrInvalidCode
|
||||||
}
|
}
|
||||||
for _, c := range st.Codes {
|
for _, c := range st.Codes {
|
||||||
if c.ID == currentCode.ID {
|
if c.ID == currentCode.ID {
|
||||||
return nil, ErrOldCode
|
return ErrOldCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
st.Codes = append(st.Codes, currentCode)
|
if err := db.Model(st).Association("Codes").Append(currentCode); err != nil {
|
||||||
|
return err
|
||||||
if err := db.Save(st).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(st.Codes) != len(st.Task.Codes) {
|
if len(st.Codes) != len(st.Task.Codes) {
|
||||||
return st, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Уровень пройден. Выдаем следующий
|
// Уровень пройден. Выдаем следующий
|
||||||
|
|
||||||
st.Status = models.TaskFinished
|
if err := db.Model(st).UpdateColumn("Status", models.TaskFinished).Error; err != nil {
|
||||||
if err := db.Save(st).Error; err != nil {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if st.Task.Next == nil {
|
nextTask, err := e.GetNext(ctx, game.ID, st.Task.TaskOrder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if nextTask == nil {
|
||||||
user.Experience += st.Game.Points
|
user.Experience += st.Game.Points
|
||||||
if err := db.Save(user).Error; err != nil {
|
if err := db.Model(user).UpdateColumn("Experience", user.Experience).Error; err != nil {
|
||||||
return nil, err
|
return err
|
||||||
|
}
|
||||||
|
if err := db.Model(st).UpdateColumn("Finish", true).Error; err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrGameFinished
|
return ErrGameFinished
|
||||||
}
|
}
|
||||||
|
|
||||||
newState := &models.GameCursor{
|
st = &models.GameCursor{
|
||||||
User: user,
|
User: user,
|
||||||
Game: game,
|
Game: game,
|
||||||
Task: st.Task.Next,
|
Task: nextTask,
|
||||||
Status: models.TaskStarted,
|
Status: models.TaskStarted,
|
||||||
Codes: []*models.Code{},
|
Codes: []*models.Code{},
|
||||||
}
|
}
|
||||||
|
if err := db.Create(st).Error; err != nil {
|
||||||
return newState, db.Create(newState).Error
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrNextLevel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) GetNext(ctx context.Context, gameID uuid.UUID, currentOrder uint) (*models.Task, error) {
|
||||||
|
var t models.Task
|
||||||
|
err := e.DB.WithContext(ctx).
|
||||||
|
Preload("Codes").
|
||||||
|
Preload("Solutions").
|
||||||
|
Order("task_order ASC").
|
||||||
|
First(&t, `game_id = ? AND task_order > ?`, gameID, currentOrder).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &t, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package service
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/models"
|
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
@ -18,12 +19,16 @@ func NewGame(db *gorm.DB) *Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *Game) GetByID(ctx context.Context, id uint) (*models.Game, error) {
|
func (gs *Game) GetByID(ctx context.Context, id uuid.UUID) (*models.Game, error) {
|
||||||
g := &models.Game{}
|
g := &models.Game{}
|
||||||
|
|
||||||
return g, gs.DB.
|
return g, gs.DB.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Preload("Tasks").
|
Preload("Tasks", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Order("tasks.task_order ASC")
|
||||||
|
}).
|
||||||
|
Preload("Tasks.Codes").
|
||||||
|
Preload("Tasks.Solutions").
|
||||||
First(g, id).
|
First(g, id).
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
@ -34,15 +39,17 @@ func (gs *Game) List(ctx context.Context) ([]*models.Game, error) {
|
||||||
return games, gs.DB.
|
return games, gs.DB.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Order("created_at DESC").
|
Order("created_at DESC").
|
||||||
Find(&games, "visible = true").
|
Preload("Tasks").
|
||||||
|
Preload("Authors").
|
||||||
|
Find(&games).
|
||||||
Limit(20).
|
Limit(20).
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *Game) GetTaskID(ctx context.Context, id uint) (*models.Task, error) {
|
func (gs *Game) GetTaskID(ctx context.Context, id uuid.UUID) (*models.Task, error) {
|
||||||
t := &models.Task{}
|
t := &models.Task{}
|
||||||
|
|
||||||
return t, gs.DB.WithContext(ctx).Preload("Next").First(t, id).Error
|
return t, gs.DB.WithContext(ctx).First(t, id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*models.Game, error) {
|
func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*models.Game, error) {
|
||||||
|
@ -59,19 +66,8 @@ func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*model
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game, error) {
|
func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game, error) {
|
||||||
return game, gs.DB.Transaction(func(tx *gorm.DB) error {
|
return game, gs.DB.
|
||||||
if err := tx.Create(game).Error; err != nil {
|
Session(&gorm.Session{FullSaveAssociations: true}).
|
||||||
return err
|
Create(game).
|
||||||
}
|
Error
|
||||||
for i, t := range game.Tasks {
|
|
||||||
if i < len(game.Tasks)-1 {
|
|
||||||
t.Next = game.Tasks[i+1]
|
|
||||||
if err := tx.Save(t).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
|
||||||
normalizer "github.com/dimuska139/go-email-normalizer"
|
normalizer "github.com/dimuska139/go-email-normalizer"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo-contrib/session"
|
"github.com/labstack/echo-contrib/session"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
@ -58,6 +59,9 @@ func (s *User) Register(ctx context.Context, username, email, password, password
|
||||||
}
|
}
|
||||||
|
|
||||||
u := &models.User{
|
u := &models.User{
|
||||||
|
Model: models.Model{
|
||||||
|
ID: uuid.New(),
|
||||||
|
},
|
||||||
Username: username,
|
Username: username,
|
||||||
Email: normalizer.NewNormalizer().Normalize(email),
|
Email: normalizer.NewNormalizer().Normalize(email),
|
||||||
Password: hex.EncodeToString(hashed),
|
Password: hex.EncodeToString(hashed),
|
||||||
|
@ -99,14 +103,12 @@ func (s *User) Login(ctx context.Context, email, password string) (*models.User,
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *User) GetUserByID(ctx context.Context, userID uint) (*models.User, error) {
|
func (s *User) GetUserByID(ctx context.Context, userID uuid.UUID) (*models.User, error) {
|
||||||
u := new(models.User)
|
u := new(models.User)
|
||||||
|
|
||||||
return u, s.DB.WithContext(ctx).
|
return u, s.DB.WithContext(ctx).
|
||||||
Preload("Games").
|
Preload("Games", `Finish = true`).
|
||||||
Preload("Games.Game").
|
Preload("Games.Game").
|
||||||
Preload("Games.Task").
|
|
||||||
Preload("Games.Task.Next").
|
|
||||||
First(u, userID).Error
|
First(u, userID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,12 +118,16 @@ func (s *User) GetUser(c echo.Context) *models.User {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, ok := sess.Values["userID"].(uint)
|
userID, ok := sess.Values["userID"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
uid, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
user, err := s.GetUserByID(c.Request().Context(), userID)
|
user, err := s.GetUserByID(c.Request().Context(), uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
11
pkg/utils/exp.go
Normal file
11
pkg/utils/exp.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
func ExpToLevel(exp int) int {
|
||||||
|
return int(math.Floor((math.Sqrt(625+100*float64(exp))-25)/50)) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func LevelToExp(level int) int {
|
||||||
|
return 25*level*level - 25*level
|
||||||
|
}
|
120
requests.http
120
requests.http
|
@ -33,13 +33,14 @@ POST http://localhost:8000/api/games
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"title": "Тестовая игра",
|
"title": "Тестовая игра 2",
|
||||||
"description": "Описание тестовой игры",
|
"description": "Описание тестовой игры",
|
||||||
"type": "city",
|
"type": "city",
|
||||||
|
"points": 500,
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"title": "Задание 1",
|
"title": "Задание 1",
|
||||||
"text": "Текст первого задания",
|
"text": "Текст первого задания.\n\n*Коды: `nq1111`*",
|
||||||
"codes": [
|
"codes": [
|
||||||
{
|
{
|
||||||
"description": "1+",
|
"description": "1+",
|
||||||
|
@ -96,11 +97,114 @@ Content-Type: application/json
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
GET http://localhost:8000/api/engine/1
|
|
||||||
|
POST http://localhost:8000/api/games
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Тестовая игра 3",
|
||||||
|
"description": "Описание тестовой игры",
|
||||||
|
"type": "city",
|
||||||
|
"points": 500,
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"title": "Задание 1",
|
||||||
|
"text": "Текст первого задания.\n\n*Коды: `nq1111`*",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq1111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Задание 2",
|
||||||
|
"text": "Текст второго задания",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq2211"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Задание 3",
|
||||||
|
"text": "Текст третьего задания",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq3311"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Задание 4",
|
||||||
|
"text": "Текст 4 задания",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq4411"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Задание 5",
|
||||||
|
"text": "Текст 5 задания",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq5511"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Задание 6",
|
||||||
|
"text": "Текст 6 задания",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq6611"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Задание 7",
|
||||||
|
"text": "Текст 7 задания",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq7711"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Задание 8",
|
||||||
|
"text": "Текст 8 задания",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq8811"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
POST http://localhost:8000/api/engine/1/code
|
GET http://localhost:8000/api/engine/2
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8000/api/engine/2/code
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -108,7 +212,7 @@ Content-Type: application/json
|
||||||
}
|
}
|
||||||
###
|
###
|
||||||
|
|
||||||
POST http://localhost:8000/api/engine/1/code
|
POST http://localhost:8000/api/engine/2/code
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -116,7 +220,7 @@ Content-Type: application/json
|
||||||
}
|
}
|
||||||
###
|
###
|
||||||
|
|
||||||
POST http://localhost:8000/api/engine/1/code
|
POST http://localhost:8000/api/engine/2/code
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -124,9 +228,9 @@ Content-Type: application/json
|
||||||
}
|
}
|
||||||
###
|
###
|
||||||
|
|
||||||
POST http://localhost:8000/api/engine/1/code
|
POST http://localhost:8000/api/engine/2/code
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"code": "NQ3322"
|
"code": "NQ3333"
|
||||||
}
|
}
|
|
@ -1,83 +0,0 @@
|
||||||
{{ template "header" . }}
|
|
||||||
|
|
||||||
{{ if .State }}
|
|
||||||
<h5 class="card-title mb-2">Уровень</h5>
|
|
||||||
<table class="table table-bordered mb-4">
|
|
||||||
<thead class="table-primary">
|
|
||||||
<th colspan="4">
|
|
||||||
{{.State.Task.Title}}
|
|
||||||
</th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Выдано:</td>
|
|
||||||
<td>{{ .State.CreatedAt.Format "15:04 02.01.2006" }}</td>
|
|
||||||
<td>Автопереход:</td>
|
|
||||||
<td>{{ .State.Deadline.Format "15:04 02.01.2006" }} (через {{ (.State.Deadline.Sub now) | toTime }})</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="4">{{ .State.Task.Text | markDown }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table class="table table-bordered mb-4">
|
|
||||||
<thead class="table-primary">
|
|
||||||
<th colspan="1">
|
|
||||||
Ввод кода
|
|
||||||
</th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<form method="post" action="/go/{{ .Game.ID }}/code">
|
|
||||||
<div class="col-lg-8 px-0">
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<input type="text" class="form-control" id="code" name="code">
|
|
||||||
</div>
|
|
||||||
<input type="submit" class="col-sm-2 btn btn-outline-primary" value="Ввод" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h5 class="card-title">История игры</h5>
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead class="table-primary">
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="thin">Уровень</th>
|
|
||||||
<th scope="col" class="thin">Время начала</th>
|
|
||||||
<th scope="col" class="thin">Время окончания</th>
|
|
||||||
<th scope="col">Статус</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{ range $i, $a := .History }}
|
|
||||||
<tr>
|
|
||||||
<td>{{ inc $i }}</td>
|
|
||||||
<td>{{ $a.CreatedAt.Format "15:04 02.01.2006" }}</td>
|
|
||||||
<td>{{ $a.Deadline.Format "15:04 02.01.2006" }}</td>
|
|
||||||
<td>
|
|
||||||
{{ if eq $a.Status 0 }}
|
|
||||||
Текущее
|
|
||||||
{{ else if eq $a.Status 1 }}
|
|
||||||
Пройден
|
|
||||||
{{ else if eq $a.Status 2 }}
|
|
||||||
Снят
|
|
||||||
{{ else if eq $a.Status 3 }}
|
|
||||||
Автопереход
|
|
||||||
{{ end }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{{ else }}
|
|
||||||
<p>Вам не предусмотренно следующее задание</p>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ template "footer" . }}
|
|
|
@ -1,48 +0,0 @@
|
||||||
{{ template "header" . }}
|
|
||||||
<h1>Текущие игры</h1>
|
|
||||||
{{ range .Games }}
|
|
||||||
<table class="table table-bordered mb-4">
|
|
||||||
<thead class="table-primary">
|
|
||||||
<th colspan="2">
|
|
||||||
{{.Title}}
|
|
||||||
</th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
Начало
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ .StartAt.Format "15:04 02.01.2006" }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">
|
|
||||||
{{ .Description | markDown }}
|
|
||||||
<div>
|
|
||||||
<a href="/go/{{ .ID }}" class="btn btn-primary">Войти в игру</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
Участвуют:
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<ul>
|
|
||||||
{{ range .Teams }}
|
|
||||||
<li><a href="/team/{{.Team.ID}}">{{.Team.Name}}</a></li>
|
|
||||||
{{ else }}
|
|
||||||
Никто пока не подал заявку
|
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{{ else }}
|
|
||||||
<p>
|
|
||||||
<strong>Игр пока не анонсировано</strong>
|
|
||||||
</p>
|
|
||||||
{{ end }}
|
|
||||||
{{ template "footer" . }}
|
|
|
@ -1,67 +0,0 @@
|
||||||
{{define "header"}}
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>nQuest</title>
|
|
||||||
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="/static/styles.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body data-bs-theme="dark">
|
|
||||||
<nav class="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
|
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="https://nquest.ru/">nQuest</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
|
||||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/">Игры</a>
|
|
||||||
</li>
|
|
||||||
{{ if .User }}
|
|
||||||
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/teams">Команды</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
<div class="d-flex">
|
|
||||||
{{ if .User }}
|
|
||||||
<span class="navbar-text me-2">
|
|
||||||
{{ .User.Username }}
|
|
||||||
{{ if .User.Team }}
|
|
||||||
(<a href="/team">{{.User.Team.Team.Name}}</a>)
|
|
||||||
{{ else }}
|
|
||||||
(без команды)
|
|
||||||
{{ end }}
|
|
||||||
</span>
|
|
||||||
<form method="POST" action="/user/logout">
|
|
||||||
<input type="submit" class="btn btn-defult" value="Выход" /></a>
|
|
||||||
</form>
|
|
||||||
{{ else }}
|
|
||||||
<div class="btn-group">
|
|
||||||
<a class="btn btn-success" href="/user/login">Вход</a>
|
|
||||||
<a class="btn btn-outline-success" href="/user/register">Регистрация</a>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container my-5">
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{define "footer"}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
{{end}}
|
|
|
@ -1,27 +0,0 @@
|
||||||
{{ template "header" . }}
|
|
||||||
<h1>Вход</h1>
|
|
||||||
<form method="POST">
|
|
||||||
<div class="col-lg-8 px-0">
|
|
||||||
{{ if .Error }}
|
|
||||||
<div class="alert alert-danger" role="alert">{{ .Error }}</div>
|
|
||||||
{{ end }}
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="staticEmail" class="col-sm-2 col-form-label">Email</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<input type="text" class="form-control" id="staticEmail" value="{{ .Email }}" name="email">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="inputPassword" class="col-sm-2 col-form-label">Пароль</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<input type="password" class="form-control" id="inputPassword" name="password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<div class="col-sm-10 offset-md-2">
|
|
||||||
<input type="submit" class="form-control btn btn-primary" value="Вход">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{{ template "footer" . }}
|
|
|
@ -1,39 +0,0 @@
|
||||||
{{ template "header" . }}
|
|
||||||
<h1>Регистрация</h1>
|
|
||||||
<form method="POST">
|
|
||||||
<div class="col-lg-8 px-0">
|
|
||||||
{{ if .Error }}
|
|
||||||
<div class="alert alert-danger" role="alert">{{ .Error }}</div>
|
|
||||||
{{ end }}
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="staticEmail" class="col-sm-4 col-form-label">Имя пользователя</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input type="text" class="form-control" name="username" value="{{ .Username }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="staticEmail" class="col-sm-4 col-form-label">Email</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input type="text" class="form-control" name="email" value="{{ .Email }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="inputPassword" class="col-sm-4 col-form-label">Пароль</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input type="password" class="form-control" name="password" value="{{ .Password }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="inputPassword" class="col-sm-4 col-form-label">Повторите пароль</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input type="password" class="form-control" name="password_2" value="{{ .Password2 }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<div class="col-sm-8 offset-md-2">
|
|
||||||
<input type="submit" class="form-control btn btn-primary" value="Регистрация">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{{ template "footer" . }}
|
|
Loading…
Reference in a new issue