mirror of https://gitee.com/answerdev/answer.git
Merge branch 'release/1.0.3' into 'main'
Release/1.0.3 See merge request opensource/answer!421
This commit is contained in:
commit
006d89a405
|
@ -181,7 +181,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
reportAdminService := report_admin.NewReportAdminService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo)
|
||||
controller_adminReportController := controller_admin.NewReportController(reportAdminService)
|
||||
userAdminRepo := user.NewUserAdminRepo(dataData, authRepo)
|
||||
userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon)
|
||||
userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo)
|
||||
userAdminController := controller_admin.NewUserAdminController(userAdminService)
|
||||
reasonRepo := reason.NewReasonRepo(configRepo)
|
||||
reasonService := reason2.NewReasonService(reasonRepo)
|
||||
|
|
|
@ -6948,10 +6948,6 @@ const docTemplate = `{
|
|||
},
|
||||
"schema.SiteBrandingReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"logo",
|
||||
"square_icon"
|
||||
],
|
||||
"properties": {
|
||||
"favicon": {
|
||||
"type": "string",
|
||||
|
@ -6973,10 +6969,6 @@ const docTemplate = `{
|
|||
},
|
||||
"schema.SiteBrandingResp": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"logo",
|
||||
"square_icon"
|
||||
],
|
||||
"properties": {
|
||||
"favicon": {
|
||||
"type": "string",
|
||||
|
|
|
@ -6936,10 +6936,6 @@
|
|||
},
|
||||
"schema.SiteBrandingReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"logo",
|
||||
"square_icon"
|
||||
],
|
||||
"properties": {
|
||||
"favicon": {
|
||||
"type": "string",
|
||||
|
@ -6961,10 +6957,6 @@
|
|||
},
|
||||
"schema.SiteBrandingResp": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"logo",
|
||||
"square_icon"
|
||||
],
|
||||
"properties": {
|
||||
"favicon": {
|
||||
"type": "string",
|
||||
|
|
|
@ -1241,9 +1241,6 @@ definitions:
|
|||
square_icon:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- logo
|
||||
- square_icon
|
||||
type: object
|
||||
schema.SiteBrandingResp:
|
||||
properties:
|
||||
|
@ -1259,9 +1256,6 @@ definitions:
|
|||
square_icon:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- logo
|
||||
- square_icon
|
||||
type: object
|
||||
schema.SiteCustomCssHTMLReq:
|
||||
properties:
|
||||
|
|
4
go.mod
4
go.mod
|
@ -15,7 +15,6 @@ require (
|
|||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/goccy/go-json v0.9.11
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/wire v0.5.0
|
||||
github.com/gosimple/slug v1.13.1
|
||||
|
@ -24,6 +23,7 @@ require (
|
|||
github.com/jinzhu/now v1.1.5
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/microcosm-cc/bluemonday v1.0.21
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/ory/dockertest/v3 v3.9.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
|
@ -55,6 +55,7 @@ require (
|
|||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/containerd/continuity v0.3.0 // indirect
|
||||
github.com/docker/cli v20.10.14+incompatible // indirect
|
||||
|
@ -71,6 +72,7 @@ require (
|
|||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gosimple/unidecode v1.0.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
|
|
8
go.sum
8
go.sum
|
@ -82,6 +82,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
|
|||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
|
@ -256,8 +258,6 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
|
|||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c h1:iyaGYbCmcYK0Ja9a3OUa2Fo+EaN0cbLu0eKpBwPFzc8=
|
||||
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
|
@ -301,6 +301,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
|
|||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
|
@ -476,6 +478,8 @@ github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
|
|||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
|
|
|
@ -607,7 +607,8 @@ ui:
|
|||
msg:
|
||||
empty: Cannot be empty.
|
||||
login:
|
||||
page_title: Welcome to Answer
|
||||
page_title: Welcome to {{site_name}}
|
||||
login_to_continue: Log in to continue
|
||||
info_sign: Don't have an account? <1>Sign up</1>
|
||||
info_login: Already have an account? <1>Log in</1>
|
||||
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
|
||||
|
@ -1266,14 +1267,14 @@ ui:
|
|||
branding:
|
||||
page_title: Branding
|
||||
logo:
|
||||
label: Logo
|
||||
label: Logo (optional)
|
||||
msg: Logo cannot be empty.
|
||||
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
|
||||
mobile_logo:
|
||||
label: Mobile Logo (optional)
|
||||
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
|
||||
square_icon:
|
||||
label: Square Icon
|
||||
label: Square Icon (optional)
|
||||
msg: Square icon cannot be empty.
|
||||
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
|
||||
favicon:
|
||||
|
|
|
@ -21,7 +21,7 @@ type RespBody struct {
|
|||
// TrMsg translate the reason cause as a message
|
||||
func (r *RespBody) TrMsg(lang i18n.Language) *RespBody {
|
||||
if len(r.Message) == 0 {
|
||||
r.Message = translator.GlobalTrans.Tr(lang, r.Reason)
|
||||
r.Message = translator.Tr(lang, r.Reason)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -106,3 +106,12 @@ func CheckLanguageIsValid(lang string) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Tr use language to translate data. If this language translation is not available, return default english translation.
|
||||
func Tr(lang i18n.Language, data string) string {
|
||||
translation := GlobalTrans.Tr(lang, data)
|
||||
if translation == data {
|
||||
return GlobalTrans.Tr(i18n.DefaultLanguage, data)
|
||||
}
|
||||
return translation
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ func createDefaultValidator(la i18n.Language) *validator.Validate {
|
|||
validate.RegisterTagNameFunc(func(fld reflect.StructField) (res string) {
|
||||
defer func() {
|
||||
if len(res) > 0 {
|
||||
res = translator.GlobalTrans.Tr(la, res)
|
||||
res = translator.Tr(la, res)
|
||||
}
|
||||
}()
|
||||
if jsonTag := fld.Tag.Get("json"); len(jsonTag) > 0 {
|
||||
|
@ -168,7 +168,7 @@ func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err
|
|||
return nil, nil
|
||||
}
|
||||
for _, errField := range errFields {
|
||||
errField.ErrorMsg = translator.GlobalTrans.Tr(m.Lang, errField.ErrorMsg)
|
||||
errField.ErrorMsg = translator.Tr(m.Lang, errField.ErrorMsg)
|
||||
}
|
||||
return errFields, err
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ package controller
|
|||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -63,3 +65,21 @@ func (uc *UploadController) UploadFile(ctx *gin.Context) {
|
|||
}
|
||||
handler.HandleResponse(ctx, err, url)
|
||||
}
|
||||
|
||||
// PostRender render post content
|
||||
// @Summary render post content
|
||||
// @Description render post content
|
||||
// @Tags Upload
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.PostRenderReq true "PostRenderReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/post/render [post]
|
||||
func (uc *UploadController) PostRender(ctx *gin.Context) {
|
||||
req := &schema.PostRenderReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, converter.Markdown2HTML(req.Content))
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
|
|||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
|
@ -124,7 +124,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
|
|||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "e_mail",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields)
|
||||
return
|
||||
|
@ -151,7 +151,7 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
|
|||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
|
@ -236,7 +236,7 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
|
|||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
|
@ -245,7 +245,8 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
|
|||
resp, errFields, err := uc.userService.UserRegisterByEmail(ctx, req)
|
||||
if len(errFields) > 0 {
|
||||
for _, field := range errFields {
|
||||
field.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), field.ErrorMsg)
|
||||
field.ErrorMsg = translator.
|
||||
Tr(handler.GetLang(ctx), field.ErrorMsg)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, errFields)
|
||||
} else {
|
||||
|
@ -312,7 +313,7 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
|
|||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
|
@ -350,7 +351,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
|||
if !oldPassVerification {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "old_pass",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.OldPasswordVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.OldPasswordVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields)
|
||||
return
|
||||
|
@ -358,7 +359,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
|||
if req.OldPass == req.Pass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.NewPasswordSameAsPreviousSetting),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.NewPasswordSameAsPreviousSetting),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields)
|
||||
return
|
||||
|
@ -386,7 +387,7 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
|
|||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
errFields, err := uc.userService.UpdateInfo(ctx, req)
|
||||
for _, field := range errFields {
|
||||
field.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), field.ErrorMsg)
|
||||
field.ErrorMsg = translator.Tr(handler.GetLang(ctx), field.ErrorMsg)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, errFields)
|
||||
}
|
||||
|
@ -491,7 +492,7 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
|
|||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
|
|
|
@ -77,7 +77,7 @@ type InitBaseInfoReq struct {
|
|||
SiteName string `validate:"required,gt=0,lte=30" json:"site_name"`
|
||||
SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"`
|
||||
ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"`
|
||||
AdminName string `validate:"required,gt=4,lte=30" json:"name"`
|
||||
AdminName string `validate:"required,gt=3,lte=30" json:"name"`
|
||||
AdminPassword string `validate:"required,gte=8,lte=32" json:"password"`
|
||||
AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"`
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, hasR
|
|||
cond := &entity.Tag{}
|
||||
session := tr.data.DB.Where("")
|
||||
if name != "" {
|
||||
session.Where("slug_name LIKE ?", name+"%")
|
||||
session.Where("slug_name LIKE ? or display_name LIKE ?", name+"%", name+"%")
|
||||
} else {
|
||||
session.UseBool("recommend")
|
||||
cond.Recommend = true
|
||||
|
|
|
@ -217,6 +217,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
|
||||
// upload file
|
||||
r.POST("/file", a.uploadController.UploadFile)
|
||||
r.POST("/post/render", a.uploadController.PostRender)
|
||||
|
||||
// activity
|
||||
r.GET("/activity/timeline", a.activityController.GetObjectTimeline)
|
||||
|
|
|
@ -246,9 +246,9 @@ type QuestionPageReq struct {
|
|||
}
|
||||
|
||||
const (
|
||||
QuestionPageRespOperationTypeAsked = "question.operation_type.asked"
|
||||
QuestionPageRespOperationTypeAnswered = "question.operation_type.answered"
|
||||
QuestionPageRespOperationTypeModified = "question.operation_type.modified"
|
||||
QuestionPageRespOperationTypeAsked = "asked"
|
||||
QuestionPageRespOperationTypeAnswered = "answered"
|
||||
QuestionPageRespOperationTypeModified = "modified"
|
||||
)
|
||||
|
||||
type QuestionPageResp struct {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package schema
|
||||
|
||||
// PostRenderReq post render request
|
||||
type PostRenderReq struct {
|
||||
Content string `json:"content"`
|
||||
}
|
|
@ -46,9 +46,9 @@ type SiteInterfaceReq struct {
|
|||
|
||||
// SiteBrandingReq site branding request
|
||||
type SiteBrandingReq struct {
|
||||
Logo string `validate:"required,gt=0,lte=512" form:"logo" json:"logo"`
|
||||
Logo string `validate:"omitempty,gt=0,lte=512" form:"logo" json:"logo"`
|
||||
MobileLogo string `validate:"omitempty,gt=0,lte=512" form:"mobile_logo" json:"mobile_logo"`
|
||||
SquareIcon string `validate:"required,gt=0,lte=512" form:"square_icon" json:"square_icon"`
|
||||
SquareIcon string `validate:"omitempty,gt=0,lte=512" form:"square_icon" json:"square_icon"`
|
||||
Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"`
|
||||
}
|
||||
|
||||
|
@ -134,7 +134,7 @@ type SiteThemeResp struct {
|
|||
func (s *SiteThemeResp) TrTheme(ctx context.Context) {
|
||||
la := handler.GetLangByCtx(ctx)
|
||||
for _, option := range s.ThemeOptions {
|
||||
tr := translator.GlobalTrans.Tr(la, option.Value)
|
||||
tr := translator.Tr(la, option.Value)
|
||||
// if tr is equal the option value means not found translation, so use the original label
|
||||
if tr != option.Value {
|
||||
option.Label = tr
|
||||
|
|
|
@ -228,7 +228,7 @@ type UserEmailLogin struct {
|
|||
// UserRegisterReq user register request
|
||||
type UserRegisterReq struct {
|
||||
// name
|
||||
Name string `validate:"required,gt=4,lte=30" json:"name"`
|
||||
Name string `validate:"required,gt=3,lte=30" json:"name"`
|
||||
// email
|
||||
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" `
|
||||
// password
|
||||
|
@ -277,7 +277,7 @@ type UpdateInfoRequest struct {
|
|||
// display_name
|
||||
DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"`
|
||||
// username
|
||||
Username string `validate:"omitempty,gt=0,lte=30" json:"username"`
|
||||
Username string `validate:"omitempty,gt=3,lte=30" json:"username"`
|
||||
// avatar
|
||||
Avatar AvatarInfo `json:"avatar"`
|
||||
// bio
|
||||
|
@ -300,12 +300,13 @@ type AvatarInfo struct {
|
|||
|
||||
func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if len(u.Username) > 0 {
|
||||
errFields := make([]*validator.FormErrorField, 0)
|
||||
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
|
||||
match := re.MatchString(u.Username)
|
||||
if !match {
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: err.Error(),
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
|
|
|
@ -134,7 +134,7 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
|
|||
continue
|
||||
}
|
||||
lang, _ := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language)
|
||||
item.NotificationAction = translator.GlobalTrans.Tr(lang, item.NotificationAction)
|
||||
item.NotificationAction = translator.Tr(lang, item.NotificationAction)
|
||||
item.ID = notificationInfo.ID
|
||||
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
|
||||
if notificationInfo.IsRead == schema.NotificationRead {
|
||||
|
|
|
@ -6,9 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
|
@ -250,13 +248,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
|
|||
func (qs *QuestionCommon) FormatQuestionsPage(
|
||||
ctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) (
|
||||
formattedQuestions []*schema.QuestionPageResp, err error) {
|
||||
language := handler.GetLangByCtx(ctx)
|
||||
askedOp := translator.GlobalTrans.Tr(language, schema.QuestionPageRespOperationTypeAsked)
|
||||
answeredOp := translator.GlobalTrans.Tr(language, schema.QuestionPageRespOperationTypeAnswered)
|
||||
modifiedOp := translator.GlobalTrans.Tr(language, schema.QuestionPageRespOperationTypeModified)
|
||||
|
||||
formattedQuestions = make([]*schema.QuestionPageResp, 0)
|
||||
|
||||
questionIDs := make([]string, 0)
|
||||
userIDs := make([]string, 0)
|
||||
for _, questionInfo := range questionList {
|
||||
|
@ -300,20 +292,20 @@ func (qs *QuestionCommon) FormatQuestionsPage(
|
|||
|
||||
// if order condition is newest or nobody edited or nobody answered, only show question author
|
||||
if orderCond == schema.QuestionOrderCondNewest || (!haveEdited && !haveAnswered) {
|
||||
t.OperationType = askedOp
|
||||
t.OperationType = schema.QuestionPageRespOperationTypeAsked
|
||||
t.OperatedAt = questionInfo.CreatedAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID}
|
||||
} else {
|
||||
// if no one
|
||||
if haveEdited {
|
||||
t.OperationType = modifiedOp
|
||||
t.OperationType = schema.QuestionPageRespOperationTypeModified
|
||||
t.OperatedAt = questionInfo.UpdatedAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID}
|
||||
}
|
||||
|
||||
if haveAnswered {
|
||||
if t.LastAnsweredAt.Unix() > t.OperatedAt {
|
||||
t.OperationType = answeredOp
|
||||
t.OperationType = schema.QuestionPageRespOperationTypeAnswered
|
||||
t.OperatedAt = t.LastAnsweredAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: t.LastAnsweredUserID}
|
||||
}
|
||||
|
|
|
@ -140,8 +140,8 @@ func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language)
|
|||
return nil, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
for _, t := range resp {
|
||||
t.Name = translator.GlobalTrans.Tr(lang, t.Name)
|
||||
t.Description = translator.GlobalTrans.Tr(lang, t.Description)
|
||||
t.Name = translator.Tr(lang, t.Name)
|
||||
t.Description = translator.Tr(lang, t.Description)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.Que
|
|||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
return errorlist, err
|
||||
|
@ -176,7 +176,7 @@ func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.Que
|
|||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
return errorlist, err
|
||||
|
@ -213,7 +213,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
|||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
return errorlist, err
|
||||
|
@ -226,7 +226,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
|||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
return errorlist, err
|
||||
|
@ -539,7 +539,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
|
|||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
return errorlist, err
|
||||
|
|
|
@ -74,8 +74,8 @@ func (rs *ReportService) GetReportTypeList(ctx context.Context, lang i18n.Langua
|
|||
err = errors.BadRequest(reason.UnknownError)
|
||||
}
|
||||
for _, t := range resp {
|
||||
t.Name = translator.GlobalTrans.Tr(lang, t.Name)
|
||||
t.Description = translator.GlobalTrans.Tr(lang, t.Description)
|
||||
t.Name = translator.Tr(lang, t.Name)
|
||||
t.Description = translator.Tr(lang, t.Description)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
|
|
@ -72,13 +72,13 @@ func (rs *RoleService) GetRoleMapping(ctx context.Context) (roleMapping map[int]
|
|||
func (rs *RoleService) translateRole(ctx context.Context, role *entity.Role) {
|
||||
switch role.Name {
|
||||
case roleUserName:
|
||||
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameUser)
|
||||
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser)
|
||||
role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameUser)
|
||||
role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser)
|
||||
case roleAdminName:
|
||||
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin)
|
||||
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin)
|
||||
role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin)
|
||||
role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin)
|
||||
case roleModeratorName:
|
||||
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator)
|
||||
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator)
|
||||
role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator)
|
||||
role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -496,7 +496,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
|
|||
thisObjTagNameList := make([]string, 0)
|
||||
thisObjTagIDList := make([]string, 0)
|
||||
for _, t := range objectTagData.Tags {
|
||||
t.SlugName = strings.ToLower(t.SlugName)
|
||||
// t.SlugName = strings.ToLower(t.SlugName)
|
||||
thisObjTagNameList = append(thisObjTagNameList, t.SlugName)
|
||||
}
|
||||
|
||||
|
@ -508,13 +508,13 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
|
|||
|
||||
tagInDbMapping := make(map[string]*entity.Tag)
|
||||
for _, tag := range tagListInDb {
|
||||
tagInDbMapping[tag.SlugName] = tag
|
||||
tagInDbMapping[strings.ToLower(tag.SlugName)] = tag
|
||||
thisObjTagIDList = append(thisObjTagIDList, tag.ID)
|
||||
}
|
||||
|
||||
addTagList := make([]*entity.Tag, 0)
|
||||
for _, tag := range objectTagData.Tags {
|
||||
_, ok := tagInDbMapping[tag.SlugName]
|
||||
_, ok := tagInDbMapping[strings.ToLower(tag.SlugName)]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/activity"
|
||||
"github.com/answerdev/answer/internal/service/auth"
|
||||
"github.com/answerdev/answer/internal/service/role"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
|
@ -38,6 +39,7 @@ type UserAdminService struct {
|
|||
userRoleRelService *role.UserRoleRelService
|
||||
authService *auth.AuthService
|
||||
userCommonService *usercommon.UserCommon
|
||||
userActivity activity.UserActiveActivityRepo
|
||||
}
|
||||
|
||||
// NewUserAdminService new user admin service
|
||||
|
@ -46,12 +48,14 @@ func NewUserAdminService(
|
|||
userRoleRelService *role.UserRoleRelService,
|
||||
authService *auth.AuthService,
|
||||
userCommonService *usercommon.UserCommon,
|
||||
userActivity activity.UserActiveActivityRepo,
|
||||
) *UserAdminService {
|
||||
return &UserAdminService{
|
||||
userRepo: userRepo,
|
||||
userRoleRelService: userRoleRelService,
|
||||
authService: authService,
|
||||
userCommonService: userCommonService,
|
||||
userActivity: userActivity,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +87,17 @@ func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.Up
|
|||
userInfo.Status = entity.UserStatusAvailable
|
||||
userInfo.MailStatus = entity.EmailStatusAvailable
|
||||
}
|
||||
return us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail)
|
||||
|
||||
err = us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if user reputation is zero means this user is inactive, so try to activate this user.
|
||||
if req.IsNormal() && userInfo.Rank == 0 {
|
||||
return us.userActivity.UserActive(ctx, userInfo.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserRole update user role
|
||||
|
|
|
@ -510,7 +510,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
|
|||
if exist {
|
||||
resp = append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "e_mail",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.EmailDuplicate),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailDuplicate),
|
||||
})
|
||||
return resp, errors.BadRequest(reason.EmailDuplicate)
|
||||
}
|
||||
|
|
|
@ -3,22 +3,26 @@ package converter
|
|||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
goldmarkHTML "github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// Markdown2HTML convert markdown to html
|
||||
func Markdown2HTML(source string) string {
|
||||
mdConverter := goldmark.New(
|
||||
goldmark.WithExtensions(extension.GFM),
|
||||
goldmark.WithExtensions(&DangerousHTMLFilterExtension{}, extension.GFM),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithHardWraps(),
|
||||
goldmarkHTML.WithHardWraps(),
|
||||
),
|
||||
)
|
||||
var buf bytes.Buffer
|
||||
|
@ -28,3 +32,56 @@ func Markdown2HTML(source string) string {
|
|||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type DangerousHTMLFilterExtension struct {
|
||||
}
|
||||
|
||||
func (e *DangerousHTMLFilterExtension) Extend(m goldmark.Markdown) {
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(&DangerousHTMLRenderer{
|
||||
Config: goldmarkHTML.NewConfig(),
|
||||
Filter: bluemonday.UGCPolicy(),
|
||||
}, 1),
|
||||
))
|
||||
}
|
||||
|
||||
type DangerousHTMLRenderer struct {
|
||||
goldmarkHTML.Config
|
||||
Filter *bluemonday.Policy
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
|
||||
reg.Register(ast.KindRawHTML, r.renderRawHTML)
|
||||
}
|
||||
|
||||
func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
n := node.(*ast.RawHTML)
|
||||
l := n.Segments.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
segment := n.Segments.At(i)
|
||||
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.HTMLBlock)
|
||||
if entering {
|
||||
l := n.Lines().Len()
|
||||
for i := 0; i < l; i++ {
|
||||
line := n.Lines().At(i)
|
||||
r.Writer.SecureWrite(w, r.Filter.SanitizeBytes(line.Value(source)))
|
||||
}
|
||||
} else {
|
||||
if n.HasClosure() {
|
||||
closure := n.ClosureLine
|
||||
r.Writer.SecureWrite(w, closure.Value(source))
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ If you would like to help us with the i18n translation, please visit [Answer@Cro
|
|||
├── common (project information/data defined here)
|
||||
├── components (all components of the project)
|
||||
├── hooks (all hooks of the project)
|
||||
├── i18n (Initialize the front-end i18n)
|
||||
├── i18n (Used only to initialize the front-end i18n tool)
|
||||
├── pages (all pages of the project)
|
||||
├── router (Project routing definition)
|
||||
├── services (all data api of the project)
|
||||
|
|
|
@ -1 +1 @@
|
|||
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.fde484b3.js"></script><link href="/static/css/main.401dc3ca.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask"><noscript><style>#spin-mask{display:none!important}</style></noscript><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html>
|
||||
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.cb9bf782.js"></script><link href="/static/css/main.b8d8739f.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask"><noscript><style>#spin-mask{display:none!important}</style></noscript><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html>
|
|
@ -1,3 +1,5 @@
|
|||
$link-hover-decoration: none;
|
||||
$enable-negative-margins: true;
|
||||
$blue: #0033FF !default;
|
||||
$placeholder-opacity-max: .2;
|
||||
$placeholder-opacity-min: .1;
|
||||
|
|
|
@ -102,6 +102,7 @@ export interface ModifyUserReq {
|
|||
}
|
||||
|
||||
export interface UserInfoBase {
|
||||
id?: string;
|
||||
avatar: any;
|
||||
username: string;
|
||||
display_name: string;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { marked } from 'marked';
|
|||
import * as Types from '@/common/interface';
|
||||
import { Modal } from '@/components';
|
||||
import { usePageUsers, useReportModal } from '@/hooks';
|
||||
import { matchedUsers, parseUserInfo, scrollTop } from '@/utils';
|
||||
import { matchedUsers, parseUserInfo, scrollTop, bgFadeOut } from '@/utils';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import {
|
||||
useQueryComments,
|
||||
|
@ -27,7 +27,6 @@ import './index.scss';
|
|||
const Comment = ({ objectId, mode, commentId }) => {
|
||||
const pageUsers = usePageUsers();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [comments, setComments] = useState<any>([]);
|
||||
const [visibleComment, setVisibleComment] = useState(false);
|
||||
const pageSize = pageIndex === 0 ? 3 : 15;
|
||||
const { data, mutate } = useQueryComments({
|
||||
|
@ -36,6 +35,7 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
page: pageIndex,
|
||||
page_size: pageSize,
|
||||
});
|
||||
const [comments, setComments] = useState<any>([]);
|
||||
|
||||
const reportModal = useReportModal();
|
||||
|
||||
|
@ -44,6 +44,7 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
if (pageIndex === 0 && co.comment_id === commentId) {
|
||||
setTimeout(() => {
|
||||
scrollTop(el);
|
||||
bgFadeOut(el);
|
||||
}, 100);
|
||||
}
|
||||
}, []);
|
||||
|
@ -74,6 +75,9 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
}, [data]);
|
||||
|
||||
const handleReply = (id) => {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
setComments(
|
||||
comments.map((item) => {
|
||||
if (item.comment_id === id) {
|
||||
|
@ -99,6 +103,9 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
const users = matchedUsers(item.value);
|
||||
const userNames = unionBy(users.map((user) => user.userName));
|
||||
const html = marked.parse(parseUserInfo(item.value));
|
||||
if (!item.value || !html) {
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
object_id: objectId,
|
||||
original_text: item.value,
|
||||
|
@ -163,9 +170,8 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
deleteComment(id).then(() => {
|
||||
if (pageIndex === 0) {
|
||||
mutate();
|
||||
} else {
|
||||
setComments(comments.filter((item) => item.comment_id !== id));
|
||||
}
|
||||
setComments(comments.filter((item) => item.comment_id !== id));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -302,7 +308,9 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
variant="link"
|
||||
className="p-0 fs-14 btn-no-border"
|
||||
onClick={() => {
|
||||
setVisibleComment(!visibleComment);
|
||||
if (tryNormalLogged(true)) {
|
||||
setVisibleComment(!visibleComment);
|
||||
}
|
||||
}}>
|
||||
{t('btn_add_comment')}
|
||||
</Button>
|
||||
|
|
|
@ -251,7 +251,7 @@ const Image: FC<IEditorContext> = ({ editor }) => {
|
|||
|
||||
<Form.Group controlId="editor.imgDescription" className="mb-3">
|
||||
<Form.Label>
|
||||
{t('image.form_image.fields.description.label')}
|
||||
{t('image.form_image.fields.desc.label')}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
|
|
|
@ -7,28 +7,31 @@ import {
|
|||
useImperativeHandle,
|
||||
} from 'react';
|
||||
|
||||
import { marked } from 'marked';
|
||||
import { markdownToHtml } from '@/services';
|
||||
|
||||
import { htmlRender } from './utils';
|
||||
|
||||
let scrollTop = 0;
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
});
|
||||
let renderTimer;
|
||||
|
||||
const Index = ({ value }, ref) => {
|
||||
const [html, setHtml] = useState('');
|
||||
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const renderMarkdown = (markdown) => {
|
||||
clearTimeout(renderTimer);
|
||||
const timeout = renderTimer ? 1000 : 0;
|
||||
renderTimer = setTimeout(() => {
|
||||
markdownToHtml(markdown).then((resp) => {
|
||||
scrollTop = previewRef.current?.scrollTop || 0;
|
||||
setHtml(resp);
|
||||
});
|
||||
}, timeout);
|
||||
};
|
||||
useEffect(() => {
|
||||
const previewHtml = marked(value).replace(
|
||||
/<img/gi,
|
||||
'<img referrerpolicy="no-referrer"',
|
||||
);
|
||||
scrollTop = previewRef.current?.scrollTop || 0;
|
||||
setHtml(previewHtml);
|
||||
renderMarkdown(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!html) {
|
||||
return;
|
||||
|
|
|
@ -56,6 +56,7 @@ const ToolItem: FC<IProps> = (props) => {
|
|||
disable ? 'disabled' : ''
|
||||
} `}
|
||||
disabled={disable}
|
||||
tabIndex={-1}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (typeof onClick === 'function') {
|
||||
|
|
|
@ -113,4 +113,20 @@ export function htmlRender(el: HTMLElement | null) {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
el.querySelectorAll('table').forEach((table) => {
|
||||
if (
|
||||
(table.parentNode as HTMLDivElement)?.classList.contains(
|
||||
'table-responsive',
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
table.classList.add('table', 'table-bordered');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'table-responsive';
|
||||
table.parentNode?.replaceChild(div, table);
|
||||
div.appendChild(table);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,12 +5,10 @@ import { Trans } from 'react-i18next';
|
|||
import dayjs from 'dayjs';
|
||||
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||
|
||||
const Index = () => {
|
||||
const fullYear = dayjs().format('YYYY');
|
||||
const siteName =
|
||||
siteInfoStore((state) => state.siteInfo.name) || DEFAULT_SITE_NAME;
|
||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
||||
const cc = `${fullYear} ${siteName}`;
|
||||
return (
|
||||
<footer className="bg-light py-3">
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
siteInfoStore,
|
||||
|
@ -27,8 +28,6 @@ import {
|
|||
themeSettingStore,
|
||||
} from '@/stores';
|
||||
import { logout, useQueryNotificationStatus } from '@/services';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||
|
||||
import NavItems from './components/NavItems';
|
||||
|
||||
|
@ -36,7 +35,7 @@ import './index.scss';
|
|||
|
||||
const Header: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, clear } = loggedUserInfoStore();
|
||||
const { user, clear: clearUserStore } = loggedUserInfoStore();
|
||||
const { t } = useTranslation();
|
||||
const [urlSearch] = useSearchParams();
|
||||
const q = urlSearch.get('q');
|
||||
|
@ -60,8 +59,14 @@ const Header: FC = () => {
|
|||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
clear();
|
||||
navigate(RouteAlias.home);
|
||||
clearUserStore();
|
||||
window.location.replace(window.location.href);
|
||||
};
|
||||
const onLoginClick = (evt) => {
|
||||
evt.preventDefault();
|
||||
floppyNavigation.navigateToLogin((loginPath) => {
|
||||
navigate(loginPath, { replace: true });
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -116,7 +121,7 @@ const Header: FC = () => {
|
|||
/>
|
||||
</>
|
||||
) : (
|
||||
<span>{siteInfo.name || DEFAULT_SITE_NAME}</span>
|
||||
<span>{siteInfo.name}</span>
|
||||
)}
|
||||
</Navbar.Brand>
|
||||
|
||||
|
@ -132,6 +137,7 @@ const Header: FC = () => {
|
|||
'link-light': navbarStyle === 'theme-colored',
|
||||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
onClick={onLoginClick}
|
||||
href="/users/login">
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
|
@ -219,6 +225,7 @@ const Header: FC = () => {
|
|||
'link-light': navbarStyle === 'theme-colored',
|
||||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
onClick={onLoginClick}
|
||||
href="/users/login">
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { loginToContinueStore, siteInfoStore } from '@/stores';
|
||||
|
||||
interface IProps {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const Index: React.FC<IProps> = ({ visible = false }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
const { update: updateStore } = loginToContinueStore();
|
||||
const { siteInfo } = siteInfoStore((_) => _);
|
||||
const closeModal = () => {
|
||||
updateStore({ show: false });
|
||||
};
|
||||
const linkClick = (evt) => {
|
||||
evt.stopPropagation();
|
||||
closeModal();
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
title="LoginToContinue"
|
||||
show={visible}
|
||||
onHide={closeModal}
|
||||
centered
|
||||
fullscreen="sm-down">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title as="h5">{t('login_to_continue')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="p-5">
|
||||
<div className="d-flex flex-column align-items-center text-center text-body">
|
||||
<h3>{t('page_title', { site_name: siteInfo.name })}</h3>
|
||||
<p>{siteInfo.description}</p>
|
||||
</div>
|
||||
<div className="d-grid gap-2">
|
||||
<Link
|
||||
to="/users/login"
|
||||
className="btn btn-primary"
|
||||
onClick={linkClick}>
|
||||
{t('login', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
<Link
|
||||
to="/users/register"
|
||||
className="btn btn-link"
|
||||
onClick={linkClick}>
|
||||
{t('signup', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default Index;
|
|
@ -1,6 +1,7 @@
|
|||
import DefaultModal from './Modal';
|
||||
import confirm, { Config } from './Confirm';
|
||||
import PicAuthCodeModal from './PicAuthCodeModal';
|
||||
import LoginToContinueModal from './LoginToContinueModal';
|
||||
|
||||
type ModalType = typeof DefaultModal & {
|
||||
confirm: (config: Config) => void;
|
||||
|
@ -14,3 +15,4 @@ Modal.confirm = function (props: Config) {
|
|||
export default Modal;
|
||||
|
||||
export { PicAuthCodeModal };
|
||||
export { LoginToContinueModal };
|
||||
|
|
|
@ -41,10 +41,10 @@ const Index: FC<IProps> = ({
|
|||
const navigate = useNavigate();
|
||||
const reportModal = useReportModal();
|
||||
|
||||
const refershQuestion = () => {
|
||||
const refreshQuestion = () => {
|
||||
callback?.('default');
|
||||
};
|
||||
const closeModal = useReportModal(refershQuestion);
|
||||
const closeModal = useReportModal(refreshQuestion);
|
||||
const editUrl =
|
||||
type === 'answer' ? `/posts/${qid}/${aid}/edit` : `/posts/${qid}/edit`;
|
||||
|
||||
|
@ -97,7 +97,7 @@ const Index: FC<IProps> = ({
|
|||
deleteAnswer({
|
||||
id: aid,
|
||||
}).then(() => {
|
||||
// refersh page
|
||||
// refresh page
|
||||
toast.onShow({
|
||||
msg: t('tip_answer_deleted'),
|
||||
variant: 'success',
|
||||
|
@ -132,7 +132,7 @@ const Index: FC<IProps> = ({
|
|||
msg: t('success', { keyPrefix: 'question_detail.reopen' }),
|
||||
variant: 'success',
|
||||
});
|
||||
refershQuestion();
|
||||
refreshQuestion();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
Empty,
|
||||
BaseUserCard,
|
||||
QueryGroup,
|
||||
QuestionListLoader,
|
||||
} from '@/components';
|
||||
import { useQuestionList } from '@/services';
|
||||
|
||||
|
@ -64,69 +65,73 @@ const QuestionList: FC<Props> = ({ source }) => {
|
|||
/>
|
||||
</div>
|
||||
<ListGroup className="rounded-0">
|
||||
{listData?.list?.map((li) => {
|
||||
return (
|
||||
<ListGroup.Item
|
||||
key={li.id}
|
||||
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
|
||||
<h5 className="text-wrap text-break">
|
||||
<NavLink
|
||||
to={pathFactory.questionLanding(li.id, li.url_title)}
|
||||
className="link-dark">
|
||||
{li.title}
|
||||
{li.status === 2 ? ` [${t('closed')}]` : ''}
|
||||
</NavLink>
|
||||
</h5>
|
||||
<div className="d-flex flex-column flex-md-row align-items-md-center fs-14 mb-2 text-secondary">
|
||||
<div className="d-flex">
|
||||
<BaseUserCard
|
||||
data={li.operator}
|
||||
showAvatar={false}
|
||||
className="me-1"
|
||||
/>
|
||||
•
|
||||
<FormatTime
|
||||
time={li.operated_at}
|
||||
className="text-secondary ms-1"
|
||||
preFix={t(li.operation_type)}
|
||||
/>
|
||||
</div>
|
||||
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
|
||||
<span>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
<em className="fst-normal ms-1">{li.vote_count}</em>
|
||||
</span>
|
||||
<span
|
||||
className={`ms-3 ${
|
||||
li.accepted_answer_id >= 1 ? 'text-success' : ''
|
||||
}`}>
|
||||
<Icon
|
||||
name={
|
||||
li.accepted_answer_id >= 1
|
||||
? 'check-circle-fill'
|
||||
: 'chat-square-text-fill'
|
||||
}
|
||||
{isLoading ? (
|
||||
<QuestionListLoader />
|
||||
) : (
|
||||
listData?.list?.map((li) => {
|
||||
return (
|
||||
<ListGroup.Item
|
||||
key={li.id}
|
||||
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
|
||||
<h5 className="text-wrap text-break">
|
||||
<NavLink
|
||||
to={pathFactory.questionLanding(li.id, li.url_title)}
|
||||
className="link-dark">
|
||||
{li.title}
|
||||
{li.status === 2 ? ` [${t('closed')}]` : ''}
|
||||
</NavLink>
|
||||
</h5>
|
||||
<div className="d-flex flex-column flex-md-row align-items-md-center fs-14 mb-2 text-secondary">
|
||||
<div className="d-flex">
|
||||
<BaseUserCard
|
||||
data={li.operator}
|
||||
showAvatar={false}
|
||||
className="me-1"
|
||||
/>
|
||||
<em className="fst-normal ms-1">{li.answer_count}</em>
|
||||
</span>
|
||||
<span className="summary-stat ms-3">
|
||||
<Icon name="eye-fill" />
|
||||
<em className="fst-normal ms-1">{li.view_count}</em>
|
||||
</span>
|
||||
•
|
||||
<FormatTime
|
||||
time={li.operated_at}
|
||||
className="text-secondary ms-1"
|
||||
preFix={t(li.operation_type)}
|
||||
/>
|
||||
</div>
|
||||
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
|
||||
<span>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
<em className="fst-normal ms-1">{li.vote_count}</em>
|
||||
</span>
|
||||
<span
|
||||
className={`ms-3 ${
|
||||
li.accepted_answer_id >= 1 ? 'text-success' : ''
|
||||
}`}>
|
||||
<Icon
|
||||
name={
|
||||
li.accepted_answer_id >= 1
|
||||
? 'check-circle-fill'
|
||||
: 'chat-square-text-fill'
|
||||
}
|
||||
/>
|
||||
<em className="fst-normal ms-1">{li.answer_count}</em>
|
||||
</span>
|
||||
<span className="summary-stat ms-3">
|
||||
<Icon name="eye-fill" />
|
||||
<em className="fst-normal ms-1">{li.view_count}</em>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="question-tags m-n1">
|
||||
{Array.isArray(li.tags)
|
||||
? li.tags.map((tag) => {
|
||||
return (
|
||||
<Tag key={tag.slug_name} className="m-1" data={tag} />
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
);
|
||||
})}
|
||||
<div className="question-tags m-n1">
|
||||
{Array.isArray(li.tags)
|
||||
? li.tags.map((tag) => {
|
||||
return (
|
||||
<Tag key={tag.slug_name} className="m-1" data={tag} />
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ListGroup>
|
||||
{count <= 0 && !isLoading && <Empty />}
|
||||
<div className="mt-4 mb-2 d-flex justify-content-center">
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { ListGroupItem } from 'react-bootstrap';
|
||||
|
||||
interface Props {
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ count = 10 }) => {
|
||||
const list = new Array(count).fill(0).map((v, i) => v + i);
|
||||
return (
|
||||
<>
|
||||
{list.map((v) => (
|
||||
<ListGroupItem
|
||||
className="bg-transparent py-3 px-0 border-start-0 border-end-0 placeholder-glow"
|
||||
key={v}>
|
||||
<div
|
||||
className="placeholder w-100 h5 align-top"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="placeholder w-75 d-block align-top mb-2"
|
||||
style={{ height: '21px' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="placeholder w-50 align-top"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
</ListGroupItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -1,10 +1,4 @@
|
|||
.tag-selector-wrap {
|
||||
.btn {
|
||||
&.warning {
|
||||
-webkit-animation: tag-input-warning 2s;
|
||||
animation: tag-input-warning 2s;
|
||||
}
|
||||
}
|
||||
.dropdown-menu {
|
||||
min-width: 15rem;
|
||||
}
|
||||
|
@ -18,23 +12,5 @@
|
|||
color: #212529;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
@-webkit-keyframes tag-input-warning {
|
||||
0% {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tag-input-warning {
|
||||
0% {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,7 +188,7 @@ const TagSelector: FC<IProps> = ({
|
|||
key={item.slug_name}
|
||||
className={classNames(
|
||||
'm-1 text-nowrap d-flex align-items-center',
|
||||
index === repeatIndex && 'warning',
|
||||
index === repeatIndex && 'bg-fade-out',
|
||||
)}
|
||||
variant={`outline-${
|
||||
item.reserved ? 'danger' : item.recommend ? 'dark' : 'secondary'
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { Col, Card } from 'react-bootstrap';
|
||||
|
||||
interface Props {
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ count = 20 }) => {
|
||||
const list = new Array(count).fill(0).map((v, i) => v + i);
|
||||
return (
|
||||
<>
|
||||
{list.map((v) => (
|
||||
<Col
|
||||
key={v}
|
||||
xs={12}
|
||||
lg={3}
|
||||
md={4}
|
||||
sm={6}
|
||||
className="mb-4 placeholder-glow">
|
||||
<Card className="h-100">
|
||||
<Card.Body className="d-flex flex-column align-items-start">
|
||||
<div
|
||||
className="placeholder align-top w-25 mb-3"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
|
||||
<p
|
||||
className="placeholder fs-14 text-truncate-3 w-100"
|
||||
style={{ height: '42px' }}
|
||||
/>
|
||||
<div className="d-flex align-items-center">
|
||||
<div
|
||||
className="placeholder me-2"
|
||||
style={{ width: '80px', height: '31px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder text-secondary fs-14 text-nowrap"
|
||||
style={{ width: '100px', height: '21px' }}
|
||||
/>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -30,6 +30,8 @@ import DiffContent from './DiffContent';
|
|||
import Customize from './Customize';
|
||||
import CustomizeTheme from './CustomizeTheme';
|
||||
import PageTags from './PageTags';
|
||||
import QuestionListLoader from './QuestionListLoader';
|
||||
import TagsLoader from './TagsLoader';
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
|
@ -66,5 +68,7 @@ export {
|
|||
Customize,
|
||||
CustomizeTheme,
|
||||
PageTags,
|
||||
QuestionListLoader,
|
||||
TagsLoader,
|
||||
};
|
||||
export type { EditorRef, JSONSchema, UISchema };
|
||||
|
|
|
@ -14,6 +14,9 @@ const usePageUsers = () => {
|
|||
getUsers,
|
||||
setUsers: (data: Types.PageUser | Types.PageUser[]) => {
|
||||
if (data instanceof Array) {
|
||||
if (data.length === 0) {
|
||||
return;
|
||||
}
|
||||
setUsers(uniqBy([...users, ...data], 'userName'));
|
||||
globalUsers = uniqBy([...globalUsers, ...data], 'userName');
|
||||
} else {
|
||||
|
|
|
@ -95,13 +95,13 @@ const useTagModal = (props: IProps = {}) => {
|
|||
isInvalid: true,
|
||||
errorMsg: t('form.fields.slug_name.msg.range'),
|
||||
};
|
||||
} else if (/[^a-z0-9+#\-.]/.test(slugName.value)) {
|
||||
bol = false;
|
||||
formData.slugName = {
|
||||
value: slugName.value,
|
||||
isInvalid: true,
|
||||
errorMsg: t('form.fields.slug_name.msg.character'),
|
||||
};
|
||||
// } else if (/[^a-z0-9+#\-.]/.test(slugName.value)) {
|
||||
// bol = false;
|
||||
// formData.slugName = {
|
||||
// value: slugName.value,
|
||||
// isInvalid: true,
|
||||
// errorMsg: t('form.fields.slug_name.msg.character'),
|
||||
// };
|
||||
} else {
|
||||
formData.slugName = {
|
||||
value: slugName.value,
|
||||
|
@ -217,7 +217,9 @@ const useTagModal = (props: IProps = {}) => {
|
|||
isInvalid={formData.slugName.isInvalid}
|
||||
/>
|
||||
|
||||
<Form.Text as="div">{t('form.fields.slug_name.desc')}</Form.Text>
|
||||
<Form.Text as="div">
|
||||
{t('form.fields.slug_name.msg.range')}
|
||||
</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.slugName.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
|
|
|
@ -18,10 +18,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
|
@ -239,6 +235,12 @@ a {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
table {
|
||||
td,
|
||||
th {
|
||||
word-break: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-img-wrap {
|
||||
|
@ -283,3 +285,16 @@ a {
|
|||
.pre-line {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@keyframes bg-fade-out {
|
||||
0%,
|
||||
25% {
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.bg-fade-out {
|
||||
animation: bg-fade-out 2s ease 0.3s;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ const Index: FC = () => {
|
|||
'ui:widget': 'textarea',
|
||||
'ui:options': {
|
||||
rows: 10,
|
||||
className: 'font-monospace',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ const Index: FC<Props> = ({ visible, siteUrl = '' }) => {
|
|||
<p>
|
||||
<Trans i18nKey="install.ready_description">
|
||||
If you ever feel like changing more settings, visit
|
||||
<a href={`${siteUrl}/users/login`}>admin section</a>; find it in the
|
||||
<a href={`${siteUrl}/users/login`}> admin section</a>; find it in the
|
||||
site menu.
|
||||
</Trans>
|
||||
</p>
|
||||
|
|
|
@ -23,7 +23,7 @@ const Index: FC<Props> = ({ visible, errorMsg, nextCallback }) => {
|
|||
<div className="fmt">
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="install.config_yaml.description"
|
||||
i18nKey="install.config_yaml.desc"
|
||||
components={{ 1: <code /> }}
|
||||
/>
|
||||
</p>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { HelmetProvider } from 'react-helmet-async';
|
|||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { toastStore } from '@/stores';
|
||||
import { toastStore, loginToContinueStore } from '@/stores';
|
||||
import {
|
||||
Header,
|
||||
Footer,
|
||||
|
@ -13,13 +13,14 @@ import {
|
|||
CustomizeTheme,
|
||||
PageTags,
|
||||
} from '@/components';
|
||||
import { LoginToContinueModal } from '@/components/Modal';
|
||||
|
||||
const Layout: FC = () => {
|
||||
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
||||
const closeToast = () => {
|
||||
toastClear();
|
||||
};
|
||||
|
||||
const { show: showLoginToContinueModal } = loginToContinueStore();
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<PageTags />
|
||||
|
@ -35,6 +36,7 @@ const Layout: FC = () => {
|
|||
<Toast msg={toastMsg} variant={variant} onClose={closeToast} />
|
||||
<Footer />
|
||||
<Customize />
|
||||
<LoginToContinueModal visible={showLoginToContinueModal} />
|
||||
</SWRConfig>
|
||||
</HelmetProvider>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { memo, FC, useEffect, useRef } from 'react';
|
||||
import { Row, Col, Button } from 'react-bootstrap';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
|
||||
|
@ -12,7 +12,7 @@ import {
|
|||
FormatTime,
|
||||
htmlRender,
|
||||
} from '@/components';
|
||||
import { scrollTop } from '@/utils';
|
||||
import { scrollTop, bgFadeOut } from '@/utils';
|
||||
import { AnswerItem } from '@/common/interface';
|
||||
import { acceptanceAnswer } from '@/services';
|
||||
|
||||
|
@ -54,13 +54,18 @@ const Index: FC<Props> = ({
|
|||
if (!answerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
htmlRender(answerRef.current.querySelector('.fmt'));
|
||||
|
||||
if (aid === data.id) {
|
||||
setTimeout(() => {
|
||||
const element = answerRef.current;
|
||||
scrollTop(element);
|
||||
if (!searchParams.get('commentId')) {
|
||||
bgFadeOut(answerRef.current);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
htmlRender(answerRef.current.querySelector('.fmt'));
|
||||
}, [data.id, answerRef.current]);
|
||||
if (!data?.id) {
|
||||
return null;
|
||||
|
@ -107,8 +112,8 @@ const Index: FC<Props> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<Row className="mt-4 mb-3">
|
||||
<Col className="mb-3 mb-md-0">
|
||||
<div className="d-block d-md-flex flex-wrap mt-4 mb-3">
|
||||
<div className="mb-3 mb-md-0 me-4 flex-grow-1">
|
||||
<Operate
|
||||
qid={data.question_id}
|
||||
aid={data.id}
|
||||
|
@ -119,8 +124,8 @@ const Index: FC<Props> = ({
|
|||
slugTitle={slugTitle}
|
||||
callback={callback}
|
||||
/>
|
||||
</Col>
|
||||
<Col lg={3} className="mb-3 mb-md-0">
|
||||
</div>
|
||||
<div className="mb-3 mb-md-0 me-4" style={{ minWidth: '196px' }}>
|
||||
{data.update_user_info &&
|
||||
data.update_user_info?.username !== data.user_info?.username ? (
|
||||
<UserCard
|
||||
|
@ -145,8 +150,8 @@ const Index: FC<Props> = ({
|
|||
className="text-secondary fs-14"
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
</div>
|
||||
<div style={{ minWidth: '196px' }}>
|
||||
<UserCard
|
||||
data={data?.user_info}
|
||||
time={Number(data.create_time)}
|
||||
|
@ -154,8 +159,8 @@ const Index: FC<Props> = ({
|
|||
isLogged={isLogged}
|
||||
timelinePath={`/posts/${data.question_id}/${data.id}/timeline`}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Comment
|
||||
objectId={data.id}
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
import { FC, memo } from 'react';
|
||||
|
||||
const Index: FC = () => {
|
||||
return (
|
||||
<div className="placeholder-glow">
|
||||
<div className="placeholder w-100 h1 mb-3" style={{ height: '34px' }} />
|
||||
|
||||
<div className="placeholder w-75 mb-3" style={{ height: '21px' }} />
|
||||
|
||||
<div
|
||||
className="placeholder w-50 d-block align-top mb-4"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
|
||||
<div style={{ marginBottom: '2rem' }}>
|
||||
<p>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-75 d-block align-top"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-50 d-block align-top"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="d-flex">
|
||||
<div
|
||||
className="placeholder align-top me-3 rounded"
|
||||
style={{ height: '38px', width: '120px' }}
|
||||
/>
|
||||
<div
|
||||
className="placeholder align-top rounded"
|
||||
style={{ height: '38px', width: '68px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="d-block d-md-flex flex-wrap mt-4 mb-3">
|
||||
<div
|
||||
className="placeholder mb-3 mb-md-0 me-4"
|
||||
style={{ height: '21px', width: '40%' }}
|
||||
/>
|
||||
<div
|
||||
style={{ minWidth: '196px', height: '24px' }}
|
||||
className="placeholder mb-3 me-4 mb-md-0 d-block d-md-none"
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{ minWidth: '196px', height: '24px' }}
|
||||
className="placeholder d-block d-md-none"
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{ minWidth: '196px', height: '40px' }}
|
||||
className="placeholder mb-3 me-4 mb-md-0 d-none d-md-block"
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{ minWidth: '196px', height: '40px' }}
|
||||
className="placeholder d-none d-md-block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{[0, 1, 2].map((item, i) => (
|
||||
<div
|
||||
className={`border-bottom py-2 ${i === 0 ? 'border-top' : ''}`}
|
||||
key={item}>
|
||||
<div className="placeholder w-100 mb-1" style={{ height: '17px' }} />
|
||||
<div className="placeholder w-50" style={{ height: '17px' }} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="d-flex mt-2 mb-4">
|
||||
<div
|
||||
className="placeholder align-top me-4"
|
||||
style={{ height: '21px', width: '140px' }}
|
||||
/>
|
||||
<div
|
||||
className="placeholder align-top"
|
||||
style={{ height: '21px', width: '140px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, FC, useState, useEffect, useRef } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Row, Col, Button } from 'react-bootstrap';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
import {
|
||||
Tag,
|
||||
|
@ -12,7 +12,7 @@ import {
|
|||
FormatTime,
|
||||
htmlRender,
|
||||
} from '@/components';
|
||||
import { formatCount } from '@/utils';
|
||||
import { formatCount, guard } from '@/utils';
|
||||
import { following } from '@/services';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
|
@ -33,6 +33,9 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
|||
|
||||
const handleFollow = (e) => {
|
||||
e.preventDefault();
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
following({
|
||||
object_id: data?.id,
|
||||
is_cancel: followed,
|
||||
|
@ -122,8 +125,8 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
|||
}}
|
||||
/>
|
||||
|
||||
<Row className="mt-4 mb-3">
|
||||
<Col lg={5} className="mb-3 mb-md-0">
|
||||
<div className="d-block d-md-flex flex-wrap mt-4 mb-3">
|
||||
<div className="mb-3 mb-md-0 me-4 flex-grow-1">
|
||||
<Operate
|
||||
qid={data?.id}
|
||||
type="question"
|
||||
|
@ -134,8 +137,8 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
|||
isAccepted={Boolean(data?.accepted_answer_id)}
|
||||
callback={initPage}
|
||||
/>
|
||||
</Col>
|
||||
<Col lg={3} className="mb-3 mb-md-0">
|
||||
</div>
|
||||
<div style={{ minWidth: '196px' }} className="mb-3 me-4 mb-md-0">
|
||||
{data.update_user_info &&
|
||||
data.update_user_info?.username !== data.user_info?.username ? (
|
||||
<UserCard
|
||||
|
@ -160,8 +163,8 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
|||
className="text-secondary fs-14"
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col lg={3}>
|
||||
</div>
|
||||
<div style={{ minWidth: '196px' }}>
|
||||
<UserCard
|
||||
data={data?.user_info}
|
||||
time={data.create_time}
|
||||
|
@ -169,8 +172,8 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
|||
isLogged={isLogged}
|
||||
timelinePath={`/posts/${data.id}/timeline`}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Comment
|
||||
objectId={data?.id}
|
||||
|
|
|
@ -17,11 +17,15 @@ const Index: FC<Props> = ({ id }) => {
|
|||
keyPrefix: 'related_question',
|
||||
});
|
||||
|
||||
const { data } = useSimilarQuestion({
|
||||
const { data, isLoading } = useSimilarQuestion({
|
||||
question_id: id,
|
||||
page_size: 5,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>{t('title')}</Card.Header>
|
||||
|
|
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
|||
import { Editor, Modal, TextArea } from '@/components';
|
||||
import { FormDataType } from '@/common/interface';
|
||||
import { postAnswer } from '@/services';
|
||||
import { guard } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
visible?: boolean;
|
||||
|
@ -31,9 +32,13 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
},
|
||||
});
|
||||
const [showEditor, setShowEditor] = useState<boolean>(visible);
|
||||
const [focusType, setForceType] = useState('');
|
||||
const [focusType, setFocusType] = useState('');
|
||||
const [editorFocusState, setEditorFocusState] = useState(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
if (!formData.content.value) {
|
||||
setFormData({
|
||||
content: {
|
||||
|
@ -62,6 +67,9 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
};
|
||||
|
||||
const clickBtn = () => {
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
if (data?.answered && !showEditor) {
|
||||
Modal.confirm({
|
||||
title: t('confirm_title'),
|
||||
|
@ -81,9 +89,14 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
|
||||
handleSubmit();
|
||||
};
|
||||
const handleFocusForTextArea = () => {
|
||||
setForceType('answer');
|
||||
const handleFocusForTextArea = (evt) => {
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
evt.currentTarget.blur();
|
||||
return;
|
||||
}
|
||||
setFocusType('answer');
|
||||
setShowEditor(true);
|
||||
setEditorFocusState(true);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -114,7 +127,7 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
focusType === 'answer' && 'focus',
|
||||
)}
|
||||
value={formData.content.value}
|
||||
autoFocus
|
||||
autoFocus={editorFocusState}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
content: {
|
||||
|
@ -125,10 +138,10 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
});
|
||||
}}
|
||||
onFocus={() => {
|
||||
setForceType('answer');
|
||||
setFocusType('answer');
|
||||
}}
|
||||
onBlur={() => {
|
||||
setForceType('');
|
||||
setFocusType('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -4,5 +4,14 @@ import AnswerHead from './AnswerHead';
|
|||
import RelatedQuestions from './RelatedQuestions';
|
||||
import WriteAnswer from './WriteAnswer';
|
||||
import Alert from './Alert';
|
||||
import ContentLoader from './ContentLoader';
|
||||
|
||||
export { Question, Answer, AnswerHead, RelatedQuestions, WriteAnswer, Alert };
|
||||
export {
|
||||
Question,
|
||||
Answer,
|
||||
AnswerHead,
|
||||
RelatedQuestions,
|
||||
WriteAnswer,
|
||||
Alert,
|
||||
ContentLoader,
|
||||
};
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
RelatedQuestions,
|
||||
WriteAnswer,
|
||||
Alert,
|
||||
ContentLoader,
|
||||
} from './components';
|
||||
|
||||
import './index.scss';
|
||||
|
@ -35,7 +36,9 @@ const Index = () => {
|
|||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('translation');
|
||||
const { qid = '', slugPermalink = '' } = useParams();
|
||||
// Compatible with Permalink
|
||||
/**
|
||||
* Note: Compatible with Permalink
|
||||
*/
|
||||
let { aid = '' } = useParams();
|
||||
if (!aid && Pattern.isAnswerId.test(slugPermalink)) {
|
||||
aid = slugPermalink;
|
||||
|
@ -45,6 +48,7 @@ const Index = () => {
|
|||
const page = Number(urlSearch.get('page') || 0);
|
||||
const order = urlSearch.get('order') || '';
|
||||
const [question, setQuestion] = useState<QuestionDetailRes | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [answers, setAnswers] = useState<ListResult<AnswerItem>>({
|
||||
count: -1,
|
||||
list: [],
|
||||
|
@ -95,15 +99,35 @@ const Index = () => {
|
|||
};
|
||||
|
||||
const getDetail = async () => {
|
||||
const res = await questionDetail(qid);
|
||||
if (res) {
|
||||
// undo
|
||||
setUsers([
|
||||
res.user_info,
|
||||
res?.update_user_info,
|
||||
res?.last_answered_user_info,
|
||||
]);
|
||||
setQuestion(res);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await questionDetail(qid);
|
||||
if (res) {
|
||||
setUsers([
|
||||
{
|
||||
id: res.user_info.id,
|
||||
displayName: res.user_info.display_name,
|
||||
userName: res.user_info.username,
|
||||
avatar_url: res.user_info.avatar,
|
||||
},
|
||||
{
|
||||
id: res?.update_user_info?.id,
|
||||
displayName: res?.update_user_info?.display_name,
|
||||
userName: res?.update_user_info?.username,
|
||||
avatar_url: res?.update_user_info?.avatar,
|
||||
},
|
||||
{
|
||||
id: res?.last_answered_user_info?.id,
|
||||
displayName: res?.last_answered_user_info?.display_name,
|
||||
userName: res?.last_answered_user_info?.username,
|
||||
avatar_url: res?.last_answered_user_info?.avatar,
|
||||
},
|
||||
]);
|
||||
setQuestion(res);
|
||||
}
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -114,7 +138,6 @@ const Index = () => {
|
|||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'default') {
|
||||
window.scrollTo(0, 0);
|
||||
getDetail();
|
||||
|
@ -141,6 +164,7 @@ const Index = () => {
|
|||
if (!qid) {
|
||||
return;
|
||||
}
|
||||
window.scrollTo(0, 0);
|
||||
getDetail();
|
||||
requestAnswers();
|
||||
}, [qid]);
|
||||
|
@ -162,13 +186,17 @@ const Index = () => {
|
|||
{question?.operation?.operation_type && (
|
||||
<Alert data={question.operation} />
|
||||
)}
|
||||
<Question
|
||||
data={question}
|
||||
initPage={initPage}
|
||||
hasAnswer={answers.count > 0}
|
||||
isLogged={isLogged}
|
||||
/>
|
||||
{answers.count > 0 && (
|
||||
{isLoading ? (
|
||||
<ContentLoader />
|
||||
) : (
|
||||
<Question
|
||||
data={question}
|
||||
initPage={initPage}
|
||||
hasAnswer={answers.count > 0}
|
||||
isLogged={isLogged}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && answers.count > 0 && (
|
||||
<>
|
||||
<AnswerHead count={answers.count} order={order} />
|
||||
{answers?.list?.map((item) => {
|
||||
|
@ -188,7 +216,7 @@ const Index = () => {
|
|||
</>
|
||||
)}
|
||||
|
||||
{Math.ceil(answers.count / 15) > 1 && (
|
||||
{!isLoading && Math.ceil(answers.count / 15) > 1 && (
|
||||
<div className="d-flex justify-content-center answer-item pt-4">
|
||||
<Pagination
|
||||
currentPage={Number(page || 1)}
|
||||
|
@ -198,9 +226,8 @@ const Index = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!question?.operation?.operation_type && (
|
||||
{!isLoading && !question?.operation?.operation_type && (
|
||||
<WriteAnswer
|
||||
visible={answers.count === 0}
|
||||
data={{
|
||||
qid,
|
||||
answered: question?.answered,
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { FC } from 'react';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import { useMatch } from 'react-router-dom';
|
||||
import { useMatch, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { FollowingTags } from '@/components';
|
||||
import QuestionList from '@/components/QuestionList';
|
||||
import HotQuestions from '@/components/HotQuestions';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { siteInfoStore, loggedUserInfoStore } from '@/stores';
|
||||
|
||||
const Questions: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
|
||||
const { user: loggedUser } = loggedUserInfoStore((_) => _);
|
||||
const isIndexPage = useMatch('/');
|
||||
let pageTitle = t('questions', { keyPrefix: 'page_title' });
|
||||
let slogan = '';
|
||||
|
@ -29,7 +29,26 @@ const Questions: FC = () => {
|
|||
<QuestionList source="questions" />
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<FollowingTags />
|
||||
{!loggedUser.access_token && (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">
|
||||
{t('page_title', {
|
||||
keyPrefix: 'login',
|
||||
site_name: siteInfo.name,
|
||||
})}
|
||||
</h5>
|
||||
<p className="card-text">{siteInfo.description}</p>
|
||||
<Link to="/users/login" className="btn btn-primary">
|
||||
{t('login', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
<Link to="/users/register" className="btn btn-link ms-2">
|
||||
{t('signup', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loggedUser.access_token && <FollowingTags />}
|
||||
<HotQuestions />
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { ListGroupItem } from 'react-bootstrap';
|
||||
|
||||
interface Props {
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ count = 10 }) => {
|
||||
const list = new Array(count).fill(0).map((v, i) => v + i);
|
||||
return (
|
||||
<>
|
||||
{list.map((v) => (
|
||||
<ListGroupItem
|
||||
className="py-3 px-0 border-start-0 border-end-0 bg-transparent placeholder-glow"
|
||||
key={v}>
|
||||
<div className="mb-2">
|
||||
<div
|
||||
className="placeholder me-2"
|
||||
style={{ height: '25px', width: '30px' }}
|
||||
/>
|
||||
<div
|
||||
className="h5 mb-0 w-75 placeholder"
|
||||
style={{ height: '25px' }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="placeholder w-50 h5 align-top mb-2"
|
||||
style={{ height: '21px' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="placeholder w-100 d-block align-top mb-2"
|
||||
style={{ height: '42px' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="placeholder w-25 align-top"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
</ListGroupItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -3,5 +3,6 @@ import SearchItem from './SearchItem';
|
|||
import Tips from './Tips';
|
||||
import Empty from './Empty';
|
||||
import SearchHead from './SearchHead';
|
||||
import ListLoader from './ListLoader';
|
||||
|
||||
export { Head, SearchItem, Tips, Empty, SearchHead };
|
||||
export { Head, SearchItem, Tips, Empty, SearchHead, ListLoader };
|
||||
|
|
|
@ -7,7 +7,14 @@ import { usePageTags } from '@/hooks';
|
|||
import { Pagination } from '@/components';
|
||||
import { useSearch } from '@/services';
|
||||
|
||||
import { Head, SearchHead, SearchItem, Tips, Empty } from './components';
|
||||
import {
|
||||
Head,
|
||||
SearchHead,
|
||||
SearchItem,
|
||||
Tips,
|
||||
Empty,
|
||||
ListLoader,
|
||||
} from './components';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation');
|
||||
|
@ -38,9 +45,13 @@ const Index = () => {
|
|||
<Head data={extra} />
|
||||
<SearchHead sort={order} count={count} />
|
||||
<ListGroup className="rounded-0 mb-5">
|
||||
{list?.map((item) => {
|
||||
return <SearchItem key={item.object.id} data={item} />;
|
||||
})}
|
||||
{isLoading ? (
|
||||
<ListLoader />
|
||||
) : (
|
||||
list?.map((item) => {
|
||||
return <SearchItem key={item.object.id} data={item} />;
|
||||
})
|
||||
)}
|
||||
</ListGroup>
|
||||
|
||||
{!isLoading && !list?.length && <Empty />}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { FollowingTags } from '@/components';
|
|||
import { useTagInfo, useFollow, useQuerySynonymsTags } from '@/services';
|
||||
import QuestionList from '@/components/QuestionList';
|
||||
import HotQuestions from '@/components/HotQuestions';
|
||||
import { escapeRemove } from '@/utils';
|
||||
import { escapeRemove, guard } from '@/utils';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
const Questions: FC = () => {
|
||||
|
@ -19,10 +19,13 @@ const Questions: FC = () => {
|
|||
const curTagName = routeParams.tagName || '';
|
||||
const [tagInfo, setTagInfo] = useState<any>({});
|
||||
const [tagFollow, setTagFollow] = useState<Type.FollowParams>();
|
||||
const { data: tagResp } = useTagInfo({ name: curTagName });
|
||||
const { data: tagResp, isLoading } = useTagInfo({ name: curTagName });
|
||||
const { data: followResp } = useFollow(tagFollow);
|
||||
const { data: synonymsRes } = useQuerySynonymsTags(tagInfo?.tag_id);
|
||||
const toggleFollow = () => {
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
setTagFollow({
|
||||
is_cancel: tagInfo.is_follower,
|
||||
object_id: tagInfo.tag_id,
|
||||
|
@ -73,37 +76,52 @@ const Questions: FC = () => {
|
|||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
<Col xxl={7} lg={8} sm={12}>
|
||||
<div className="tag-box mb-5">
|
||||
<h3 className="mb-3">
|
||||
<Link
|
||||
to={pathFactory.tagLanding(tagInfo.slug_name)}
|
||||
replace
|
||||
className="link-dark">
|
||||
{tagInfo.display_name}
|
||||
</Link>
|
||||
</h3>
|
||||
{isLoading ? (
|
||||
<div className="tag-box mb-5 placeholder-glow">
|
||||
<div className="mb-3 h3 placeholder" style={{ width: '120px' }} />
|
||||
<p
|
||||
className="placeholder w-100 d-block align-top"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
|
||||
<p className="text-break">
|
||||
{escapeRemove(tagInfo.excerpt) || t('no_desc')}
|
||||
<Link to={pathFactory.tagInfo(curTagName)} className="ms-1">
|
||||
[{t('more')}]
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div className="box-ft">
|
||||
{tagInfo.is_follower ? (
|
||||
<Button variant="primary" onClick={() => toggleFollow()}>
|
||||
{t('button_following')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={() => toggleFollow()}>
|
||||
{t('button_follow')}
|
||||
</Button>
|
||||
)}
|
||||
<div
|
||||
className="placeholder d-block align-top"
|
||||
style={{ height: '38px', width: '100px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tag-box mb-5">
|
||||
<h3 className="mb-3">
|
||||
<Link
|
||||
to={pathFactory.tagLanding(tagInfo.slug_name)}
|
||||
replace
|
||||
className="link-dark">
|
||||
{tagInfo.display_name}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<p className="text-break">
|
||||
{escapeRemove(tagInfo.excerpt) || t('no_desc')}
|
||||
<Link to={pathFactory.tagInfo(curTagName)} className="ms-1">
|
||||
[{t('more')}]
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div className="box-ft">
|
||||
{tagInfo.is_follower ? (
|
||||
<Button variant="primary" onClick={() => toggleFollow()}>
|
||||
{t('button_following')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={() => toggleFollow()}>
|
||||
{t('button_follow')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<QuestionList source="tag" />
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
|
|
|
@ -4,8 +4,9 @@ import { useSearchParams } from 'react-router-dom';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { Tag, Pagination, QueryGroup } from '@/components';
|
||||
import { Tag, Pagination, QueryGroup, TagsLoader } from '@/components';
|
||||
import { formatCount } from '@/utils';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import { useQueryTags, following } from '@/services';
|
||||
|
||||
const sortBtns = ['popular', 'name', 'newest'];
|
||||
|
@ -19,7 +20,11 @@ const Tags = () => {
|
|||
const sort = urlSearch.get('sort');
|
||||
|
||||
const pageSize = 20;
|
||||
const { data: tags, mutate } = useQueryTags({
|
||||
const {
|
||||
data: tags,
|
||||
mutate,
|
||||
isLoading,
|
||||
} = useQueryTags({
|
||||
page,
|
||||
page_size: pageSize,
|
||||
...(searchTag ? { slug_name: searchTag } : {}),
|
||||
|
@ -31,6 +36,9 @@ const Tags = () => {
|
|||
};
|
||||
|
||||
const handleFollow = (tag) => {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
following({
|
||||
object_id: tag.tag_id,
|
||||
is_cancel: tag.is_follower,
|
||||
|
@ -69,39 +77,43 @@ const Tags = () => {
|
|||
|
||||
<Col className="mt-4" xxl={10} sm={12}>
|
||||
<Row>
|
||||
{tags?.list?.map((tag) => (
|
||||
<Col
|
||||
key={tag.slug_name}
|
||||
xs={12}
|
||||
lg={3}
|
||||
md={4}
|
||||
sm={6}
|
||||
className="mb-4">
|
||||
<Card className="h-100">
|
||||
<Card.Body className="d-flex flex-column align-items-start">
|
||||
<Tag className="mb-3" data={tag} />
|
||||
{isLoading ? (
|
||||
<TagsLoader />
|
||||
) : (
|
||||
tags?.list?.map((tag) => (
|
||||
<Col
|
||||
key={tag.slug_name}
|
||||
xs={12}
|
||||
lg={3}
|
||||
md={4}
|
||||
sm={6}
|
||||
className="mb-4">
|
||||
<Card className="h-100">
|
||||
<Card.Body className="d-flex flex-column align-items-start">
|
||||
<Tag className="mb-3" data={tag} />
|
||||
|
||||
<p className="fs-14 flex-fill text-break text-wrap text-truncate-3">
|
||||
{tag.original_text}
|
||||
</p>
|
||||
<div className="d-flex align-items-center">
|
||||
<Button
|
||||
className={`me-2 ${tag.is_follower ? 'active' : ''}`}
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => handleFollow(tag)}>
|
||||
{tag.is_follower
|
||||
? t('button_following')
|
||||
: t('button_follow')}
|
||||
</Button>
|
||||
<span className="text-secondary fs-14 text-nowrap">
|
||||
{formatCount(tag.question_count)} {t('tag_label')}
|
||||
</span>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
<p className="fs-14 flex-fill text-break text-wrap text-truncate-3">
|
||||
{tag.original_text}
|
||||
</p>
|
||||
<div className="d-flex align-items-center">
|
||||
<Button
|
||||
className={`me-2 ${tag.is_follower ? 'active' : ''}`}
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => handleFollow(tag)}>
|
||||
{tag.is_follower
|
||||
? t('button_following')
|
||||
: t('button_follow')}
|
||||
</Button>
|
||||
<span className="text-secondary fs-14 text-nowrap">
|
||||
{formatCount(tag.question_count)} {t('tag_label')}
|
||||
</span>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
))
|
||||
)}
|
||||
</Row>
|
||||
<div className="d-flex justify-content-center">
|
||||
<Pagination
|
||||
|
|
|
@ -3,6 +3,8 @@ import { Container, Form, Button, Col } from 'react-bootstrap';
|
|||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||
import { usePageTags } from '@/hooks';
|
||||
import type {
|
||||
LoginReqParams,
|
||||
|
@ -10,11 +12,13 @@ import type {
|
|||
FormDataType,
|
||||
} from '@/common/interface';
|
||||
import { Unactivate } from '@/components';
|
||||
import { loggedUserInfoStore, loginSettingStore } from '@/stores';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
loginSettingStore,
|
||||
siteInfoStore,
|
||||
} from '@/stores';
|
||||
import { guard, floppyNavigation, handleFormError } from '@/utils';
|
||||
import { login, checkImgCode } from '@/services';
|
||||
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import { PicAuthCodeModal } from '@/components/Modal';
|
||||
import Storage from '@/utils/storage';
|
||||
|
||||
|
@ -23,8 +27,8 @@ const Index: React.FC = () => {
|
|||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||
const storeUser = loggedUserInfoStore((state) => state.user);
|
||||
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
||||
const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _);
|
||||
const loginSetting = loginSettingStore((state) => state.login);
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
e_mail: {
|
||||
|
@ -91,6 +95,14 @@ const Index: React.FC = () => {
|
|||
return bol;
|
||||
};
|
||||
|
||||
const handleLoginRedirect = () => {
|
||||
const redirect = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
|
||||
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
|
||||
floppyNavigation.navigate(redirect, () => {
|
||||
navigate(redirect, { replace: true });
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogin = (event?: any) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
|
@ -113,24 +125,12 @@ const Index: React.FC = () => {
|
|||
setStep(2);
|
||||
setRefresh((pre) => pre + 1);
|
||||
} else {
|
||||
const path =
|
||||
Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
|
||||
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
|
||||
floppyNavigation.navigate(path, () => {
|
||||
navigate(path, { replace: true });
|
||||
});
|
||||
handleLoginRedirect();
|
||||
}
|
||||
|
||||
setModalState(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
// if (err.isError && err.key) {
|
||||
// formData[err.key].isInvalid = true;
|
||||
// formData[err.key].errorMsg = err.value;
|
||||
// if (err.key.indexOf('captcha') < 0) {
|
||||
// setModalState(false);
|
||||
// }
|
||||
// }
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
|
||||
|
@ -174,7 +174,9 @@ const Index: React.FC = () => {
|
|||
});
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
<h3 className="text-center mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
{step === 1 && (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
|
|
|
@ -4,13 +4,14 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { Unactivate } from '@/components';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
|
||||
import SignUpForm from './components/SignUpForm';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const [showForm, setShowForm] = useState(true);
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
|
||||
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
||||
const onStep = () => {
|
||||
setShowForm((bol) => !bol);
|
||||
};
|
||||
|
@ -19,7 +20,9 @@ const Index: React.FC = () => {
|
|||
});
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
<h3 className="text-center mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
{showForm ? (
|
||||
<SignUpForm callback={onStep} />
|
||||
) : (
|
||||
|
|
|
@ -16,7 +16,7 @@ const Index: FC<{
|
|||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
const callGuards = () => {
|
||||
if (onEnter) {
|
||||
const gr = onEnter();
|
||||
const redirectUrl = gr.redirect;
|
||||
|
@ -26,8 +26,10 @@ const Index: FC<{
|
|||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
callGuards();
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Route Guard */}
|
||||
|
|
|
@ -6,14 +6,12 @@ const tagLanding = (slugName: string) => {
|
|||
if (!slugName) {
|
||||
return '/tags';
|
||||
}
|
||||
slugName = slugName.toLowerCase();
|
||||
return urlcat('/tags/:slugName', { slugName });
|
||||
};
|
||||
const tagInfo = (slugName: string) => {
|
||||
if (!slugName) {
|
||||
return '/tags';
|
||||
}
|
||||
slugName = slugName.toLowerCase();
|
||||
return urlcat('/tags/:slugName/info', { slugName });
|
||||
};
|
||||
const tagEdit = (tagId: string) => {
|
||||
|
|
|
@ -19,12 +19,18 @@ export const useQueryQuestionByTitle = (title) => {
|
|||
};
|
||||
|
||||
export const useQueryTags = (params) => {
|
||||
return useSWR<Type.ListResult>(
|
||||
const { data, error, mutate } = useSWR<Type.ListResult>(
|
||||
`/answer/api/v1/tags/page?${qs.stringify(params, {
|
||||
skipNulls: true,
|
||||
})}`,
|
||||
request.instance.get,
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !data && !error,
|
||||
error,
|
||||
mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export const useQueryRevisions = (object_id: string | undefined) => {
|
||||
|
@ -262,3 +268,8 @@ export const unsubscribe = (code: string) => {
|
|||
const apiUrl = '/answer/api/v1/user/email/notification';
|
||||
return request.put(apiUrl, { code });
|
||||
};
|
||||
|
||||
export const markdownToHtml = (content: string) => {
|
||||
const apiUrl = '/answer/api/v1/post/render';
|
||||
return request.post(apiUrl, { content });
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import brandingStore from './branding';
|
|||
import pageTagStore from './pageTags';
|
||||
import customizeStore from './customize';
|
||||
import themeSettingStore from './themeSetting';
|
||||
import loginToContinueStore from './loginToContinue';
|
||||
|
||||
export {
|
||||
toastStore,
|
||||
|
@ -21,4 +22,5 @@ export {
|
|||
customizeStore,
|
||||
themeSettingStore,
|
||||
seoSettingStore,
|
||||
loginToContinueStore,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import create from 'zustand';
|
||||
|
||||
interface IProps {
|
||||
show: boolean;
|
||||
update: (params: { show: boolean }) => void;
|
||||
}
|
||||
|
||||
const loginToContinueStore = create<IProps>((set) => ({
|
||||
show: false,
|
||||
update: (params) =>
|
||||
set({
|
||||
...params,
|
||||
}),
|
||||
}));
|
||||
|
||||
export default loginToContinueStore;
|
|
@ -1,6 +1,7 @@
|
|||
import create from 'zustand';
|
||||
|
||||
import { AdminSettingsGeneral } from '@/common/interface';
|
||||
import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||
|
||||
interface SiteInfoType {
|
||||
siteInfo: AdminSettingsGeneral;
|
||||
|
@ -9,7 +10,7 @@ interface SiteInfoType {
|
|||
|
||||
const siteInfo = create<SiteInfoType>((set) => ({
|
||||
siteInfo: {
|
||||
name: '',
|
||||
name: DEFAULT_SITE_NAME,
|
||||
description: '',
|
||||
short_description: '',
|
||||
site_url: '',
|
||||
|
@ -19,6 +20,9 @@ const siteInfo = create<SiteInfoType>((set) => ({
|
|||
update: (params) =>
|
||||
set((_) => {
|
||||
const o = { ..._.siteInfo, ...params };
|
||||
if (!o.name) {
|
||||
o.name = DEFAULT_SITE_NAME;
|
||||
}
|
||||
return {
|
||||
siteInfo: o,
|
||||
};
|
||||
|
|
|
@ -36,6 +36,15 @@ function scrollTop(element) {
|
|||
});
|
||||
}
|
||||
|
||||
const bgFadeOut = (el) => {
|
||||
if (el && !el.classList.contains('bg-fade-out')) {
|
||||
el.classList.add('bg-fade-out');
|
||||
setTimeout(() => {
|
||||
el.classList.remove('bg-fade-out');
|
||||
}, 3200);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract user info from markdown
|
||||
* @param markdown string
|
||||
|
@ -209,6 +218,7 @@ export {
|
|||
thousandthDivision,
|
||||
formatCount,
|
||||
scrollTop,
|
||||
bgFadeOut,
|
||||
matchedUsers,
|
||||
parseUserInfo,
|
||||
formatUptime,
|
||||
|
|
|
@ -33,10 +33,13 @@ const navigate = (pathname: string, callback: Function) => {
|
|||
/**
|
||||
* auto navigate to login page with redirect info
|
||||
*/
|
||||
const navigateToLogin = () => {
|
||||
storageLoginRedirect();
|
||||
const navigateToLogin = (callback?: Function) => {
|
||||
navigate(RouteAlias.login, () => {
|
||||
window.location.replace(RouteAlias.login);
|
||||
if (callback) {
|
||||
callback(RouteAlias.login);
|
||||
} else {
|
||||
window.location.replace(RouteAlias.login);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
customizeStore,
|
||||
themeSettingStore,
|
||||
seoSettingStore,
|
||||
loginToContinueStore,
|
||||
} from '@/stores';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import Storage from '@/utils/storage';
|
||||
|
@ -225,7 +226,7 @@ export const tryNormalLogged = (canNavigate: boolean = false) => {
|
|||
// must assert logged state first and return
|
||||
if (!us.isLogged) {
|
||||
if (canNavigate) {
|
||||
floppyNavigation.navigateToLogin();
|
||||
loginToContinueStore.getState().update({ show: true });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import Storage from './storage';
|
|||
import { floppyNavigation } from './floppyNavigation';
|
||||
|
||||
const baseConfig = {
|
||||
baseURL: process.env.REACT_APP_API_URL || '',
|
||||
timeout: 10000,
|
||||
withCredentials: true,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue