mirror of https://gitee.com/answerdev/answer.git
Merge branch 'test' into 'ai_0.3_dashboard'
# Conflicts: # cmd/answer/wire_gen.go
This commit is contained in:
commit
cd8b3103d0
|
@ -21,4 +21,5 @@ Thumbs*.db
|
|||
tmp
|
||||
vendor/
|
||||
.husky
|
||||
answer-data/
|
||||
/answer-data/
|
||||
/answer
|
||||
|
|
|
@ -38,9 +38,7 @@ stages:
|
|||
stage: push
|
||||
extends: .docker-build-push
|
||||
only:
|
||||
- dev
|
||||
- master
|
||||
- main
|
||||
- test
|
||||
variables:
|
||||
DockerNamespace: sf_app
|
||||
DockerImage: answer
|
||||
|
@ -52,7 +50,7 @@ stages:
|
|||
stage: deploy-dev
|
||||
extends: .deploy-helm
|
||||
only:
|
||||
- main
|
||||
- test
|
||||
variables:
|
||||
LoadBalancerIP: 10.0.10.98
|
||||
KubernetesCluster: dev
|
||||
|
|
8
Makefile
8
Makefile
|
@ -13,6 +13,13 @@ GO=$(GO_ENV) $(shell which go)
|
|||
build:
|
||||
@$(GO_ENV) $(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)
|
||||
|
||||
# https://dev.to/thewraven/universal-macos-binaries-with-go-1-16-3mm3
|
||||
universal:
|
||||
@GOOS=darwin GOARCH=amd64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_amd64 $(DIR_SRC)
|
||||
@GOOS=darwin GOARCH=arm64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_arm64 $(DIR_SRC)
|
||||
@lipo -create -output ${BIN} ${BIN}_amd64 ${BIN}_arm64
|
||||
@rm -f ${BIN}_amd64 ${BIN}_arm64
|
||||
|
||||
generate:
|
||||
go get github.com/google/wire/cmd/wire@latest
|
||||
go generate ./...
|
||||
|
@ -23,6 +30,7 @@ test:
|
|||
|
||||
# clean all build result
|
||||
clean:
|
||||
|
||||
@$(GO) clean ./...
|
||||
@rm -f $(BIN)
|
||||
|
||||
|
|
|
@ -59,6 +59,8 @@ To run answer, use:
|
|||
Short: "init answer application",
|
||||
Long: `init answer application`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
// installwebapi := server.NewInstallHTTPServer()
|
||||
// installwebapi.Run(":8088")
|
||||
cli.InstallAllInitialEnvironment(dataDirPath)
|
||||
c, err := readConfig()
|
||||
if err != nil {
|
||||
|
|
|
@ -79,7 +79,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
langController := controller.NewLangController(i18nTranslator)
|
||||
engine, err := data.NewDB(debug, dbConf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -93,17 +92,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
siteInfoRepo := site_info.NewSiteInfo(dataData)
|
||||
configRepo := config.NewConfigRepo(dataData)
|
||||
emailRepo := export.NewEmailRepo(dataData)
|
||||
emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo)
|
||||
siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService)
|
||||
langController := controller.NewLangController(i18nTranslator, siteInfoService)
|
||||
authRepo := auth.NewAuthRepo(dataData)
|
||||
authService := auth2.NewAuthService(authRepo)
|
||||
configRepo := config.NewConfigRepo(dataData)
|
||||
userRepo := user.NewUserRepo(dataData, configRepo)
|
||||
uniqueIDRepo := unique.NewUniqueIDRepo(dataData)
|
||||
activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configRepo)
|
||||
userRankRepo := rank.NewUserRankRepo(dataData, configRepo)
|
||||
userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo)
|
||||
emailRepo := export.NewEmailRepo(dataData)
|
||||
siteInfoRepo := site_info.NewSiteInfo(dataData)
|
||||
emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo)
|
||||
userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf)
|
||||
captchaRepo := captcha.NewCaptchaRepo(dataData)
|
||||
captchaService := action.NewCaptchaService(captchaRepo)
|
||||
|
|
83
docs/docs.go
83
docs/docs.go
|
@ -62,6 +62,12 @@ const docTemplate = `{
|
|||
"description": "answer id or question title",
|
||||
"name": "query",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "question id",
|
||||
"name": "question_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -143,11 +149,6 @@ const docTemplate = `{
|
|||
},
|
||||
"/answer/admin/api/language/options": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get language options",
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
@ -1443,11 +1444,6 @@ const docTemplate = `{
|
|||
},
|
||||
"/answer/api/v1/language/options": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get language options",
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
@ -3402,6 +3398,52 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/interface": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "UserUpdateInterface update user interface config",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "UserUpdateInterface update user interface config",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "access-token",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "UpdateInfoRequest",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.UpdateUserInterfaceRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/login/email": {
|
||||
"post": {
|
||||
"description": "UserEmailLogin",
|
||||
|
@ -4833,6 +4875,10 @@ const docTemplate = `{
|
|||
"description": "is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"language": {
|
||||
"description": "language",
|
||||
"type": "string"
|
||||
},
|
||||
"last_login_date": {
|
||||
"description": "last login date",
|
||||
"type": "integer"
|
||||
|
@ -4929,6 +4975,10 @@ const docTemplate = `{
|
|||
"description": "is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"language": {
|
||||
"description": "language",
|
||||
"type": "string"
|
||||
},
|
||||
"last_login_date": {
|
||||
"description": "last login date",
|
||||
"type": "integer"
|
||||
|
@ -5594,6 +5644,19 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.UpdateUserInterfaceRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"language"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
"description": "language",
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.UpdateUserStatusReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
|
@ -50,6 +50,12 @@
|
|||
"description": "answer id or question title",
|
||||
"name": "query",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "question id",
|
||||
"name": "question_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -131,11 +137,6 @@
|
|||
},
|
||||
"/answer/admin/api/language/options": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get language options",
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
@ -1431,11 +1432,6 @@
|
|||
},
|
||||
"/answer/api/v1/language/options": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get language options",
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
@ -3390,6 +3386,52 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/interface": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "UserUpdateInterface update user interface config",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "UserUpdateInterface update user interface config",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "access-token",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "UpdateInfoRequest",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.UpdateUserInterfaceRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/login/email": {
|
||||
"post": {
|
||||
"description": "UserEmailLogin",
|
||||
|
@ -4821,6 +4863,10 @@
|
|||
"description": "is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"language": {
|
||||
"description": "language",
|
||||
"type": "string"
|
||||
},
|
||||
"last_login_date": {
|
||||
"description": "last login date",
|
||||
"type": "integer"
|
||||
|
@ -4917,6 +4963,10 @@
|
|||
"description": "is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"language": {
|
||||
"description": "language",
|
||||
"type": "string"
|
||||
},
|
||||
"last_login_date": {
|
||||
"description": "last login date",
|
||||
"type": "integer"
|
||||
|
@ -5582,6 +5632,19 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.UpdateUserInterfaceRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"language"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
"description": "language",
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.UpdateUserStatusReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
|
@ -652,6 +652,9 @@ definitions:
|
|||
is_admin:
|
||||
description: is admin
|
||||
type: boolean
|
||||
language:
|
||||
description: language
|
||||
type: string
|
||||
last_login_date:
|
||||
description: last login date
|
||||
type: integer
|
||||
|
@ -723,6 +726,9 @@ definitions:
|
|||
is_admin:
|
||||
description: is admin
|
||||
type: boolean
|
||||
language:
|
||||
description: language
|
||||
type: string
|
||||
last_login_date:
|
||||
description: last login date
|
||||
type: integer
|
||||
|
@ -1203,6 +1209,15 @@ definitions:
|
|||
- synonym_tag_list
|
||||
- tag_id
|
||||
type: object
|
||||
schema.UpdateUserInterfaceRequest:
|
||||
properties:
|
||||
language:
|
||||
description: language
|
||||
maxLength: 100
|
||||
type: string
|
||||
required:
|
||||
- language
|
||||
type: object
|
||||
schema.UpdateUserStatusReq:
|
||||
properties:
|
||||
status:
|
||||
|
@ -1402,6 +1417,10 @@ paths:
|
|||
in: query
|
||||
name: query
|
||||
type: string
|
||||
- description: question id
|
||||
in: query
|
||||
name: question_id
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -1465,8 +1484,6 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get language options
|
||||
tags:
|
||||
- Lang
|
||||
|
@ -2251,8 +2268,6 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get language options
|
||||
tags:
|
||||
- Lang
|
||||
|
@ -3439,6 +3454,35 @@ paths:
|
|||
summary: UserUpdateInfo update user info
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/interface:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: UserUpdateInterface update user interface config
|
||||
parameters:
|
||||
- description: access-token
|
||||
in: header
|
||||
name: Authorization
|
||||
required: true
|
||||
type: string
|
||||
- description: UpdateInfoRequest
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.UpdateUserInterfaceRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: UserUpdateInterface update user interface config
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/login/email:
|
||||
post:
|
||||
consumes:
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# all support language
|
||||
language_options:
|
||||
- label: "简体中文(CN)"
|
||||
value: "zh_CN"
|
||||
- label: "English(US)"
|
||||
value: "en_US"
|
|
@ -0,0 +1,59 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/answerdev/answer/ui"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
const UIStaticPath = "build/static"
|
||||
|
||||
type _resource struct {
|
||||
fs embed.FS
|
||||
}
|
||||
|
||||
// Open to implement the interface by http.FS required
|
||||
func (r *_resource) Open(name string) (fs.File, error) {
|
||||
name = fmt.Sprintf(UIStaticPath+"/%s", name)
|
||||
log.Debugf("open static path %s", name)
|
||||
return r.fs.Open(name)
|
||||
}
|
||||
|
||||
// NewHTTPServer new http server.
|
||||
func NewInstallHTTPServer() *gin.Engine {
|
||||
r := gin.New()
|
||||
gin.SetMode(gin.DebugMode)
|
||||
|
||||
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK??") })
|
||||
|
||||
// gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
r.StaticFS("/static", http.FS(&_resource{
|
||||
fs: ui.Build,
|
||||
}))
|
||||
|
||||
installApi := r.Group("")
|
||||
installApi.GET("/install", Install)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func Install(c *gin.Context) {
|
||||
filePath := ""
|
||||
var file []byte
|
||||
var err error
|
||||
filePath = "build/index.html"
|
||||
c.Header("content-type", "text/html;charset=utf-8")
|
||||
file, err = ui.Build.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(file))
|
||||
}
|
|
@ -1,17 +1,58 @@
|
|||
package translator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/wire"
|
||||
myTran "github.com/segmentfault/pacman/contrib/i18n"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// ProviderSet is providers.
|
||||
var ProviderSet = wire.NewSet(NewTranslator)
|
||||
var GlobalTrans i18n.Translator
|
||||
|
||||
// LangOption language option
|
||||
type LangOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// LanguageOptions language
|
||||
var LanguageOptions []*LangOption
|
||||
|
||||
// NewTranslator new a translator
|
||||
func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
|
||||
GlobalTrans, err = myTran.NewTranslator(c.BundleDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read i18n file failed: %s", err)
|
||||
}
|
||||
|
||||
s := struct {
|
||||
LangOption []*LangOption `json:"language_options"`
|
||||
}{}
|
||||
err = yaml.Unmarshal(i18nFile, &s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("i18n file parsing failed: %s", err)
|
||||
}
|
||||
LanguageOptions = s.LangOption
|
||||
return GlobalTrans, err
|
||||
}
|
||||
|
||||
// CheckLanguageIsValid check user input language is valid
|
||||
func CheckLanguageIsValid(lang string) bool {
|
||||
for _, option := range LanguageOptions {
|
||||
if option.Value == lang {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -4,18 +4,20 @@ import (
|
|||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
)
|
||||
|
||||
type LangController struct {
|
||||
translator i18n.Translator
|
||||
translator i18n.Translator
|
||||
siteInfoService *service.SiteInfoService
|
||||
}
|
||||
|
||||
// NewLangController new language controller.
|
||||
func NewLangController(tr i18n.Translator) *LangController {
|
||||
return &LangController{translator: tr}
|
||||
func NewLangController(tr i18n.Translator, siteInfoService *service.SiteInfoService) *LangController {
|
||||
return &LangController{translator: tr, siteInfoService: siteInfoService}
|
||||
}
|
||||
|
||||
// GetLangMapping get language config mapping
|
||||
|
@ -33,15 +35,38 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// GetLangOptions Get language options
|
||||
// GetAdminLangOptions Get language options
|
||||
// @Summary Get language options
|
||||
// @Description Get language options
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Lang
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/language/options [get]
|
||||
// @Router /answer/admin/api/language/options [get]
|
||||
func (u *LangController) GetLangOptions(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, nil, schema.GetLangOptions)
|
||||
func (u *LangController) GetAdminLangOptions(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
|
||||
}
|
||||
|
||||
// GetUserLangOptions Get language options
|
||||
// @Summary Get language options
|
||||
// @Description Get language options
|
||||
// @Tags Lang
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/language/options [get]
|
||||
func (u *LangController) GetUserLangOptions(ctx *gin.Context) {
|
||||
siteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
options := translator.LanguageOptions
|
||||
if len(siteInterfaceResp.Language) > 0 {
|
||||
defaultOption := []*translator.LangOption{
|
||||
{Label: "Default", Value: siteInterfaceResp.Language},
|
||||
}
|
||||
options = append(defaultOption, options...)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, options)
|
||||
}
|
||||
|
|
|
@ -391,6 +391,7 @@ func (qc *QuestionController) CmsSearchList(ctx *gin.Context) {
|
|||
// @Param page_size query int false "page size"
|
||||
// @Param status query string false "user status" Enums(available,deleted)
|
||||
// @Param query query string false "answer id or question title"
|
||||
// @Param question_id query string false "question id"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/admin/api/answer/page [get]
|
||||
func (qc *QuestionController) CmsSearchAnswerList(ctx *gin.Context) {
|
||||
|
|
|
@ -373,6 +373,27 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UserUpdateInterface update user interface config
|
||||
// @Summary UserUpdateInterface update user interface config
|
||||
// @Description UserUpdateInterface update user interface config
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param Authorization header string true "access-token"
|
||||
// @Param data body schema.UpdateUserInterfaceRequest true "UpdateInfoRequest"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/user/interface [put]
|
||||
func (uc *UserController) UserUpdateInterface(ctx *gin.Context) {
|
||||
req := &schema.UpdateUserInterfaceRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserId = middleware.GetLoginUserIDFromContext(ctx)
|
||||
err := uc.userService.UserUpdateInterface(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UploadUserAvatar godoc
|
||||
// @Summary UserUpdateInfo
|
||||
// @Description UserUpdateInfo
|
||||
|
|
|
@ -40,11 +40,12 @@ type AnswerSearch struct {
|
|||
}
|
||||
|
||||
type CmsAnswerSearch struct {
|
||||
Page int `json:"page" form:"page"` // Query number of pages
|
||||
PageSize int `json:"page_size" form:"page_size"` // Search page size
|
||||
Status int `json:"-" form:"-"`
|
||||
StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 Deleted
|
||||
Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string
|
||||
Page int `json:"page" form:"page"` // Query number of pages
|
||||
PageSize int `json:"page_size" form:"page_size"` // Search page size
|
||||
Status int `json:"-" form:"-"`
|
||||
StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 Deleted
|
||||
Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string
|
||||
QuestionID string `validate:"omitempty,gt=0,lte=24" json:"question_id" form:"question_id" ` //Query string
|
||||
}
|
||||
|
||||
type AdminSetAnswerStatusRequest struct {
|
||||
|
|
|
@ -45,6 +45,7 @@ type User struct {
|
|||
Location string `xorm:"not null default '' VARCHAR(100) location"`
|
||||
IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"`
|
||||
IsAdmin bool `xorm:"not null default false BOOL is_admin"`
|
||||
Language string `xorm:"not null default '' VARCHAR(100) language"`
|
||||
}
|
||||
|
||||
// TableName user table name
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
|
@ -43,6 +42,7 @@ var noopMigration = func(_ *xorm.Engine) error { return nil }
|
|||
var migrations = []Migration{
|
||||
// 0->1
|
||||
NewMigration("this is first version, no operation", noopMigration),
|
||||
NewMigration("add user language", addUserLanguage),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
@ -86,17 +86,17 @@ func Migrate(dataConf *data.Database) error {
|
|||
expectedVersion := ExpectedVersion()
|
||||
|
||||
for currentDBVersion < expectedVersion {
|
||||
log.Infof("[migrate] current db version is %d, try to migrate version %d, latest version is %d",
|
||||
fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n",
|
||||
currentDBVersion, currentDBVersion+1, expectedVersion)
|
||||
migrationFunc := migrations[currentDBVersion]
|
||||
log.Infof("[migrate] try to migrate db version %d, description: %s", currentDBVersion+1, migrationFunc.Description())
|
||||
fmt.Printf("[migrate] try to migrate db version %d, description: %s\n", currentDBVersion+1, migrationFunc.Description())
|
||||
if err := migrationFunc.Migrate(engine); err != nil {
|
||||
log.Errorf("[migrate] migrate to db version %d failed: ", currentDBVersion+1, err.Error())
|
||||
fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error())
|
||||
return err
|
||||
}
|
||||
log.Infof("[migrate] migrate to db version %d success", currentDBVersion+1)
|
||||
fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1)
|
||||
if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil {
|
||||
log.Errorf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
|
||||
fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
|
||||
return err
|
||||
}
|
||||
currentDBVersion++
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addUserLanguage(x *xorm.Engine) error {
|
||||
type User struct {
|
||||
Language string `xorm:"not null default '' VARCHAR(100) language"`
|
||||
}
|
||||
return x.Sync(new(User))
|
||||
}
|
|
@ -268,6 +268,13 @@ func (ar *answerRepo) CmsSearchList(ctx context.Context, search *entity.CmsAnswe
|
|||
}
|
||||
}
|
||||
|
||||
// check search by question id
|
||||
if len(search.QuestionID) > 0 {
|
||||
session.And(builder.Eq{
|
||||
"question_id": search.QuestionID,
|
||||
})
|
||||
}
|
||||
|
||||
offset := search.Page * search.PageSize
|
||||
session.
|
||||
OrderBy("a.updated_at desc").
|
||||
|
|
|
@ -101,6 +101,14 @@ func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err
|
|||
return
|
||||
}
|
||||
|
||||
func (ur *userRepo) UpdateLanguage(ctx context.Context, userID, language string) (err error) {
|
||||
_, err = ur.data.DB.Where("id = ?", userID).Update(&entity.User{Language: language})
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateInfo update user info
|
||||
func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) {
|
||||
_, err = ur.data.DB.Where("id = ?", userInfo.ID).
|
||||
|
|
|
@ -82,7 +82,7 @@ func NewAnswerAPIRouter(
|
|||
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
||||
// i18n
|
||||
r.GET("/language/config", a.langController.GetLangMapping)
|
||||
r.GET("/language/options", a.langController.GetLangOptions)
|
||||
r.GET("/language/options", a.langController.GetUserLangOptions)
|
||||
|
||||
// comment
|
||||
r.GET("/comment/page", a.commentController.GetCommentWithPage)
|
||||
|
@ -180,6 +180,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
// user
|
||||
r.PUT("/user/password", a.userController.UserModifyPassWord)
|
||||
r.PUT("/user/info", a.userController.UserUpdateInfo)
|
||||
r.PUT("/user/interface", a.userController.UserUpdateInterface)
|
||||
r.POST("/user/avatar/upload", a.userController.UploadUserAvatar)
|
||||
r.POST("/user/post/file", a.userController.UploadUserPostFile)
|
||||
r.POST("/user/notice/set", a.userController.UserNoticeSet)
|
||||
|
@ -216,7 +217,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
|
|||
r.GET("/reasons", a.reasonController.Reasons)
|
||||
|
||||
// language
|
||||
r.GET("/language/options", a.langController.GetLangOptions)
|
||||
r.GET("/language/options", a.langController.GetAdminLangOptions)
|
||||
|
||||
// theme
|
||||
r.GET("/theme/options", a.themeController.GetThemeOptions)
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/answerdev/answer/i18n"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/ui"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -40,6 +42,7 @@ func (r *_resource) Open(name string) (fs.File, error) {
|
|||
// Register a new static resource which generated by ui directory
|
||||
func (a *UIRouter) Register(r *gin.Engine) {
|
||||
staticPath := os.Getenv("ANSWER_STATIC_PATH")
|
||||
r.StaticFS("/i18n/", http.FS(i18n.I18n))
|
||||
|
||||
// if ANSWER_STATIC_PATH is set and not empty, ignore embed resource
|
||||
if staticPath != "" {
|
||||
|
@ -66,6 +69,59 @@ func (a *UIRouter) Register(r *gin.Engine) {
|
|||
fs: ui.Build,
|
||||
}))
|
||||
|
||||
// Install godoc
|
||||
// @Summary Install
|
||||
// @Description Install
|
||||
// @Tags Install
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /install [get]
|
||||
r.GET("/install", func(c *gin.Context) {
|
||||
filePath := ""
|
||||
var file []byte
|
||||
var err error
|
||||
filePath = "build/index.html"
|
||||
c.Header("content-type", "text/html;charset=utf-8")
|
||||
file, err = ui.Build.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(file))
|
||||
})
|
||||
|
||||
// Install godoc
|
||||
// @Summary Install
|
||||
// @Description Install
|
||||
// @Tags Install
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.FollowReq true "follow"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /install/db/check [put]
|
||||
r.PUT("/install/db/check", func(c *gin.Context) {
|
||||
handler.HandleResponse(c, nil, gin.H{})
|
||||
})
|
||||
|
||||
// Install godoc
|
||||
// @Summary Install
|
||||
// @Description Install
|
||||
// @Tags Install
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.FollowReq true "follow"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /install [put]
|
||||
r.PUT("/install", func(c *gin.Context) {
|
||||
handler.HandleResponse(c, nil, gin.H{})
|
||||
})
|
||||
|
||||
r.PUT("/install/siteconfig", func(c *gin.Context) {
|
||||
handler.HandleResponse(c, nil, gin.H{})
|
||||
})
|
||||
|
||||
// specify the not router for default routes and redirect
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
name := c.Request.URL.Path
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
package schema
|
||||
|
||||
// GetLangOption get label option
|
||||
type GetLangOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
var GetLangOptions = []*GetLangOption{
|
||||
{
|
||||
Label: "English(US)",
|
||||
Value: "en_US",
|
||||
},
|
||||
{
|
||||
Label: "中文(CN)",
|
||||
Value: "zh_CN",
|
||||
},
|
||||
}
|
|
@ -62,6 +62,8 @@ type GetUserResp struct {
|
|||
Location string `json:"location"`
|
||||
// ip info
|
||||
IPInfo string `json:"ip_info"`
|
||||
// language
|
||||
Language string `json:"language"`
|
||||
// access token
|
||||
AccessToken string `json:"access_token"`
|
||||
// is admin
|
||||
|
@ -305,6 +307,14 @@ func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error)
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// UpdateUserInterfaceRequest update user interface request
|
||||
type UpdateUserInterfaceRequest struct {
|
||||
// language
|
||||
Language string `validate:"required,gt=1,lte=100" json:"language"`
|
||||
// user id
|
||||
UserId string `json:"-" `
|
||||
}
|
||||
|
||||
type UserRetrievePassWordRequest struct {
|
||||
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail
|
||||
CaptchaID string `json:"captcha_id" ` // captcha_id
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
|
@ -73,10 +74,9 @@ func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGe
|
|||
|
||||
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
|
||||
var (
|
||||
siteType = "interface"
|
||||
themeExist,
|
||||
langExist bool
|
||||
content []byte
|
||||
siteType = "interface"
|
||||
themeExist bool
|
||||
content []byte
|
||||
)
|
||||
|
||||
// check theme
|
||||
|
@ -92,13 +92,7 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site
|
|||
}
|
||||
|
||||
// check language
|
||||
for _, lang := range schema.GetLangOptions {
|
||||
if lang.Value == req.Language {
|
||||
langExist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !langExist {
|
||||
if !translator.CheckLanguageIsValid(req.Language) {
|
||||
err = errors.BadRequest(reason.LangNotFound)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ type UserRepo interface {
|
|||
UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error
|
||||
UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error
|
||||
UpdateEmail(ctx context.Context, userID, email string) error
|
||||
UpdateLanguage(ctx context.Context, userID, language string) error
|
||||
UpdatePass(ctx context.Context, userID, pass string) error
|
||||
UpdateInfo(ctx context.Context, userInfo *entity.User) (err error)
|
||||
GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"github.com/Chain-Zhang/pinyin"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/activity"
|
||||
|
@ -283,6 +284,18 @@ func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, er
|
|||
return has, nil
|
||||
}
|
||||
|
||||
// UserUpdateInterface update user interface
|
||||
func (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.UpdateUserInterfaceRequest) (err error) {
|
||||
if !translator.CheckLanguageIsValid(req.Language) {
|
||||
return errors.BadRequest(reason.LangNotFound)
|
||||
}
|
||||
err = us.userRepo.UpdateLanguage(ctx, req.UserId, req.Language)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserRegisterByEmail user register
|
||||
func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) (
|
||||
resp *schema.GetUserResp, err error,
|
||||
|
|
|
@ -64,7 +64,7 @@ module.exports = {
|
|||
position: 'before',
|
||||
},
|
||||
{
|
||||
pattern: '@answer/**',
|
||||
pattern: '@/**',
|
||||
group: 'internal',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
extends: ['@commitlint/routes-conventional'],
|
||||
};
|
||||
|
|
|
@ -8,13 +8,6 @@ module.exports = {
|
|||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'@answer/pages': path.resolve(__dirname, 'src/pages'),
|
||||
'@answer/components': path.resolve(__dirname, 'src/components'),
|
||||
'@answer/stores': path.resolve(__dirname, 'src/stores'),
|
||||
'@answer/hooks': path.resolve(__dirname, 'src/hooks'),
|
||||
'@answer/utils': path.resolve(__dirname, 'src/utils'),
|
||||
'@answer/common': path.resolve(__dirname, 'src/common'),
|
||||
'@answer/api': path.resolve(__dirname, 'src/services/api'),
|
||||
};
|
||||
|
||||
return config;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import router from '@/router';
|
||||
import { routes, createBrowserRouter } from '@/router';
|
||||
|
||||
function App() {
|
||||
const router = createBrowserRouter(routes);
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
export const LOGIN_NEED_BACK = [
|
||||
'/users/login',
|
||||
'/users/register',
|
||||
'/users/account-recovery',
|
||||
'/users/password-reset',
|
||||
];
|
||||
export const DEFAULT_LANG = 'en_US';
|
||||
export const CURRENT_LANG_STORAGE_KEY = '_a_lang__';
|
||||
export const LOGGED_USER_STORAGE_KEY = '_a_lui_';
|
||||
export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_';
|
||||
export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_';
|
||||
export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
|
||||
|
||||
export const ADMIN_LIST_STATUS = {
|
||||
// normal;
|
||||
|
@ -56,3 +56,229 @@ export const ADMIN_NAV_MENUS = [
|
|||
child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }],
|
||||
},
|
||||
];
|
||||
// timezones
|
||||
export const TIMEZONES = [
|
||||
{
|
||||
label: 'UTC-12',
|
||||
value: 'UTC-12',
|
||||
},
|
||||
{
|
||||
label: 'UTC-11:30',
|
||||
value: 'UTC-11.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-11',
|
||||
value: 'UTC-11',
|
||||
},
|
||||
{
|
||||
label: 'UTC-10:30',
|
||||
value: 'UTC-10.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-10',
|
||||
value: 'UTC-10',
|
||||
},
|
||||
{
|
||||
label: 'UTC-9:30',
|
||||
value: 'UTC-9.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-9',
|
||||
value: 'UTC-9',
|
||||
},
|
||||
{
|
||||
label: 'UTC-8:30',
|
||||
value: 'UTC-8.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-8',
|
||||
value: 'UTC-8',
|
||||
},
|
||||
{
|
||||
label: 'UTC-7:30',
|
||||
value: 'UTC-7.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-7',
|
||||
value: 'UTC-7',
|
||||
},
|
||||
{
|
||||
label: 'UTC-6:30',
|
||||
value: 'UTC-6.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-6',
|
||||
value: 'UTC-6',
|
||||
},
|
||||
{
|
||||
label: 'UTC-5:30',
|
||||
value: 'UTC-5.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-5',
|
||||
value: 'UTC-5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-4:30',
|
||||
value: 'UTC-4.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-4',
|
||||
value: 'UTC-4',
|
||||
},
|
||||
{
|
||||
label: 'UTC-3:30',
|
||||
value: 'UTC-3.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-3',
|
||||
value: 'UTC-3',
|
||||
},
|
||||
{
|
||||
label: 'UTC-2:30',
|
||||
value: 'UTC-2.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-2',
|
||||
value: 'UTC-2',
|
||||
},
|
||||
{
|
||||
label: 'UTC-1:30',
|
||||
value: 'UTC-1.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC-1',
|
||||
value: 'UTC-1',
|
||||
},
|
||||
{
|
||||
label: 'UTC-0:30',
|
||||
value: 'UTC-0.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+0',
|
||||
value: 'UTC+0',
|
||||
},
|
||||
{
|
||||
label: 'UTC+0:30',
|
||||
value: 'UTC+0.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+1',
|
||||
value: 'UTC+1',
|
||||
},
|
||||
{
|
||||
label: 'UTC+1:30',
|
||||
value: 'UTC+1.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+2',
|
||||
value: 'UTC+2',
|
||||
},
|
||||
{
|
||||
label: 'UTC+2:30',
|
||||
value: 'UTC+2.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+3',
|
||||
value: 'UTC+3',
|
||||
},
|
||||
{
|
||||
label: 'UTC+3:30',
|
||||
|
||||
value: 'UTC+3.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+4',
|
||||
value: 'UTC+4',
|
||||
},
|
||||
{
|
||||
label: 'UTC+4:30',
|
||||
value: 'UTC+4.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+5',
|
||||
value: 'UTC+5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+5:30',
|
||||
value: 'UTC+5.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+5:45',
|
||||
value: 'UTC+5.75',
|
||||
},
|
||||
{
|
||||
label: 'UTC+6',
|
||||
value: 'UTC+6',
|
||||
},
|
||||
{
|
||||
label: 'UTC+6:30',
|
||||
|
||||
value: 'UTC+6.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+7',
|
||||
value: 'UTC+7',
|
||||
},
|
||||
{
|
||||
label: 'UTC+7:30',
|
||||
value: 'UTC+7.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+8',
|
||||
value: 'UTC+8',
|
||||
},
|
||||
{
|
||||
label: 'UTC+8:30',
|
||||
value: 'UTC+8.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+8:45',
|
||||
value: 'UTC+8.75',
|
||||
},
|
||||
{
|
||||
label: 'UTC+9',
|
||||
value: 'UTC+9',
|
||||
},
|
||||
{
|
||||
label: 'UTC+9:30',
|
||||
value: 'UTC+9.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+10',
|
||||
value: 'UTC+10',
|
||||
},
|
||||
{
|
||||
label: 'UTC+10:30',
|
||||
value: 'UTC+10.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+11',
|
||||
value: 'UTC+11',
|
||||
},
|
||||
{
|
||||
label: 'UTC+11:30',
|
||||
value: 'UTC+11.5',
|
||||
},
|
||||
{
|
||||
label: 'UTC+12',
|
||||
value: 'UTC+12',
|
||||
},
|
||||
{
|
||||
label: 'UTC+12:45',
|
||||
value: 'UTC+12.75',
|
||||
},
|
||||
{
|
||||
label: 'UTC+13',
|
||||
value: 'UTC+13',
|
||||
},
|
||||
{
|
||||
label: 'UTC+13:45',
|
||||
value: 'UTC+13.75',
|
||||
},
|
||||
{
|
||||
label: 'UTC+14',
|
||||
value: 'UTC+14',
|
||||
},
|
||||
];
|
||||
export const DEFAULT_TIMEZONE = 'UTC+0';
|
||||
|
|
|
@ -109,7 +109,7 @@ export interface UserInfoBase {
|
|||
*/
|
||||
status?: string;
|
||||
/** roles */
|
||||
is_admin?: true;
|
||||
is_admin?: boolean;
|
||||
}
|
||||
|
||||
export interface UserInfoRes extends UserInfoBase {
|
||||
|
@ -228,6 +228,7 @@ export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted';
|
|||
|
||||
export interface AdminContentsReq extends Paging {
|
||||
status: AdminContentsFilterBy;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -263,6 +264,7 @@ export interface AdminSettingsInterface {
|
|||
logo: string;
|
||||
language: string;
|
||||
theme: string;
|
||||
time_zone?: string;
|
||||
}
|
||||
|
||||
export interface AdminSettingsSmtp {
|
||||
|
@ -321,3 +323,21 @@ export interface SearchResItem {
|
|||
export interface SearchRes extends ListResult<SearchResItem> {
|
||||
extra: any;
|
||||
}
|
||||
|
||||
export interface AdminDashboard {
|
||||
info: {
|
||||
question_count: number;
|
||||
answer_count: number;
|
||||
comment_count: number;
|
||||
vote_count: number;
|
||||
user_count: number;
|
||||
report_count: number;
|
||||
uploading_files: boolean;
|
||||
smtp: boolean;
|
||||
time_zone: string;
|
||||
occupying_storage_space: string;
|
||||
app_start_time: number;
|
||||
app_version: string;
|
||||
https: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom';
|
|||
|
||||
import { useAccordionButton } from 'react-bootstrap/AccordionButton';
|
||||
|
||||
import { Icon } from '@answer/components';
|
||||
import { Icon } from '@/components';
|
||||
|
||||
function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' });
|
||||
|
|
|
@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Icon } from '@answer/components';
|
||||
import { bookmark, postVote } from '@answer/api';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { useToast } from '@answer/hooks';
|
||||
import { Icon } from '@/components';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { useToast } from '@/hooks';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import { bookmark, postVote } from '@/services';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
@ -32,7 +32,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
state: data?.collected,
|
||||
count: data?.collectCount,
|
||||
});
|
||||
const { username = '' } = userInfoStore((state) => state.user);
|
||||
const { username = '' } = loggedUserInfoStore((state) => state.user);
|
||||
const toast = useToast();
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
|
@ -48,7 +48,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
}, []);
|
||||
|
||||
const handleVote = (type: 'up' | 'down') => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
};
|
||||
|
||||
const handleBookmark = () => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
bookmark({
|
||||
|
|
|
@ -21,9 +21,9 @@ const Index: FC<IProps> = ({ avatar, size, className, searchStr = '' }) => {
|
|||
avatar?.includes('gravatar') ? '&d=identicon' : ''
|
||||
}`;
|
||||
}
|
||||
} else if (avatar?.type === 'gravatar') {
|
||||
} else if (avatar?.type === 'gravatar' && avatar.gravatar) {
|
||||
url = `${avatar.gravatar}?${searchStr}&d=identicon`;
|
||||
} else if (avatar?.type === 'custom') {
|
||||
} else if (avatar?.type === 'custom' && avatar.custom) {
|
||||
url = `${avatar.custom}?${searchStr}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { memo, FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Avatar } from '@answer/components';
|
||||
|
||||
import { Avatar } from '@/components';
|
||||
import { formatCount } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Icon, FormatTime } from '@answer/components';
|
||||
import { Icon, FormatTime } from '@/components';
|
||||
|
||||
const ActionBar = ({
|
||||
nickName,
|
||||
|
|
|
@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { TextArea, Mentions } from '@answer/components';
|
||||
import { usePageUsers } from '@answer/hooks';
|
||||
import { TextArea, Mentions } from '@/components';
|
||||
import { usePageUsers } from '@/hooks';
|
||||
|
||||
const Form = ({
|
||||
className = '',
|
||||
|
|
|
@ -2,8 +2,8 @@ import { useState, memo } from 'react';
|
|||
import { Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { TextArea, Mentions } from '@answer/components';
|
||||
import { usePageUsers } from '@answer/hooks';
|
||||
import { TextArea, Mentions } from '@/components';
|
||||
import { usePageUsers } from '@/hooks';
|
||||
|
||||
const Form = ({ userName, onSendReply, onCancel, mode }) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
|
|
@ -7,17 +7,18 @@ import classNames from 'classnames';
|
|||
import { unionBy } from 'lodash';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import * as Types from '@answer/common/interface';
|
||||
import * as Types from '@/common/interface';
|
||||
import { Modal } from '@/components';
|
||||
import { usePageUsers, useReportModal } from '@/hooks';
|
||||
import { matchedUsers, parseUserInfo } from '@/utils';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import {
|
||||
useQueryComments,
|
||||
addComment,
|
||||
deleteComment,
|
||||
updateComment,
|
||||
postVote,
|
||||
} from '@answer/api';
|
||||
import { Modal } from '@answer/components';
|
||||
import { usePageUsers, useReportModal } from '@answer/hooks';
|
||||
import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils';
|
||||
} from '@/services';
|
||||
|
||||
import { Form, ActionBar, Reply } from './components';
|
||||
|
||||
|
@ -163,7 +164,7 @@ const Comment = ({ objectId, mode }) => {
|
|||
};
|
||||
|
||||
const handleVote = (id, is_cancel) => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -189,7 +190,7 @@ const Comment = ({ objectId, mode }) => {
|
|||
};
|
||||
|
||||
const handleAction = ({ action }, item) => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
if (action === 'report') {
|
||||
|
|
|
@ -2,10 +2,10 @@ import { FC, useEffect, useState, memo } from 'react';
|
|||
import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Modal as AnswerModal } from '@answer/components';
|
||||
import { uploadImage } from '@answer/api';
|
||||
import { Modal as AnswerModal } from '@/components';
|
||||
import ToolItem from '../toolItem';
|
||||
import { IEditorContext } from '../types';
|
||||
import { uploadImage } from '@/services';
|
||||
|
||||
const Image: FC<IEditorContext> = ({ editor }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'editor' });
|
||||
|
|
|
@ -3,9 +3,9 @@ import { Card, Button } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { TagSelector, Tag } from '@answer/components';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import { useFollowingTags, followTags } from '@answer/api';
|
||||
import { TagSelector, Tag } from '@/components';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import { useFollowingTags, followTags } from '@/services';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
|
@ -32,7 +32,7 @@ const Index: FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
if (!isLogin()) {
|
||||
if (!tryNormalLogged()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Nav, Dropdown } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
|
||||
import { Avatar, Icon } from '@answer/components';
|
||||
import { Avatar, Icon } from '@/components';
|
||||
|
||||
interface Props {
|
||||
redDot;
|
||||
|
|
|
@ -50,6 +50,10 @@
|
|||
|
||||
@media (max-width: 992.9px) {
|
||||
#header {
|
||||
.logo {
|
||||
max-width: 93px;
|
||||
max-height: auto;
|
||||
}
|
||||
.nav-grow {
|
||||
flex-grow: 1!important;
|
||||
}
|
||||
|
|
|
@ -17,9 +17,9 @@ import {
|
|||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { userInfoStore, siteInfoStore, interfaceStore } from '@answer/stores';
|
||||
import { logout, useQueryNotificationStatus } from '@answer/api';
|
||||
import Storage from '@answer/utils/storage';
|
||||
import { loggedUserInfoStore, siteInfoStore, interfaceStore } from '@/stores';
|
||||
import { logout, useQueryNotificationStatus } from '@/services';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
|
||||
import NavItems from './components/NavItems';
|
||||
|
||||
|
@ -27,7 +27,7 @@ import './index.scss';
|
|||
|
||||
const Header: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, clear } = userInfoStore();
|
||||
const { user, clear } = loggedUserInfoStore();
|
||||
const { t } = useTranslation();
|
||||
const [urlSearch] = useSearchParams();
|
||||
const q = urlSearch.get('q');
|
||||
|
@ -42,9 +42,8 @@ const Header: FC = () => {
|
|||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
Storage.remove('token');
|
||||
clear();
|
||||
navigate('/');
|
||||
navigate(RouteAlias.home);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -3,8 +3,8 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useHotQuestions } from '@answer/api';
|
||||
import { Icon } from '@answer/components';
|
||||
import { Icon } from '@/components';
|
||||
import { useHotQuestions } from '@/services';
|
||||
|
||||
const HotQuestions: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useRef, useState, FC } from 'react';
|
||||
import { Dropdown } from 'react-bootstrap';
|
||||
|
||||
import * as Types from '@answer/common/interface';
|
||||
import * as Types from '@/common/interface';
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
|
|
|
@ -2,12 +2,10 @@ import React from 'react';
|
|||
import { Modal, Form, Button, InputGroup } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon } from '@answer/components';
|
||||
import type {
|
||||
FormValue,
|
||||
FormDataType,
|
||||
ImgCodeRes,
|
||||
} from '@answer/common/interface';
|
||||
import { Icon } from '@/components';
|
||||
import type { FormValue, FormDataType, ImgCodeRes } from '@/common/interface';
|
||||
import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants';
|
||||
import Storage from '@/utils/storage';
|
||||
|
||||
interface IProps {
|
||||
/** control visible */
|
||||
|
@ -55,7 +53,7 @@ const Index: React.FC<IProps> = ({
|
|||
placeholder={t('placeholder')}
|
||||
isInvalid={captcha.isInvalid}
|
||||
onChange={(e) => {
|
||||
localStorage.setItem('captchaCode', e.target.value);
|
||||
Storage.set(CAPTCHA_CODE_STORAGE_KEY, e.target.value);
|
||||
handleCaptcha({
|
||||
captcha_code: {
|
||||
value: e.target.value,
|
||||
|
|
|
@ -3,11 +3,11 @@ import { Button } from 'react-bootstrap';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Modal } from '@answer/components';
|
||||
import { useReportModal, useToast } from '@answer/hooks';
|
||||
import { deleteQuestion, deleteAnswer } from '@answer/api';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import { Modal } from '@/components';
|
||||
import { useReportModal, useToast } from '@/hooks';
|
||||
import Share from '../Share';
|
||||
import { deleteQuestion, deleteAnswer } from '@/services';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
|
||||
interface IProps {
|
||||
type: 'answer' | 'question';
|
||||
|
@ -98,7 +98,7 @@ const Index: FC<IProps> = ({
|
|||
};
|
||||
|
||||
const handleAction = (action) => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
if (action === 'delete') {
|
||||
|
|
|
@ -3,8 +3,7 @@ import { Row, Col, ListGroup } from 'react-bootstrap';
|
|||
import { NavLink, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useQuestionList } from '@answer/api';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
import type * as Type from '@/common/interface';
|
||||
import {
|
||||
Icon,
|
||||
Tag,
|
||||
|
@ -13,7 +12,8 @@ import {
|
|||
Empty,
|
||||
BaseUserCard,
|
||||
QueryGroup,
|
||||
} from '@answer/components';
|
||||
} from '@/components';
|
||||
import { useQuestionList } from '@/services';
|
||||
|
||||
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
|
||||
'newest',
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { FacebookShareButton, TwitterShareButton } from 'next-share';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
interface IProps {
|
||||
type: 'answer' | 'question';
|
||||
|
@ -15,7 +15,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
const Index: FC<IProps> = ({ type, qid, aid, title }) => {
|
||||
const user = userInfoStore((state) => state.user);
|
||||
const user = loggedUserInfoStore((state) => state.user);
|
||||
const [show, setShow] = useState(false);
|
||||
const [showTip, setShowTip] = useState(false);
|
||||
const [canSystemShare, setSystemShareState] = useState(false);
|
||||
|
|
|
@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next';
|
|||
import { marked } from 'marked';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useTagModal } from '@answer/hooks';
|
||||
import { queryTags } from '@answer/api';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
import { useTagModal } from '@/hooks';
|
||||
import type * as Type from '@/common/interface';
|
||||
import { queryTags } from '@/services';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
|
|
|
@ -3,14 +3,12 @@ import { Button, Col } from 'react-bootstrap';
|
|||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { resendEmail, checkImgCode } from '@answer/api';
|
||||
import { PicAuthCodeModal } from '@answer/components/Modal';
|
||||
import type {
|
||||
ImgCodeRes,
|
||||
ImgCodeReq,
|
||||
FormDataType,
|
||||
} from '@answer/common/interface';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { PicAuthCodeModal } from '@/components/Modal';
|
||||
import type { ImgCodeRes, ImgCodeReq, FormDataType } from '@/common/interface';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { resendEmail, checkImgCode } from '@/services';
|
||||
import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants';
|
||||
import Storage from '@/utils/storage';
|
||||
|
||||
interface IProps {
|
||||
visible: boolean;
|
||||
|
@ -20,7 +18,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
|
|||
const { t } = useTranslation('translation', { keyPrefix: 'inactive' });
|
||||
const [isSuccess, setSuccess] = useState(false);
|
||||
const [showModal, setModalState] = useState(false);
|
||||
const { e_mail } = userInfoStore((state) => state.user);
|
||||
const { e_mail } = loggedUserInfoStore((state) => state.user);
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
captcha_code: {
|
||||
value: '',
|
||||
|
@ -48,7 +46,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
|
|||
}
|
||||
let obj: ImgCodeReq = {};
|
||||
if (imgCode.verify) {
|
||||
const code = localStorage.getItem('captchaCode') || '';
|
||||
const code = Storage.get(CAPTCHA_CODE_STORAGE_KEY) || '';
|
||||
obj = {
|
||||
captcha_code: code,
|
||||
captcha_id: imgCode.captcha_id,
|
||||
|
|
|
@ -3,8 +3,7 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Avatar, FormatTime } from '@answer/components';
|
||||
|
||||
import { Avatar, FormatTime } from '@/components';
|
||||
import { formatCount } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { changeUserStatus } from '@answer/api';
|
||||
import { Modal as AnswerModal } from '@answer/components';
|
||||
import { Modal as AnswerModal } from '@/components';
|
||||
import { changeUserStatus } from '@/services';
|
||||
|
||||
const div = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(div);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
import * as Types from '@answer/common/interface';
|
||||
import * as Types from '@/common/interface';
|
||||
|
||||
let globalUsers: Types.PageUser[] = [];
|
||||
const usePageUsers = () => {
|
||||
|
|
|
@ -4,9 +4,9 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { reportList, postReport, closeQuestion, putReport } from '@answer/api';
|
||||
import { useToast } from '@answer/hooks';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
import { useToast } from '@/hooks';
|
||||
import type * as Type from '@/common/interface';
|
||||
import { reportList, postReport, closeQuestion, putReport } from '@/services';
|
||||
|
||||
interface Params {
|
||||
isBackend?: boolean;
|
||||
|
|
|
@ -3,6 +3,8 @@ import { initReactI18next } from 'react-i18next';
|
|||
import i18next from 'i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
|
||||
import { DEFAULT_LANG } from '@/common/constants';
|
||||
|
||||
import en from './locales/en.json';
|
||||
import zh from './locales/zh_CN.json';
|
||||
|
||||
|
@ -21,7 +23,7 @@ i18next
|
|||
},
|
||||
},
|
||||
// debug: process.env.NODE_ENV === 'development',
|
||||
fallbackLng: process.env.REACT_APP_LANG || 'en_US',
|
||||
fallbackLng: process.env.REACT_APP_LANG || DEFAULT_LANG,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
|
|
@ -28,7 +28,10 @@
|
|||
"confirm_email": "Confirm Email",
|
||||
"account_suspended": "Account Suspended",
|
||||
"admin": "Admin",
|
||||
"change_email": "Modify Email"
|
||||
"change_email": "Modify Email",
|
||||
"install": "Answer Installation",
|
||||
"upgrade": "Answer Upgrade",
|
||||
"maintenance": "Webite Maintenance"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
|
@ -290,7 +293,9 @@
|
|||
"now": "now",
|
||||
"x_seconds_ago": "{{count}}s ago",
|
||||
"x_minutes_ago": "{{count}}m ago",
|
||||
"x_hours_ago": "{{count}}h ago"
|
||||
"x_hours_ago": "{{count}}h ago",
|
||||
"hour": "hour",
|
||||
"day": "day"
|
||||
},
|
||||
"comment": {
|
||||
"btn_add_comment": "Add comment",
|
||||
|
@ -523,7 +528,8 @@
|
|||
"custom": "Custom",
|
||||
"btn_refresh": "Refresh",
|
||||
"custom_text": "You can upload your image.",
|
||||
"default": "Default"
|
||||
"default": "Default",
|
||||
"msg": "Please upload an avatar"
|
||||
},
|
||||
"bio": {
|
||||
"label": "About Me (optional)"
|
||||
|
@ -734,6 +740,84 @@
|
|||
"x_answers": "answers",
|
||||
"x_questions": "questions"
|
||||
},
|
||||
"install": {
|
||||
"title": "Answer",
|
||||
"next": "Next",
|
||||
"done": "Done",
|
||||
"lang": {
|
||||
"label": "Please choose a language"
|
||||
},
|
||||
"db_type": {
|
||||
"label": "Database Engine"
|
||||
},
|
||||
"db_username": {
|
||||
"label": "Username",
|
||||
"placeholder": "root",
|
||||
"msg": "Username cannot be empty."
|
||||
},
|
||||
"db_password": {
|
||||
"label": "Password",
|
||||
"placeholder": "root",
|
||||
"msg": "Password cannot be empty."
|
||||
},
|
||||
"db_host": {
|
||||
"label": "Database Host",
|
||||
"placeholder": "db:3306",
|
||||
"msg": "Database Host cannot be empty."
|
||||
},
|
||||
"db_name": {
|
||||
"label": "Database Name",
|
||||
"placeholder": "answer",
|
||||
"msg": "Database Name cannot be empty."
|
||||
},
|
||||
"db_file": {
|
||||
"label": "Database File",
|
||||
"placeholder": "/data/answer.db",
|
||||
"msg": "Database File cannot be empty."
|
||||
},
|
||||
"config_yaml": {
|
||||
"title": "Create config.yaml",
|
||||
"label": "The config.yaml file created.",
|
||||
"description": "You can create the <1>config.yaml</1> file manually in the <1>/var/wwww/xxx/</1> directory and paste the following text into it.",
|
||||
"info": "After you’ve done that, click “Next” button."
|
||||
},
|
||||
"site_information": "Site Information",
|
||||
"admin_account": "Admin Account",
|
||||
"site_name": {
|
||||
"label": "Site Name"
|
||||
},
|
||||
"contact_email": {
|
||||
"label": "Contact Email",
|
||||
"text": "Email address of key contact responsible for this site."
|
||||
},
|
||||
"admin_name": {
|
||||
"label": "Name"
|
||||
},
|
||||
"admin_password": {
|
||||
"label": "Password",
|
||||
"text": "You will need this password to log in. Please store it in a secure location."
|
||||
},
|
||||
"admin_email": {
|
||||
"label": "Email",
|
||||
"text": "You will need this email to log in."
|
||||
},
|
||||
"ready_title": "Your Answer is Ready!",
|
||||
"ready_description": "If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.",
|
||||
"good_luck": "Have fun, and good luck!",
|
||||
"warning": "Warning",
|
||||
"warning_description": "The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try <2>installing now</2>.",
|
||||
"installed": "Already installed",
|
||||
"installed_description": "You appear to have already installed. To reinstall please clear your old database tables first."
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Answer",
|
||||
"update_btn": "Update data",
|
||||
"update_title": "Data update required",
|
||||
"update_description": "<1>Answer has been updated! Before you continue, we have to update your data to the newest version.</1><1>The update process may take a little while, so please be patient.</1>",
|
||||
"done_title": "No update required",
|
||||
"done_btn": "Done",
|
||||
"done_desscription": "Your Answer data is already up-to-date."
|
||||
},
|
||||
"page_404": {
|
||||
"description": "Unfortunately, this page doesn't exist.",
|
||||
"back_home": "Back to homepage"
|
||||
|
@ -742,6 +826,9 @@
|
|||
"description": "The server encountered an error and could not complete your request.",
|
||||
"back_home": "Back to homepage"
|
||||
},
|
||||
"page_maintenance": {
|
||||
"description": "We are under maintenance, we’ll be back soon."
|
||||
},
|
||||
"admin": {
|
||||
"admin_header": {
|
||||
"title": "Admin"
|
||||
|
@ -761,7 +848,36 @@
|
|||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"welcome": "Welcome to Answer Admin !",
|
||||
"version": "Version"
|
||||
"site_statistics": "Site Statistics",
|
||||
"questions": "Questions:",
|
||||
"answers": "Answers:",
|
||||
"comments": "Comments:",
|
||||
"votes": "Votes:",
|
||||
"active_users": "Active users:",
|
||||
"flags": "Flags:",
|
||||
"site_health_status": "Site Health Status",
|
||||
"version": "Version:",
|
||||
"https": "HTTPS:",
|
||||
"uploading_files": "Uploading files:",
|
||||
"smtp": "SMTP:",
|
||||
"timezone": "Timezone:",
|
||||
"system_info": "System Info",
|
||||
"storage_used": "Storage used:",
|
||||
"uptime": "Uptime:",
|
||||
"answer_links": "Answer Links",
|
||||
"documents": "Documents",
|
||||
"feedback": "Feedback",
|
||||
"review": "Review",
|
||||
"config": "Config",
|
||||
"update_to": "Update to",
|
||||
"latest": "Latest",
|
||||
"check_failed": "Check failed",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"not_allowed": "Not allowed",
|
||||
"allowed": "Allowed",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"flags": {
|
||||
"title": "Flags",
|
||||
|
@ -818,7 +934,10 @@
|
|||
"inactive": "Inactive",
|
||||
"suspended": "Suspended",
|
||||
"deleted": "Deleted",
|
||||
"normal": "Normal"
|
||||
"normal": "Normal",
|
||||
"filter": {
|
||||
"placeholder": "Filter by name, user:id"
|
||||
}
|
||||
},
|
||||
"questions": {
|
||||
"page_title": "Questions",
|
||||
|
@ -831,7 +950,10 @@
|
|||
"created": "Created",
|
||||
"status": "Status",
|
||||
"action": "Action",
|
||||
"change": "Change"
|
||||
"change": "Change",
|
||||
"filter": {
|
||||
"placeholder": "Filter by title, question:id"
|
||||
}
|
||||
},
|
||||
"answers": {
|
||||
"page_title": "Answers",
|
||||
|
@ -842,7 +964,10 @@
|
|||
"created": "Created",
|
||||
"status": "Status",
|
||||
"action": "Action",
|
||||
"change": "Change"
|
||||
"change": "Change",
|
||||
"filter": {
|
||||
"placeholder": "Filter by title, answer:id"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"page_title": "General",
|
||||
|
@ -878,6 +1003,11 @@
|
|||
"label": "Interface Language",
|
||||
"msg": "Interface language cannot be empty.",
|
||||
"text": "User interface language. It will change when you refresh the page."
|
||||
},
|
||||
"timezone": {
|
||||
"label": "Timezone",
|
||||
"msg": "Timezone cannot be empty.",
|
||||
"text": "Choose a UTC (Coordinated Universal Time) time offset."
|
||||
}
|
||||
},
|
||||
"smtp": {
|
||||
|
|
|
@ -77,6 +77,10 @@ a {
|
|||
.page-wrap {
|
||||
min-height: calc(100vh - 148px);
|
||||
}
|
||||
.page-wrap2 {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.btn-no-border,
|
||||
.btn-no-border:hover,
|
||||
|
|
|
@ -2,15 +2,27 @@ import React from 'react';
|
|||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { Guard } from '@/utils';
|
||||
|
||||
import App from './App';
|
||||
|
||||
import './i18n/init';
|
||||
import './index.scss';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement,
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
async function bootstrapApp() {
|
||||
/**
|
||||
* NOTICE: must pre init logged user info for router
|
||||
*/
|
||||
await Guard.pullLoggedUser();
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
}
|
||||
|
||||
bootstrapApp();
|
||||
|
|
|
@ -11,21 +11,23 @@ import {
|
|||
BaseUserCard,
|
||||
Empty,
|
||||
QueryGroup,
|
||||
} from '@answer/components';
|
||||
import { ADMIN_LIST_STATUS } from '@answer/common/constants';
|
||||
import { useEditStatusModal } from '@answer/hooks';
|
||||
import { useAnswerSearch, changeAnswerStatus } from '@answer/api';
|
||||
import * as Type from '@answer/common/interface';
|
||||
} from '@/components';
|
||||
import { ADMIN_LIST_STATUS } from '@/common/constants';
|
||||
import { useEditStatusModal } from '@/hooks';
|
||||
import * as Type from '@/common/interface';
|
||||
import { useAnswerSearch, changeAnswerStatus } from '@/services';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted'];
|
||||
|
||||
const Answers: FC = () => {
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
|
||||
const curFilter = urlSearchParams.get('status') || answerFilterItems[0];
|
||||
const PAGE_SIZE = 20;
|
||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||
const curQuery = urlSearchParams.get('query') || '';
|
||||
const questionId = urlSearchParams.get('questionId') || '';
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' });
|
||||
|
||||
const {
|
||||
|
@ -36,6 +38,8 @@ const Answers: FC = () => {
|
|||
page_size: PAGE_SIZE,
|
||||
page: curPage,
|
||||
status: curFilter as Type.AdminContentsFilterBy,
|
||||
query: curQuery,
|
||||
question_id: questionId,
|
||||
});
|
||||
const count = listData?.count || 0;
|
||||
|
||||
|
@ -77,6 +81,11 @@ const Answers: FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleFilter = (e) => {
|
||||
urlSearchParams.set('query', e.target.value);
|
||||
urlSearchParams.delete('page');
|
||||
setUrlSearchParams(urlSearchParams);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('page_title')}</h3>
|
||||
|
@ -89,19 +98,20 @@ const Answers: FC = () => {
|
|||
/>
|
||||
|
||||
<Form.Control
|
||||
value={curQuery}
|
||||
onChange={handleFilter}
|
||||
size="sm"
|
||||
type="input"
|
||||
placeholder="Filter by title"
|
||||
className="d-none"
|
||||
placeholder={t('filter.placeholder')}
|
||||
style={{ width: '12.25rem' }}
|
||||
/>
|
||||
</div>
|
||||
<Table>
|
||||
<Table responsive>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '45%' }}>{t('post')}</th>
|
||||
<th>{t('post')}</th>
|
||||
<th>{t('votes')}</th>
|
||||
<th style={{ width: '20%' }}>{t('created')}</th>
|
||||
<th>{t('created')}</th>
|
||||
<th>{t('status')}</th>
|
||||
{curFilter !== 'deleted' && <th>{t('action')}</th>}
|
||||
</tr>
|
||||
|
@ -132,6 +142,7 @@ const Answers: FC = () => {
|
|||
__html: li.description,
|
||||
}}
|
||||
className="last-p text-truncate-2 fs-14"
|
||||
style={{ maxWidth: '30rem' }}
|
||||
/>
|
||||
</Stack>
|
||||
</td>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { Card, Row, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const AnswerLinks = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||
|
||||
return (
|
||||
<Card className="mb-4">
|
||||
<Card.Body>
|
||||
<h6 className="mb-3">{t('answer_links')}</h6>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<a href="https://answer.dev" target="_blank" rel="noreferrer">
|
||||
{t('documents')}
|
||||
</a>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<a
|
||||
href="https://github.com/answerdev/answer/issues"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
{t('feedback')}
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnswerLinks;
|
|
@ -0,0 +1,54 @@
|
|||
import { FC } from 'react';
|
||||
import { Card, Row, Col, Badge } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
interface IProps {
|
||||
data: Type.AdminDashboard['info'];
|
||||
}
|
||||
|
||||
const HealthStatus: FC<IProps> = ({ data }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||
|
||||
return (
|
||||
<Card className="mb-4">
|
||||
<Card.Body>
|
||||
<h6 className="mb-3">{t('site_health_status')}</h6>
|
||||
<Row>
|
||||
<Col xs={6} className="mb-1 d-flex align-items-center">
|
||||
<span className="text-secondary me-1">{t('version')}</span>
|
||||
<strong>90</strong>
|
||||
<Badge pill bg="warning" text="dark" className="ms-1">
|
||||
{t('update_to')} {data.app_version}
|
||||
</Badge>
|
||||
</Col>
|
||||
<Col xs={6} className="mb-1">
|
||||
<span className="text-secondary me-1">{t('https')}</span>
|
||||
<strong>{data.https ? t('yes') : t('yes')}</strong>
|
||||
</Col>
|
||||
<Col xs={6} className="mb-1">
|
||||
<span className="text-secondary me-1">{t('uploading_files')}</span>
|
||||
<strong>
|
||||
{data.uploading_files ? t('allowed') : t('not_allowed')}
|
||||
</strong>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<span className="text-secondary me-1">{t('smtp')}</span>
|
||||
<strong>{data.smtp ? t('enabled') : t('disabled')}</strong>
|
||||
<Link to="/admin/smtp" className="ms-2">
|
||||
{t('config')}
|
||||
</Link>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<span className="text-secondary me-1">{t('timezone')}</span>
|
||||
<strong>{data.time_zone}</strong>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default HealthStatus;
|
|
@ -0,0 +1,51 @@
|
|||
import { FC } from 'react';
|
||||
import { Card, Row, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
interface IProps {
|
||||
data: Type.AdminDashboard['info'];
|
||||
}
|
||||
const Statistics: FC<IProps> = ({ data }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||
|
||||
return (
|
||||
<Card className="mb-4">
|
||||
<Card.Body>
|
||||
<h6 className="mb-3">{t('site_statistics')}</h6>
|
||||
<Row>
|
||||
<Col xs={6} className="mb-1">
|
||||
<span className="text-secondary me-1">{t('questions')}</span>
|
||||
<strong>{data.question_count}</strong>
|
||||
</Col>
|
||||
<Col xs={6} className="mb-1">
|
||||
<span className="text-secondary me-1">{t('answers')}</span>
|
||||
<strong>{data.answer_count}</strong>
|
||||
</Col>
|
||||
<Col xs={6} className="mb-1">
|
||||
<span className="text-secondary me-1">{t('comments')}</span>
|
||||
<strong>{data.comment_count}</strong>
|
||||
</Col>
|
||||
<Col xs={6} className="mb-1">
|
||||
<span className="text-secondary me-1">{t('votes')}</span>
|
||||
<strong>{data.vote_count}</strong>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<span className="text-secondary me-1">{t('active_users')}</span>
|
||||
<strong>{data.user_count}</strong>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<span className="text-secondary me-1">{t('flags')}</span>
|
||||
<strong>{data.report_count}</strong>
|
||||
<a href="###" className="ms-2">
|
||||
{t('review')}
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Statistics;
|
|
@ -0,0 +1,33 @@
|
|||
import { FC } from 'react';
|
||||
import { Card, Row, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { formatUptime } from '@/utils';
|
||||
|
||||
interface IProps {
|
||||
data: Type.AdminDashboard['info'];
|
||||
}
|
||||
const SystemInfo: FC<IProps> = ({ data }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||
|
||||
return (
|
||||
<Card className="mb-4">
|
||||
<Card.Body>
|
||||
<h6 className="mb-3">{t('system_info')}</h6>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<span className="text-secondary me-1">{t('storage_used')}</span>
|
||||
<strong>{data.occupying_storage_space}</strong>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<span className="text-secondary me-1">{t('uptime')}</span>
|
||||
<strong>{formatUptime(data.app_start_time)}</strong>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemInfo;
|
|
@ -0,0 +1,6 @@
|
|||
import SystemInfo from './SystemInfo';
|
||||
import Statistics from './Statistics';
|
||||
import AnswerLinks from './AnswerLinks';
|
||||
import HealthStatus from './HealthStatus';
|
||||
|
||||
export { SystemInfo, Statistics, AnswerLinks, HealthStatus };
|
|
@ -1,12 +1,41 @@
|
|||
import { FC } from 'react';
|
||||
import { Row, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDashBoard } from '@/services';
|
||||
|
||||
import {
|
||||
AnswerLinks,
|
||||
HealthStatus,
|
||||
Statistics,
|
||||
SystemInfo,
|
||||
} from './components';
|
||||
|
||||
const Dashboard: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||
const { data } = useDashBoard();
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-capitalize">{t('title')}</h3>
|
||||
<p className="mt-4">{t('welcome')}</p>
|
||||
<Row>
|
||||
<Col lg={6}>
|
||||
<Statistics data={data.info} />
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<HealthStatus data={data.info} />
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<SystemInfo data={data.info} />
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<AnswerLinks />
|
||||
</Col>
|
||||
</Row>
|
||||
{process.env.REACT_APP_VERSION && (
|
||||
<p className="mt-4">
|
||||
{`${t('version')} `}
|
||||
|
|
|
@ -9,10 +9,10 @@ import {
|
|||
Empty,
|
||||
Pagination,
|
||||
QueryGroup,
|
||||
} from '@answer/components';
|
||||
import { useReportModal } from '@answer/hooks';
|
||||
import * as Type from '@answer/common/interface';
|
||||
import { useFlagSearch } from '@answer/api';
|
||||
} from '@/components';
|
||||
import { useReportModal } from '@/hooks';
|
||||
import * as Type from '@/common/interface';
|
||||
import { useFlagSearch } from '@/services';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@ import React, { FC, useEffect, useState } from 'react';
|
|||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type * as Type from '@answer/common/interface';
|
||||
import { useToast } from '@answer/hooks';
|
||||
import { siteInfoStore } from '@answer/stores';
|
||||
import { useGeneralSetting, updateGeneralSetting } from '@answer/api';
|
||||
import type * as Type from '@/common/interface';
|
||||
import { useToast } from '@/hooks';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { useGeneralSetting, updateGeneralSetting } from '@/services';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
|
|
|
@ -2,21 +2,22 @@ import React, { FC, FormEvent, useEffect, useState } from 'react';
|
|||
import { Form, Button, Image, Stack } from 'react-bootstrap';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { useToast } from '@answer/hooks';
|
||||
import { useToast } from '@/hooks';
|
||||
import {
|
||||
LangsType,
|
||||
FormDataType,
|
||||
AdminSettingsInterface,
|
||||
} from '@answer/common/interface';
|
||||
} from '@/common/interface';
|
||||
import { interfaceStore } from '@/stores';
|
||||
import { UploadImg } from '@/components';
|
||||
import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants';
|
||||
import {
|
||||
languages,
|
||||
uploadAvatar,
|
||||
updateInterfaceSetting,
|
||||
useInterfaceSetting,
|
||||
useThemeOptions,
|
||||
} from '@answer/api';
|
||||
import { interfaceStore } from '@answer/stores';
|
||||
import { UploadImg } from '@answer/components';
|
||||
} from '@/services';
|
||||
|
||||
const Interface: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
|
@ -27,6 +28,7 @@ const Interface: FC = () => {
|
|||
const Toast = useToast();
|
||||
const [langs, setLangs] = useState<LangsType[]>();
|
||||
const { data: setting } = useInterfaceSetting();
|
||||
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
logo: {
|
||||
value: setting?.logo || '',
|
||||
|
@ -43,6 +45,11 @@ const Interface: FC = () => {
|
|||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
time_zone: {
|
||||
value: setting?.time_zone || DEFAULT_TIMEZONE,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
const getLangs = async () => {
|
||||
const res: LangsType[] = await languages();
|
||||
|
@ -106,6 +113,7 @@ const Interface: FC = () => {
|
|||
logo: formData.logo.value,
|
||||
theme: formData.theme.value,
|
||||
language: formData.language.value,
|
||||
time_zone: formData.time_zone.value,
|
||||
};
|
||||
|
||||
updateInterfaceSetting(reqParams)
|
||||
|
@ -158,12 +166,14 @@ const Interface: FC = () => {
|
|||
Object.keys(setting).forEach((k) => {
|
||||
formMeta[k] = { ...formData[k], value: setting[k] };
|
||||
});
|
||||
setFormData(formMeta);
|
||||
setFormData({ ...formData, ...formMeta });
|
||||
}
|
||||
}, [setting]);
|
||||
useEffect(() => {
|
||||
getLangs();
|
||||
}, []);
|
||||
|
||||
console.log('formData', formData);
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('page_title')}</h3>
|
||||
|
@ -249,7 +259,27 @@ const Interface: FC = () => {
|
|||
{formData.language.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="time-zone" className="mb-3">
|
||||
<Form.Label>{t('time_zone.label')}</Form.Label>
|
||||
<Form.Select
|
||||
value={formData.time_zone.value}
|
||||
isInvalid={formData.time_zone.isInvalid}
|
||||
onChange={(evt) => {
|
||||
onChange('time_zone', evt.target.value);
|
||||
}}>
|
||||
{TIMEZONES?.map((item) => {
|
||||
return (
|
||||
<option value={item.value} key={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Form.Select>
|
||||
<Form.Text as="div">{t('time_zone.text')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.time_zone.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Button variant="primary" type="submit">
|
||||
{t('save', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FC } from 'react';
|
||||
import { Button, Form, Table, Stack, Badge } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
|
@ -11,15 +11,15 @@ import {
|
|||
BaseUserCard,
|
||||
Empty,
|
||||
QueryGroup,
|
||||
} from '@answer/components';
|
||||
import { ADMIN_LIST_STATUS } from '@answer/common/constants';
|
||||
import { useEditStatusModal, useReportModal } from '@answer/hooks';
|
||||
} from '@/components';
|
||||
import { ADMIN_LIST_STATUS } from '@/common/constants';
|
||||
import { useEditStatusModal, useReportModal } from '@/hooks';
|
||||
import * as Type from '@/common/interface';
|
||||
import {
|
||||
useQuestionSearch,
|
||||
changeQuestionStatus,
|
||||
deleteQuestion,
|
||||
} from '@answer/api';
|
||||
import * as Type from '@answer/common/interface';
|
||||
} from '@/services';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
|
@ -31,9 +31,10 @@ const questionFilterItems: Type.AdminContentsFilterBy[] = [
|
|||
|
||||
const PAGE_SIZE = 20;
|
||||
const Questions: FC = () => {
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
|
||||
const curFilter = urlSearchParams.get('status') || questionFilterItems[0];
|
||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||
const curQuery = urlSearchParams.get('query') || '';
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.questions' });
|
||||
|
||||
const {
|
||||
|
@ -44,6 +45,7 @@ const Questions: FC = () => {
|
|||
page_size: PAGE_SIZE,
|
||||
page: curPage,
|
||||
status: curFilter as Type.AdminContentsFilterBy,
|
||||
query: curQuery,
|
||||
});
|
||||
const count = listData?.count || 0;
|
||||
|
||||
|
@ -96,6 +98,11 @@ const Questions: FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleFilter = (e) => {
|
||||
urlSearchParams.set('query', e.target.value);
|
||||
urlSearchParams.delete('page');
|
||||
setUrlSearchParams(urlSearchParams);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('page_title')}</h3>
|
||||
|
@ -108,10 +115,11 @@ const Questions: FC = () => {
|
|||
/>
|
||||
|
||||
<Form.Control
|
||||
value={curQuery}
|
||||
size="sm"
|
||||
type="input"
|
||||
placeholder="Filter by title"
|
||||
className="d-none"
|
||||
placeholder={t('filter.placeholder')}
|
||||
onChange={handleFilter}
|
||||
style={{ width: '12.25rem' }}
|
||||
/>
|
||||
</div>
|
||||
|
@ -147,12 +155,11 @@ const Questions: FC = () => {
|
|||
</td>
|
||||
<td>{li.vote_count}</td>
|
||||
<td>
|
||||
<a
|
||||
href={`/questions/${li.id}`}
|
||||
target="_blank"
|
||||
<Link
|
||||
to={`/admin/answers?questionId=${li.id}`}
|
||||
rel="noreferrer">
|
||||
{li.answer_count}
|
||||
</a>
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Stack>
|
||||
|
|
|
@ -2,10 +2,9 @@ import React, { FC, useEffect, useState } from 'react';
|
|||
import { Form, Button, Stack } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type * as Type from '@answer/common/interface';
|
||||
import { useToast } from '@answer/hooks';
|
||||
import { useSmtpSetting, updateSmtpSetting } from '@answer/api';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { useToast } from '@/hooks';
|
||||
import { useSmtpSetting, updateSmtpSetting } from '@/services';
|
||||
import pattern from '@/common/pattern';
|
||||
|
||||
const Smtp: FC = () => {
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { FC, useState } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { Button, Form, Table, Badge } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useQueryUsers } from '@answer/api';
|
||||
import {
|
||||
Pagination,
|
||||
FormatTime,
|
||||
BaseUserCard,
|
||||
Empty,
|
||||
QueryGroup,
|
||||
} from '@answer/components';
|
||||
import * as Type from '@answer/common/interface';
|
||||
import { useChangeModal } from '@answer/hooks';
|
||||
} from '@/components';
|
||||
import * as Type from '@/common/interface';
|
||||
import { useChangeModal } from '@/hooks';
|
||||
import { useQueryUsers } from '@/services';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
|
@ -33,11 +33,11 @@ const bgMap = {
|
|||
const PAGE_SIZE = 10;
|
||||
const Users: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
|
||||
const [userName, setUserName] = useState('');
|
||||
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
|
||||
const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];
|
||||
const curPage = Number(urlSearchParams.get('page') || '1');
|
||||
const curQuery = urlSearchParams.get('query') || '';
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
|
@ -45,7 +45,7 @@ const Users: FC = () => {
|
|||
} = useQueryUsers({
|
||||
page: curPage,
|
||||
page_size: PAGE_SIZE,
|
||||
...(userName ? { username: userName } : {}),
|
||||
query: curQuery,
|
||||
...(curFilter === 'all' ? {} : { status: curFilter }),
|
||||
});
|
||||
const changeModal = useChangeModal({
|
||||
|
@ -59,6 +59,11 @@ const Users: FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleFilter = (e) => {
|
||||
urlSearchParams.set('query', e.target.value);
|
||||
urlSearchParams.delete('page');
|
||||
setUrlSearchParams(urlSearchParams);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
|
@ -71,11 +76,10 @@ const Users: FC = () => {
|
|||
/>
|
||||
|
||||
<Form.Control
|
||||
className="d-none"
|
||||
size="sm"
|
||||
value={userName}
|
||||
onChange={(e) => setUserName(e.target.value)}
|
||||
placeholder="Filter by name"
|
||||
value={curQuery}
|
||||
onChange={handleFilter}
|
||||
placeholder={t('filter.placeholder')}
|
||||
style={{ width: '12.25rem' }}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -3,8 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { AccordionNav, PageTitle } from '@answer/components';
|
||||
import { ADMIN_NAV_MENUS } from '@answer/common/constants';
|
||||
import { AccordionNav, PageTitle } from '@/components';
|
||||
import { ADMIN_NAV_MENUS } from '@/common/constants';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { FC } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import Progress from '../Progress';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({ visible }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'install' });
|
||||
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<div>
|
||||
<h5>{t('ready_title')}</h5>
|
||||
<p>
|
||||
<Trans i18nKey="install.ready_description">
|
||||
If you ever feel like changing more settings, visit
|
||||
<a href="/">admin section</a>; find it in the site menu.
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t('good_luck')}</p>
|
||||
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<Progress step={5} />
|
||||
<Button>{t('done')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,68 @@
|
|||
import { FC, useEffect, useState } from 'react';
|
||||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { LangsType, FormValue, FormDataType } from '@/common/interface';
|
||||
import Progress from '../Progress';
|
||||
import { languages } from '@/services';
|
||||
|
||||
interface Props {
|
||||
data: FormValue;
|
||||
changeCallback: (value: FormDataType) => void;
|
||||
nextCallback: () => void;
|
||||
visible: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'install' });
|
||||
|
||||
const [langs, setLangs] = useState<LangsType[]>();
|
||||
|
||||
const getLangs = async () => {
|
||||
const res: LangsType[] = await languages();
|
||||
setLangs(res);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
nextCallback();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getLangs();
|
||||
}, []);
|
||||
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="lang" className="mb-3">
|
||||
<Form.Label>{t('lang.label')}</Form.Label>
|
||||
<Form.Select
|
||||
value={data.value}
|
||||
isInvalid={data.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
lang: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}>
|
||||
{langs?.map((item) => {
|
||||
return (
|
||||
<option value={item.value} key={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<Progress step={1} />
|
||||
<Button type="submit">{t('next')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,207 @@
|
|||
import { FC, FormEvent } from 'react';
|
||||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
import Progress from '../Progress';
|
||||
|
||||
interface Props {
|
||||
data: FormDataType;
|
||||
changeCallback: (value: FormDataType) => void;
|
||||
nextCallback: () => void;
|
||||
visible: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'install' });
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const {
|
||||
site_name,
|
||||
contact_email,
|
||||
admin_name,
|
||||
admin_password,
|
||||
admin_email,
|
||||
} = data;
|
||||
|
||||
if (!site_name.value) {
|
||||
bol = false;
|
||||
data.site_name = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('site_name.msg'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!contact_email.value) {
|
||||
bol = false;
|
||||
data.contact_email = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('contact_email.msg'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!admin_name.value) {
|
||||
bol = false;
|
||||
data.admin_name = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('admin_name.msg'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!admin_password.value) {
|
||||
bol = false;
|
||||
data.admin_password = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('admin_password.msg'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!admin_email.value) {
|
||||
bol = false;
|
||||
data.admin_email = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('admin_email.msg'),
|
||||
};
|
||||
}
|
||||
|
||||
changeCallback({
|
||||
...data,
|
||||
});
|
||||
return bol;
|
||||
};
|
||||
|
||||
const handleSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!checkValidated()) {
|
||||
return;
|
||||
}
|
||||
nextCallback();
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<h5>{t('site_information')}</h5>
|
||||
<Form.Group controlId="site_name" className="mb-3">
|
||||
<Form.Label>{t('site_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
value={data.site_name.value}
|
||||
isInvalid={data.site_name.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
site_name: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{data.site_name.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="contact_email" className="mb-3">
|
||||
<Form.Label>{t('contact_email.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
value={data.contact_email.value}
|
||||
isInvalid={data.contact_email.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
contact_email: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Text>{t('contact_email.text')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{data.contact_email.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<h5>{t('admin_account')}</h5>
|
||||
<Form.Group controlId="admin_name" className="mb-3">
|
||||
<Form.Label>{t('admin_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
value={data.admin_name.value}
|
||||
isInvalid={data.admin_name.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
admin_name: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{data.admin_name.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="admin_password" className="mb-3">
|
||||
<Form.Label>{t('admin_password.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
value={data.admin_password.value}
|
||||
isInvalid={data.admin_password.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
admin_password: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Text>{t('admin_password.text')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{data.admin_password.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="admin_email" className="mb-3">
|
||||
<Form.Label>{t('admin_email.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
value={data.admin_email.value}
|
||||
isInvalid={data.admin_email.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
admin_email: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Text>{t('admin_email.text')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{data.admin_email.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<Progress step={4} />
|
||||
<Button type="submit">{t('next')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,22 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { ProgressBar } from 'react-bootstrap';
|
||||
|
||||
interface IProps {
|
||||
step: number;
|
||||
}
|
||||
|
||||
const Index: FC<IProps> = ({ step }) => {
|
||||
return (
|
||||
<div className="d-flex align-items-center fs-14 text-secondary">
|
||||
<ProgressBar
|
||||
now={(step / 5) * 100}
|
||||
variant="success"
|
||||
style={{ width: '200px' }}
|
||||
className="me-2"
|
||||
/>
|
||||
<span>{step}/5</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -0,0 +1,246 @@
|
|||
import { FC, FormEvent } from 'react';
|
||||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Progress from '../Progress';
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
|
||||
interface Props {
|
||||
data: FormDataType;
|
||||
changeCallback: (value: FormDataType) => void;
|
||||
nextCallback: () => void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const sqlData = [
|
||||
{
|
||||
value: 'mysql',
|
||||
label: 'MariaDB/MySQL',
|
||||
},
|
||||
{
|
||||
value: 'sqlite3',
|
||||
label: 'SQLite',
|
||||
},
|
||||
{
|
||||
value: 'postgres',
|
||||
label: 'PostgreSQL',
|
||||
},
|
||||
];
|
||||
|
||||
const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'install' });
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { db_type, db_username, db_password, db_host, db_name, db_file } =
|
||||
data;
|
||||
|
||||
if (db_type.value !== 'sqllite3') {
|
||||
if (!db_username.value) {
|
||||
bol = false;
|
||||
data.db_username = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('db_username.msg'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!db_password.value) {
|
||||
bol = false;
|
||||
data.db_password = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('db_password.msg'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!db_host.value) {
|
||||
bol = false;
|
||||
data.db_host = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('db_host.msg'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!db_name.value) {
|
||||
bol = false;
|
||||
data.db_name = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('db_name.msg'),
|
||||
};
|
||||
}
|
||||
} else if (!db_file.value) {
|
||||
bol = false;
|
||||
data.db_file = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('db_file.msg'),
|
||||
};
|
||||
}
|
||||
changeCallback({
|
||||
...data,
|
||||
});
|
||||
return bol;
|
||||
};
|
||||
|
||||
const handleSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!checkValidated()) {
|
||||
return;
|
||||
}
|
||||
nextCallback();
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="database_engine" className="mb-3">
|
||||
<Form.Label>{t('db_type.label')}</Form.Label>
|
||||
<Form.Select
|
||||
value={data.db_type.value}
|
||||
isInvalid={data.db_type.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
db_type: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}>
|
||||
{sqlData.map((item) => {
|
||||
return (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
{data.db_type.value !== 'sqlite3' ? (
|
||||
<>
|
||||
<Form.Group controlId="username" className="mb-3">
|
||||
<Form.Label>{t('db_username.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
placeholder={t('db_username.placeholder')}
|
||||
value={data.db_username.value}
|
||||
isInvalid={data.db_username.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
db_username: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{data.db_username.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="db_password" className="mb-3">
|
||||
<Form.Label>{t('db_password.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
placeholder={t('db_password.placeholder')}
|
||||
value={data.db_password.value}
|
||||
isInvalid={data.db_password.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
db_password: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{data.db_password.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="db_host" className="mb-3">
|
||||
<Form.Label>{t('db_host.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
placeholder={t('db_host.placeholder')}
|
||||
value={data.db_host.value}
|
||||
isInvalid={data.db_host.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
db_host: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{data.db_host.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="name" className="mb-3">
|
||||
<Form.Label>{t('db_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
placeholder={t('db_name.placeholder')}
|
||||
value={data.db_name.value}
|
||||
isInvalid={data.db_name.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
db_name: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{data.db_name.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
</>
|
||||
) : (
|
||||
<Form.Group controlId="file" className="mb-3">
|
||||
<Form.Label>{t('db_file.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
placeholder={t('db_file.placeholder')}
|
||||
value={data.db_file.value}
|
||||
isInvalid={data.db_file.isInvalid}
|
||||
onChange={(e) => {
|
||||
changeCallback({
|
||||
db_file: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{data.db_file.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
)}
|
||||
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<Progress step={2} />
|
||||
<Button type="submit">{t('next')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,40 @@
|
|||
import { FC } from 'react';
|
||||
import { Form, Button, FormGroup } from 'react-bootstrap';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import Progress from '../Progress';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
nextCallback: () => void;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ visible, nextCallback }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'install' });
|
||||
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<div>
|
||||
<h5>{t('config_yaml.title')}</h5>
|
||||
<div className="mb-3">{t('config_yaml.label')}</div>
|
||||
<div className="fmt">
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="install.config_yaml.description"
|
||||
components={{ 1: <code /> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<FormGroup className="mb-3">
|
||||
<Form.Control type="text" as="textarea" rows={5} className="fs-14" />
|
||||
</FormGroup>
|
||||
<div className="mb-3">{t('config_yaml.info')}</div>
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<Progress step={3} />
|
||||
<Button onClick={nextCallback}>{t('next')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,7 @@
|
|||
import FirstStep from './FirstStep';
|
||||
import SecondStep from './SecondStep';
|
||||
import ThirdStep from './ThirdStep';
|
||||
import FourthStep from './FourthStep';
|
||||
import Fifth from './FifthStep';
|
||||
|
||||
export { FirstStep, SecondStep, ThirdStep, FourthStep, Fifth };
|
|
@ -0,0 +1,182 @@
|
|||
import { FC, useState, useEffect } from 'react';
|
||||
import { Container, Row, Col, Card, Alert } from 'react-bootstrap';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
import { Storage } from '@/utils';
|
||||
import { PageTitle } from '@/components';
|
||||
|
||||
import {
|
||||
FirstStep,
|
||||
SecondStep,
|
||||
ThirdStep,
|
||||
FourthStep,
|
||||
Fifth,
|
||||
} from './components';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'install' });
|
||||
const [step, setStep] = useState(1);
|
||||
const [showError] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
lang: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
db_type: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
db_username: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
db_password: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
db_host: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
db_name: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
db_file: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
|
||||
site_name: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
contact_email: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
admin_name: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
admin_password: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
admin_email: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleChange = (params: FormDataType) => {
|
||||
console.log(params);
|
||||
setFormData({ ...formData, ...params });
|
||||
};
|
||||
|
||||
const handleStep = () => {
|
||||
setStep((pre) => pre + 1);
|
||||
};
|
||||
|
||||
// const handleSubmit = () => {
|
||||
// const params = {
|
||||
// lang: formData.lang.value,
|
||||
// db_type: formData.db_type.value,
|
||||
// db_username: formData.db_username.value,
|
||||
// db_password: formData.db_password.value,
|
||||
// db_host: formData.db_host.value,
|
||||
// db_name: formData.db_name.value,
|
||||
// db_file: formData.db_file.value,
|
||||
// site_name: formData.site_name.value,
|
||||
// contact_email: formData.contact_email.value,
|
||||
// admin_name: formData.admin_name.value,
|
||||
// admin_password: formData.admin_password.value,
|
||||
// admin_email: formData.admin_email.value,
|
||||
// };
|
||||
|
||||
// console.log(params);
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
console.log('step===', Storage.get('INSTALL_STEP'));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page-wrap2">
|
||||
<PageTitle title={t('install', { keyPrefix: 'page_title' })} />
|
||||
<Container style={{ paddingTop: '74px' }}>
|
||||
<Row className="justify-content-center">
|
||||
<Col lg={6}>
|
||||
<h2 className="mb-4 text-center">{t('title')}</h2>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
{showError && <Alert variant="danger"> show error msg </Alert>}
|
||||
|
||||
<FirstStep
|
||||
visible={step === 1}
|
||||
data={formData.lang}
|
||||
changeCallback={handleChange}
|
||||
nextCallback={handleStep}
|
||||
/>
|
||||
|
||||
<SecondStep
|
||||
visible={step === 2}
|
||||
data={formData}
|
||||
changeCallback={handleChange}
|
||||
nextCallback={handleStep}
|
||||
/>
|
||||
|
||||
<ThirdStep visible={step === 3} nextCallback={handleStep} />
|
||||
|
||||
<FourthStep
|
||||
visible={step === 4}
|
||||
data={formData}
|
||||
changeCallback={handleChange}
|
||||
nextCallback={handleStep}
|
||||
/>
|
||||
|
||||
<Fifth visible={step === 5} />
|
||||
{step === 6 && (
|
||||
<div>
|
||||
<h5>{t('warning')}</h5>
|
||||
<p>
|
||||
<Trans i18nKey="install.warning_description">
|
||||
The file <code>config.yaml</code> already exists. If you
|
||||
need to reset any of the configuration items in this
|
||||
file, please delete it first. You may try{' '}
|
||||
<a href="/">installing now</a>.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 7 && (
|
||||
<div>
|
||||
<h5>{t('installed')}</h5>
|
||||
<p>{t('installed_description')}</p>
|
||||
</div>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -1,60 +1,42 @@
|
|||
import { FC, useEffect } from 'react';
|
||||
import { FC, useEffect, memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import {
|
||||
userInfoStore,
|
||||
siteInfoStore,
|
||||
interfaceStore,
|
||||
toastStore,
|
||||
} from '@answer/stores';
|
||||
import { Header, AdminHeader, Footer, Toast } from '@answer/components';
|
||||
import { useSiteSettings, useCheckUserStatus } from '@answer/api';
|
||||
|
||||
import { siteInfoStore, interfaceStore, toastStore } from '@/stores';
|
||||
import { Header, AdminHeader, Footer, Toast } from '@/components';
|
||||
import { useSiteSettings } from '@/services';
|
||||
import Storage from '@/utils/storage';
|
||||
import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants';
|
||||
|
||||
let isMounted = false;
|
||||
const Layout: FC = () => {
|
||||
const { siteInfo, update: siteStoreUpdate } = siteInfoStore();
|
||||
const { update: interfaceStoreUpdate } = interfaceStore();
|
||||
const { data: siteSettings } = useSiteSettings();
|
||||
const { data: userStatus } = useCheckUserStatus();
|
||||
useEffect(() => {
|
||||
if (siteSettings) {
|
||||
siteStoreUpdate(siteSettings.general);
|
||||
interfaceStoreUpdate(siteSettings.interface);
|
||||
}
|
||||
}, [siteSettings]);
|
||||
const updateUser = userInfoStore((state) => state.update);
|
||||
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const closeToast = () => {
|
||||
toastClear();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (siteSettings) {
|
||||
siteStoreUpdate(siteSettings.general);
|
||||
interfaceStoreUpdate(siteSettings.interface);
|
||||
}
|
||||
}, [siteSettings]);
|
||||
if (!isMounted) {
|
||||
isMounted = true;
|
||||
const lang = Storage.get('LANG');
|
||||
const user = Storage.get('userInfo');
|
||||
if (user) {
|
||||
updateUser(user);
|
||||
}
|
||||
const lang = Storage.get(CURRENT_LANG_STORAGE_KEY);
|
||||
if (lang) {
|
||||
i18n.changeLanguage(lang);
|
||||
}
|
||||
}
|
||||
|
||||
if (userStatus?.status) {
|
||||
const user = Storage.get('userInfo');
|
||||
if (userStatus.status !== user.status) {
|
||||
user.status = userStatus?.status;
|
||||
updateUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<Helmet>
|
||||
|
@ -76,4 +58,4 @@ const Layout: FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
export default memo(Layout);
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { Container } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PageTitle } from '@/components';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'page_maintenance',
|
||||
});
|
||||
return (
|
||||
<div className="page-wrap2">
|
||||
<Container
|
||||
className="d-flex flex-column justify-content-center align-items-center"
|
||||
style={{ minHeight: '100vh' }}>
|
||||
<PageTitle title={t('maintenance', { keyPrefix: 'page_title' })} />
|
||||
<div
|
||||
className="mb-4 text-secondary"
|
||||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
(=‘_‘=)
|
||||
</div>
|
||||
<div className="text-center mb-4">{t('description')}</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -2,7 +2,7 @@ import { memo } from 'react';
|
|||
import { Accordion, ListGroup } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon } from '@answer/components';
|
||||
import { Icon } from '@/components';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next';
|
|||
import dayjs from 'dayjs';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Editor, EditorRef, TagSelector, PageTitle } from '@answer/components';
|
||||
import { Editor, EditorRef, TagSelector, PageTitle } from '@/components';
|
||||
import type * as Type from '@/common/interface';
|
||||
import {
|
||||
saveQuestion,
|
||||
questionDetail,
|
||||
|
@ -14,8 +15,7 @@ import {
|
|||
useQueryRevisions,
|
||||
postAnswer,
|
||||
useQueryQuestionByTitle,
|
||||
} from '@answer/api';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
} from '@/services';
|
||||
|
||||
import SearchQuestion from './components/SearchQuestion';
|
||||
|
||||
|
|
|
@ -10,10 +10,10 @@ import {
|
|||
Comment,
|
||||
FormatTime,
|
||||
htmlRender,
|
||||
} from '@answer/components';
|
||||
import { acceptanceAnswer } from '@answer/api';
|
||||
import { scrollTop } from '@answer/utils';
|
||||
import { AnswerItem } from '@answer/common/interface';
|
||||
} from '@/components';
|
||||
import { scrollTop } from '@/utils';
|
||||
import { AnswerItem } from '@/common/interface';
|
||||
import { acceptanceAnswer } from '@/services';
|
||||
|
||||
interface Props {
|
||||
data: AnswerItem;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { QueryGroup } from '@answer/components';
|
||||
import { QueryGroup } from '@/components';
|
||||
|
||||
interface Props {
|
||||
count: number;
|
||||
|
|
|
@ -11,9 +11,9 @@ import {
|
|||
Comment,
|
||||
FormatTime,
|
||||
htmlRender,
|
||||
} from '@answer/components';
|
||||
import { formatCount } from '@answer/utils';
|
||||
import { following } from '@answer/api';
|
||||
} from '@/components';
|
||||
import { formatCount } from '@/utils';
|
||||
import { following } from '@/services';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
|
|
|
@ -3,16 +3,15 @@ import { Card, ListGroup } from 'react-bootstrap';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useSimilarQuestion } from '@answer/api';
|
||||
import { Icon } from '@answer/components';
|
||||
|
||||
import { userInfoStore } from '@/stores';
|
||||
import { Icon } from '@/components';
|
||||
import { useSimilarQuestion } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
const Index: FC<Props> = ({ id }) => {
|
||||
const { user } = userInfoStore();
|
||||
const { user } = loggedUserInfoStore();
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'related_question',
|
||||
});
|
||||
|
|
|
@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next';
|
|||
import { marked } from 'marked';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Editor, Modal } from '@answer/components';
|
||||
import { postAnswer } from '@answer/api';
|
||||
import { FormDataType } from '@answer/common/interface';
|
||||
import { Editor, Modal } from '@/components';
|
||||
import { FormDataType } from '@/common/interface';
|
||||
import { postAnswer } from '@/services';
|
||||
|
||||
interface Props {
|
||||
visible?: boolean;
|
||||
|
|
|
@ -2,16 +2,16 @@ import { useEffect, useState } from 'react';
|
|||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { questionDetail, getAnswers } from '@answer/api';
|
||||
import { Pagination, PageTitle } from '@answer/components';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { scrollTop } from '@answer/utils';
|
||||
import { usePageUsers } from '@answer/hooks';
|
||||
import { Pagination, PageTitle } from '@/components';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { scrollTop } from '@/utils';
|
||||
import { usePageUsers } from '@/hooks';
|
||||
import type {
|
||||
ListResult,
|
||||
QuestionDetailRes,
|
||||
AnswerItem,
|
||||
} from '@answer/common/interface';
|
||||
} from '@/common/interface';
|
||||
import { questionDetail, getAnswers } from '@/services';
|
||||
|
||||
import {
|
||||
Question,
|
||||
|
@ -37,7 +37,7 @@ const Index = () => {
|
|||
list: [],
|
||||
});
|
||||
const { setUsers } = usePageUsers();
|
||||
const userInfo = userInfoStore((state) => state.user);
|
||||
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||
const isAuthor = userInfo?.username === question?.user_info?.username;
|
||||
const requestAnswers = async () => {
|
||||
const res = await getAnswers({
|
||||
|
|
|
@ -6,13 +6,13 @@ import { useTranslation } from 'react-i18next';
|
|||
import dayjs from 'dayjs';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Editor, EditorRef, Icon, PageTitle } from '@answer/components';
|
||||
import { Editor, EditorRef, Icon, PageTitle } from '@/components';
|
||||
import type * as Type from '@/common/interface';
|
||||
import {
|
||||
useQueryAnswerInfo,
|
||||
modifyAnswer,
|
||||
useQueryRevisions,
|
||||
} from '@answer/api';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
} from '@/services';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
|
|
|
@ -3,8 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap';
|
|||
import { useMatch } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PageTitle, FollowingTags } from '@answer/components';
|
||||
|
||||
import { PageTitle, FollowingTags } from '@/components';
|
||||
import QuestionList from '@/components/QuestionList';
|
||||
import HotQuestions from '@/components/HotQuestions';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
|
|
|
@ -3,8 +3,8 @@ import { useSearchParams, Link } from 'react-router-dom';
|
|||
import { Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { following } from '@answer/api';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import { following } from '@/services';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
|
||||
interface Props {
|
||||
data;
|
||||
|
@ -20,7 +20,7 @@ const Index: FC<Props> = ({ data }) => {
|
|||
const [followed, setFollowed] = useState(data?.is_follower);
|
||||
|
||||
const follow = () => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
following({
|
||||
|
|
|
@ -2,7 +2,7 @@ import { FC, memo } from 'react';
|
|||
import { ListGroupItem } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { QueryGroup } from '@answer/components';
|
||||
import { QueryGroup } from '@/components';
|
||||
|
||||
const sortBtns = ['relevance', 'newest', 'active', 'score'];
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ import { memo, FC } from 'react';
|
|||
import { ListGroupItem, Badge } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon, Tag, FormatTime, BaseUserCard } from '@answer/components';
|
||||
import type { SearchResItem } from '@answer/common/interface';
|
||||
import { Icon, Tag, FormatTime, BaseUserCard } from '@/components';
|
||||
import type { SearchResItem } from '@/common/interface';
|
||||
|
||||
interface Props {
|
||||
data: SearchResItem;
|
||||
|
|
|
@ -3,8 +3,8 @@ import { Container, Row, Col, ListGroup } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { Pagination, PageTitle } from '@answer/components';
|
||||
import { useSearch } from '@answer/api';
|
||||
import { Pagination, PageTitle } from '@/components';
|
||||
import { useSearch } from '@/services';
|
||||
|
||||
import { Head, SearchHead, SearchItem, Tips, Empty } from './components';
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue