mirror of https://gitee.com/answerdev/answer.git
Merge branch 'release/1.0.6' into github-main
This commit is contained in:
commit
da4be341f2
2
Makefile
2
Makefile
|
@ -1,6 +1,6 @@
|
|||
.PHONY: build clean ui
|
||||
|
||||
VERSION=1.0.5
|
||||
VERSION=1.0.6
|
||||
BIN=answer
|
||||
DIR_SRC=./cmd/answer
|
||||
DOCKER_CMD=docker
|
||||
|
|
|
@ -5984,9 +5984,6 @@ const docTemplate = `{
|
|||
"schema.GetOtherUserInfoResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"has": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"info": {
|
||||
"$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp"
|
||||
}
|
||||
|
@ -7688,7 +7685,6 @@ const docTemplate = `{
|
|||
],
|
||||
"properties": {
|
||||
"status": {
|
||||
"description": "user status",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"normal",
|
||||
|
@ -7698,7 +7694,6 @@ const docTemplate = `{
|
|||
]
|
||||
},
|
||||
"user_id": {
|
||||
"description": "user id",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
@ -7994,6 +7989,10 @@ const docTemplate = `{
|
|||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"progress": {
|
||||
"description": "Translation completion percentage",
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
@ -5972,9 +5972,6 @@
|
|||
"schema.GetOtherUserInfoResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"has": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"info": {
|
||||
"$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp"
|
||||
}
|
||||
|
@ -7676,7 +7673,6 @@
|
|||
],
|
||||
"properties": {
|
||||
"status": {
|
||||
"description": "user status",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"normal",
|
||||
|
@ -7686,7 +7682,6 @@
|
|||
]
|
||||
},
|
||||
"user_id": {
|
||||
"description": "user id",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
@ -7982,6 +7977,10 @@
|
|||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"progress": {
|
||||
"description": "Translation completion percentage",
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
@ -488,8 +488,6 @@ definitions:
|
|||
type: object
|
||||
schema.GetOtherUserInfoResp:
|
||||
properties:
|
||||
has:
|
||||
type: boolean
|
||||
info:
|
||||
$ref: '#/definitions/schema.GetOtherUserInfoByUsernameResp'
|
||||
type: object
|
||||
|
@ -1691,7 +1689,6 @@ definitions:
|
|||
schema.UpdateUserStatusReq:
|
||||
properties:
|
||||
status:
|
||||
description: user status
|
||||
enum:
|
||||
- normal
|
||||
- suspended
|
||||
|
@ -1699,7 +1696,6 @@ definitions:
|
|||
- inactive
|
||||
type: string
|
||||
user_id:
|
||||
description: user id
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
|
@ -1911,6 +1907,9 @@ definitions:
|
|||
properties:
|
||||
label:
|
||||
type: string
|
||||
progress:
|
||||
description: Translation completion percentage
|
||||
type: integer
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
|
|
8
go.mod
8
go.mod
|
@ -35,13 +35,13 @@ require (
|
|||
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
|
||||
github.com/swaggo/files v1.0.0
|
||||
github.com/swaggo/gin-swagger v1.5.3
|
||||
github.com/swaggo/swag v1.8.7
|
||||
github.com/swaggo/swag v1.8.10
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
github.com/yuin/goldmark v1.4.13
|
||||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/net v0.2.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.14.2
|
||||
|
@ -121,7 +121,7 @@ require (
|
|||
go.uber.org/zap v1.23.0 // indirect
|
||||
golang.org/x/image v0.1.0 // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/tools v0.2.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
|
|
16
go.sum
16
go.sum
|
@ -668,13 +668,14 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
|
|||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY=
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
|
||||
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
|
||||
github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q=
|
||||
github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI=
|
||||
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||
github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU=
|
||||
github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
|
||||
github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
|
@ -850,8 +851,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -944,11 +945,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
|
@ -35,6 +35,10 @@ backend:
|
|||
other: Email and password do not match.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: Email and password do not match.
|
||||
answer:
|
||||
|
@ -81,6 +85,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: The new password is the same as the previous one.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Question not found.
|
||||
cannot_deleted:
|
||||
|
@ -819,6 +825,7 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -1015,9 +1022,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
|
@ -1407,6 +1416,10 @@ ui:
|
|||
reputation: reputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -1,26 +1,41 @@
|
|||
# all support language
|
||||
language_options:
|
||||
- label: "English(US)"
|
||||
- label: "English"
|
||||
value: "en_US"
|
||||
- label: "Español(ES)"
|
||||
progress: 100
|
||||
- label: "Español"
|
||||
value: "es_ES"
|
||||
- label: "Português(PT)"
|
||||
progress: 0
|
||||
- label: "Português(BR)"
|
||||
value: "pt_BR"
|
||||
progress: 0
|
||||
- label: "Português"
|
||||
value: "pt_PT"
|
||||
- label: "Deutsch(DE)"
|
||||
progress: 0
|
||||
- label: "Deutsch"
|
||||
value: "de_DE"
|
||||
- label: "Français(FR)"
|
||||
progress: 4
|
||||
- label: "Français"
|
||||
value: "fr_FR"
|
||||
- label: "日本語(JA)"
|
||||
progress: 100
|
||||
- label: "日本語"
|
||||
value: "ja_JP"
|
||||
- label: "Italiano(IT)"
|
||||
progress: 0
|
||||
- label: "Italiano"
|
||||
value: "it_IT"
|
||||
- label: "Русский(RU)"
|
||||
progress: 16
|
||||
- label: "Русский"
|
||||
value: "ru_RU"
|
||||
- label: "简体中文(CN)"
|
||||
progress: 13
|
||||
- label: "简体中文"
|
||||
value: "zh_CN"
|
||||
- label: "繁體中文(CN)"
|
||||
progress: 100
|
||||
- label: "繁體中文"
|
||||
value: "zh_TW"
|
||||
- label: "한국어(KO)"
|
||||
progress: 100
|
||||
- label: "한국어"
|
||||
value: "ko_KR"
|
||||
- label: "Tiếng Việt(VI)"
|
||||
progress: 0
|
||||
- label: "Tiếng Việt"
|
||||
value: "vi_VN"
|
||||
progress: 0
|
||||
|
|
1624
i18n/pt_BR.yaml
1624
i18n/pt_BR.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,5 @@
|
|||
#The following fields are used for back-end
|
||||
|
||||
backend:
|
||||
base:
|
||||
success:
|
||||
|
@ -80,6 +81,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: 新密码与之前的设置相同
|
||||
question:
|
||||
already_deleted:
|
||||
other: 该内容已被删除
|
||||
not_found:
|
||||
other: 问题未找到
|
||||
cannot_deleted:
|
||||
|
|
|
@ -21,6 +21,7 @@ const (
|
|||
QuestionCannotDeleted = "error.question.cannot_deleted"
|
||||
QuestionCannotClose = "error.question.cannot_close"
|
||||
QuestionCannotUpdate = "error.question.cannot_update"
|
||||
QuestionAlreadyDeleted = "error.question.already_deleted"
|
||||
AnswerNotFound = "error.answer.not_found"
|
||||
AnswerCannotDeleted = "error.answer.cannot_deleted"
|
||||
AnswerCannotUpdate = "error.answer.cannot_update"
|
||||
|
@ -64,4 +65,6 @@ const (
|
|||
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
|
||||
NotAllowedRegistration = "error.user.not_allowed_registration"
|
||||
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
|
||||
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
|
||||
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
|
||||
)
|
||||
|
|
|
@ -20,6 +20,8 @@ var GlobalTrans i18n.Translator
|
|||
type LangOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
// Translation completion percentage
|
||||
Progress int `json:"progress"`
|
||||
}
|
||||
|
||||
// DefaultLangOption default language option. If user config the language is default, the language option is admin choose.
|
||||
|
@ -94,6 +96,11 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
|
|||
return nil, fmt.Errorf("i18n file parsing failed: %s", err)
|
||||
}
|
||||
LanguageOptions = s.LangOption
|
||||
for _, option := range LanguageOptions {
|
||||
if option.Progress != 100 {
|
||||
option.Label = fmt.Sprintf("%s (%d%%)", option.Label, option.Progress)
|
||||
}
|
||||
}
|
||||
return GlobalTrans, err
|
||||
}
|
||||
|
||||
|
|
|
@ -112,6 +112,18 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) {
|
|||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
|
||||
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.CommentAdd,
|
||||
permission.CommentEdit,
|
||||
permission.CommentDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
req.CanAdd = canList[0]
|
||||
req.CanEdit = canList[1]
|
||||
req.CanDelete = canList[2]
|
||||
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentEdit, req.CommentID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
|
@ -122,8 +134,8 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
err = cc.commentService.UpdateComment(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
resp, err := cc.commentService.UpdateComment(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetCommentWithPage get comment page
|
||||
|
|
|
@ -41,7 +41,6 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) {
|
|||
// @Tags Lang
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/language/options [get]
|
||||
// @Router /answer/admin/api/language/options [get]
|
||||
func (u *LangController) GetAdminLangOptions(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
|
||||
|
|
|
@ -404,22 +404,17 @@ func (tc *TemplateController) UserInfo(ctx *gin.Context) {
|
|||
req := &schema.GetOtherUserInfoByUsernameReq{}
|
||||
req.Username = username
|
||||
userinfo, err := tc.templateRenderController.UserInfo(ctx, req)
|
||||
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
if !userinfo.Has {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username)
|
||||
siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name)
|
||||
tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{
|
||||
"userinfo": userinfo,
|
||||
"bio": template.HTML(userinfo.Info.BioHTML),
|
||||
"bio": template.HTML(userinfo.BioHTML),
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -451,6 +446,7 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI
|
|||
if !ok {
|
||||
data["path"] = ""
|
||||
}
|
||||
ctx.Header("X-Frame-Options", "DENY")
|
||||
ctx.HTML(code, tpl, data)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,6 @@ import (
|
|||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoResp, err error) {
|
||||
func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoByUsernameResp, err error) {
|
||||
return q.userService.GetOtherUserInfoByUsername(ctx, req.Username)
|
||||
}
|
||||
|
|
|
@ -157,7 +157,7 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
|
||||
_, err := uc.userService.RetrievePassWord(ctx, req)
|
||||
err := uc.userService.RetrievePassWord(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
|
@ -203,6 +203,7 @@ func (uc *UserController) UserLogout(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
_ = uc.authService.RemoveUserCacheInfo(ctx, accessToken)
|
||||
_ = uc.authService.RemoveAdminUserCacheInfo(ctx, accessToken)
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ func (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
err := uc.userService.UpdateUserStatus(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
|
|
@ -35,9 +35,11 @@ type Answer struct {
|
|||
|
||||
type AnswerSearch struct {
|
||||
Answer
|
||||
Order string `json:"order_by" ` // default or updated
|
||||
Page int `json:"page" form:"page"` // Query number of pages
|
||||
PageSize int `json:"page_size" form:"page_size"` // Search page size
|
||||
IncludeDeleted bool `json:"include_deleted"`
|
||||
LoginUserID string `json:"login_user_id"`
|
||||
Order string `json:"order_by"` // default or updated
|
||||
Page int `json:"page" form:"page"` // Query number of pages
|
||||
PageSize int `json:"page_size" form:"page_size"` // Search page size
|
||||
}
|
||||
|
||||
type AdminAnswerSearch struct {
|
||||
|
|
|
@ -78,7 +78,7 @@ type InitEnvironmentResp struct {
|
|||
// InitBaseInfoReq init base info request
|
||||
type InitBaseInfoReq struct {
|
||||
Language string `validate:"required,gt=0,lte=30" json:"lang"`
|
||||
SiteName string `validate:"required,gt=0,lte=30" json:"site_name"`
|
||||
SiteName string `validate:"required,sanitizer,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=3,lte=30" json:"name"`
|
||||
|
|
|
@ -206,7 +206,9 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
|
|||
default:
|
||||
session = session.OrderBy("adopted desc,vote_count desc,created_at asc")
|
||||
}
|
||||
session = session.And("status = ?", entity.AnswerStatusAvailable)
|
||||
if !search.IncludeDeleted {
|
||||
session = session.And("status = ? OR user_id = ?", entity.AnswerStatusAvailable, search.LoginUserID)
|
||||
}
|
||||
|
||||
session = session.Limit(search.PageSize, offset)
|
||||
count, err = session.FindAndCount(&rows)
|
||||
|
|
|
@ -68,3 +68,11 @@ func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha stri
|
|||
}
|
||||
return captcha, nil
|
||||
}
|
||||
|
||||
func (cr *captchaRepo) DelCaptcha(ctx context.Context, key string) (err error) {
|
||||
err = cr.data.Cache.Del(ctx, key)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -106,6 +106,7 @@ func (a *UIRouter) Register(r *gin.Engine) {
|
|||
default:
|
||||
filePath = UIIndexFilePath
|
||||
c.Header("content-type", "text/html;charset=utf-8")
|
||||
c.Header("X-Frame-Options", "DENY")
|
||||
}
|
||||
file, err := ui.Build.ReadFile(filePath)
|
||||
if err != nil {
|
||||
|
|
|
@ -83,6 +83,7 @@ type AnswerInfo struct {
|
|||
VoteStatus string `json:"vote_status"`
|
||||
VoteCount int `json:"vote_count"`
|
||||
QuestionInfo *QuestionInfo `json:"question_info,omitempty"`
|
||||
Status int `json:"status"`
|
||||
|
||||
// MemberActions
|
||||
MemberActions []*PermissionMemberAction `json:"member_actions"`
|
||||
|
|
|
@ -2,10 +2,9 @@ package schema
|
|||
|
||||
// UpdateUserStatusReq update user request
|
||||
type UpdateUserStatusReq struct {
|
||||
// user id
|
||||
UserID string `validate:"required" json:"user_id"`
|
||||
// user status
|
||||
Status string `validate:"required,oneof=normal suspended deleted inactive" json:"status" enums:"normal,suspended,deleted,inactive"`
|
||||
UserID string `validate:"required" json:"user_id"`
|
||||
Status string `validate:"required,oneof=normal suspended deleted inactive" json:"status" enums:"normal,suspended,deleted,inactive"`
|
||||
LoginUserID string `json:"-"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
|
@ -53,6 +53,12 @@ type UpdateCommentReq struct {
|
|||
// user id
|
||||
UserID string `json:"-"`
|
||||
IsAdmin bool `json:"-"`
|
||||
|
||||
CanAdd bool `json:"-"`
|
||||
// whether user can edit it
|
||||
CanEdit bool `json:"-"`
|
||||
// whether user can delete it
|
||||
CanDelete bool `json:"-"`
|
||||
}
|
||||
|
||||
func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
|
|
|
@ -176,11 +176,20 @@ type AdminQuestionInfo struct {
|
|||
UserInfo *UserBasicInfo `json:"user_info"`
|
||||
}
|
||||
|
||||
type OperationLevel string
|
||||
|
||||
const (
|
||||
OperationLevelInfo OperationLevel = "info"
|
||||
OperationLevelDanger OperationLevel = "danger"
|
||||
OperationLevelWarning OperationLevel = "warning"
|
||||
)
|
||||
|
||||
type Operation struct {
|
||||
OperationType string `json:"operation_type"`
|
||||
OperationDescription string `json:"operation_description"`
|
||||
OperationMsg string `json:"operation_msg"`
|
||||
OperationTime int64 `json:"operation_time"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Msg string `json:"msg"`
|
||||
Time int64 `json:"time"`
|
||||
Level OperationLevel `json:"level"`
|
||||
}
|
||||
|
||||
type GetCloseTypeResp struct {
|
||||
|
|
|
@ -90,7 +90,7 @@ type GetTagResp struct {
|
|||
}
|
||||
|
||||
func (tr *GetTagResp) GetExcerpt() {
|
||||
excerpt := strings.TrimSpace(tr.OriginalText)
|
||||
excerpt := strings.TrimSpace(tr.ParsedText)
|
||||
idx := strings.Index(excerpt, "\n")
|
||||
if idx >= 0 {
|
||||
excerpt = excerpt[0:idx]
|
||||
|
|
|
@ -309,7 +309,7 @@ func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, er
|
|||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
}
|
||||
req.BioHTML = converter.Markdown2HTML(req.Bio)
|
||||
req.BioHTML = converter.Markdown2BasicHTML(req.Bio)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -386,7 +386,6 @@ type GetOtherUserInfoByUsernameReq struct {
|
|||
|
||||
type GetOtherUserInfoResp struct {
|
||||
Info *GetOtherUserInfoByUsernameResp `json:"info"`
|
||||
Has bool `json:"has"`
|
||||
}
|
||||
|
||||
type UserChangeEmailSendCodeReq struct {
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
type CaptchaRepo interface {
|
||||
SetCaptcha(ctx context.Context, key, captcha string) (err error)
|
||||
GetCaptcha(ctx context.Context, key string) (captcha string, err error)
|
||||
DelCaptcha(ctx context.Context, key string) (err error)
|
||||
SetActionType(ctx context.Context, ip, actionType string, amount int) (err error)
|
||||
GetActionType(ctx context.Context, ip, actionType string) (amount int, err error)
|
||||
DelActionType(ctx context.Context, ip, actionType string) (err error)
|
||||
|
@ -143,6 +144,12 @@ func (cs *CaptchaService) GenerateCaptcha(ctx context.Context) (key, captchaBase
|
|||
func (cs *CaptchaService) VerifyCaptcha(ctx context.Context, key, captcha string) (isCorrect bool, err error) {
|
||||
realCaptcha, err := cs.captchaRepo.GetCaptcha(ctx, key)
|
||||
if err != nil {
|
||||
log.Error("VerifyCaptcha GetCaptcha Error", err.Error())
|
||||
return false, nil
|
||||
}
|
||||
err = cs.captchaRepo.DelCaptcha(ctx, key)
|
||||
if err != nil {
|
||||
log.Error("VerifyCaptcha DelCaptcha Error", err.Error())
|
||||
return false, nil
|
||||
}
|
||||
return strings.TrimSpace(captcha) == realCaptcha, nil
|
||||
|
|
|
@ -73,6 +73,7 @@ func (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *sc
|
|||
}
|
||||
info.UserID = data.UserID
|
||||
info.UpdateUserID = data.LastEditUserID
|
||||
info.Status = data.Status
|
||||
return &info
|
||||
}
|
||||
|
||||
|
|
|
@ -164,7 +164,7 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
|
|||
if err != nil {
|
||||
log.Error("UpdateLastAnswer error", err.Error())
|
||||
}
|
||||
err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID)
|
||||
err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID)
|
||||
if err != nil {
|
||||
return insertData.ID, err
|
||||
}
|
||||
|
@ -232,6 +232,11 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq
|
|||
return "", nil
|
||||
}
|
||||
|
||||
if answerInfo.Status == entity.AnswerStatusDeleted {
|
||||
err = errors.BadRequest(reason.AnswerCannotUpdate)
|
||||
return "", err
|
||||
}
|
||||
|
||||
//If the content is the same, ignore it
|
||||
if answerInfo.OriginalText == req.Content {
|
||||
return "", nil
|
||||
|
@ -268,7 +273,7 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq
|
|||
if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID)
|
||||
err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID)
|
||||
if err != nil {
|
||||
return insertData.ID, err
|
||||
}
|
||||
|
@ -473,6 +478,8 @@ func (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListR
|
|||
dbSearch.Page = req.Page
|
||||
dbSearch.PageSize = req.PageSize
|
||||
dbSearch.Order = req.Order
|
||||
dbSearch.IncludeDeleted = req.CanDelete
|
||||
dbSearch.LoginUserID = req.UserID
|
||||
answerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch)
|
||||
if err != nil {
|
||||
return list, count, err
|
||||
|
|
|
@ -209,24 +209,40 @@ func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveC
|
|||
}
|
||||
|
||||
// UpdateComment update comment
|
||||
func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (err error) {
|
||||
func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (
|
||||
resp *schema.GetCommentResp, err error) {
|
||||
resp = &schema.GetCommentResp{}
|
||||
|
||||
old, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.CommentNotFound)
|
||||
return resp, errors.BadRequest(reason.CommentNotFound)
|
||||
}
|
||||
|
||||
// user can edit the comment that was posted by himself before deadline.
|
||||
if !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) {
|
||||
return errors.BadRequest(reason.CommentCannotEditAfterDeadline)
|
||||
return resp, errors.BadRequest(reason.CommentCannotEditAfterDeadline)
|
||||
}
|
||||
|
||||
comment := &entity.Comment{}
|
||||
_ = copier.Copy(comment, req)
|
||||
comment.ID = req.CommentID
|
||||
return cs.commentRepo.UpdateComment(ctx, comment)
|
||||
resp.SetFromComment(comment)
|
||||
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
|
||||
time.Now(), req.CanEdit, req.CanDelete)
|
||||
userInfo, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
resp.Username = userInfo.Username
|
||||
resp.UserDisplayName = userInfo.DisplayName
|
||||
resp.UserAvatar = userInfo.Avatar
|
||||
resp.UserStatus = userInfo.Status
|
||||
}
|
||||
return resp, cs.commentRepo.UpdateComment(ctx, comment)
|
||||
}
|
||||
|
||||
// GetComment get comment one
|
||||
|
|
|
@ -89,6 +89,7 @@ func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.Das
|
|||
}
|
||||
startTime := time.Now().Unix() - schema.AppStartTime.Unix()
|
||||
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
|
||||
dashboardInfo.VersionInfo.Version = constant.Version
|
||||
return dashboardInfo, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ func NewQuestionCommon(questionRepo QuestionRepo,
|
|||
}
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) UpdataPv(ctx context.Context, questionID string) error {
|
||||
func (qs *QuestionCommon) UpdatePv(ctx context.Context, questionID string) error {
|
||||
return qs.questionRepo.UpdatePvCount(ctx, questionID)
|
||||
}
|
||||
|
||||
|
@ -112,14 +112,14 @@ func (qs *QuestionCommon) UpdateLastAnswer(ctx context.Context, questionID, Answ
|
|||
return qs.questionRepo.UpdateLastAnswer(ctx, question)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) UpdataPostTime(ctx context.Context, questionID string) error {
|
||||
func (qs *QuestionCommon) UpdatePostTime(ctx context.Context, questionID string) error {
|
||||
questioninfo := &entity.Question{}
|
||||
now := time.Now()
|
||||
questioninfo.ID = questionID
|
||||
questioninfo.PostUpdateTime = now
|
||||
return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"})
|
||||
}
|
||||
func (qs *QuestionCommon) UpdataPostSetTime(ctx context.Context, questionID string, setTime time.Time) error {
|
||||
func (qs *QuestionCommon) UpdatePostSetTime(ctx context.Context, questionID string, setTime time.Time) error {
|
||||
questioninfo := &entity.Question{}
|
||||
questioninfo.ID = questionID
|
||||
questioninfo.PostUpdateTime = setTime
|
||||
|
@ -148,7 +148,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
|
|||
return showinfo, err
|
||||
}
|
||||
if !has {
|
||||
return showinfo, errors.BadRequest(reason.QuestionNotFound)
|
||||
return showinfo, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
showinfo = qs.ShowFormat(ctx, dbinfo)
|
||||
|
||||
|
@ -170,10 +170,11 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
|
|||
log.Error("json.Unmarshal QuestionCloseJson error", err.Error())
|
||||
} else {
|
||||
operation := &schema.Operation{}
|
||||
operation.OperationType = closeinfo.Name
|
||||
operation.OperationDescription = closeinfo.Description
|
||||
operation.OperationMsg = closemsg.CloseMsg
|
||||
operation.OperationTime = metainfo.CreatedAt.Unix()
|
||||
operation.Type = closeinfo.Name
|
||||
operation.Description = closeinfo.Description
|
||||
operation.Msg = closemsg.CloseMsg
|
||||
operation.Time = metainfo.CreatedAt.Unix()
|
||||
operation.Level = schema.OperationLevelInfo
|
||||
showinfo.Operation = operation
|
||||
}
|
||||
|
||||
|
|
|
@ -470,6 +470,10 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
|
|||
if !has {
|
||||
return
|
||||
}
|
||||
if dbinfo.Status == entity.QuestionStatusDeleted {
|
||||
err = errors.BadRequest(reason.QuestionCannotUpdate)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
question := &entity.Question{}
|
||||
|
@ -614,12 +618,23 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
// If the question is deleted, only the administrator and the author can view it
|
||||
if question.Status == entity.QuestionStatusDeleted && !per.CanReopen && question.UserID != userID {
|
||||
return nil, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
if question.Status != entity.QuestionStatusClosed {
|
||||
per.CanReopen = false
|
||||
}
|
||||
if question.Status == entity.QuestionStatusClosed {
|
||||
per.CanClose = false
|
||||
}
|
||||
if question.Status == entity.QuestionStatusDeleted {
|
||||
operation := &schema.Operation{}
|
||||
operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionAlreadyDeleted)
|
||||
operation.Level = schema.OperationLevelDanger
|
||||
question.Operation = operation
|
||||
}
|
||||
|
||||
question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240)
|
||||
question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID,
|
||||
per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen)
|
||||
|
@ -630,7 +645,7 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
|
|||
func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, loginUserID string,
|
||||
per schema.QuestionPermission) (
|
||||
resp *schema.QuestionInfo, err error) {
|
||||
err = qs.questioncommon.UpdataPv(ctx, questionID)
|
||||
err = qs.questioncommon.UpdatePv(ctx, questionID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
|
|
@ -191,7 +191,7 @@ func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem
|
|||
if saveerr != nil {
|
||||
return saveerr
|
||||
}
|
||||
saveerr = rs.questionCommon.UpdataPostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime)
|
||||
saveerr = rs.questionCommon.UpdatePostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime)
|
||||
if saveerr != nil {
|
||||
return saveerr
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
|
|||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.TagNotFound)
|
||||
return nil, errors.NotFound(reason.TagNotFound)
|
||||
}
|
||||
|
||||
resp = &schema.GetTagResp{}
|
||||
|
@ -113,7 +113,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
|
|||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.TagNotFound)
|
||||
return nil, errors.NotFound(reason.TagNotFound)
|
||||
}
|
||||
resp.MainTagSlugName = tagInfo.SlugName
|
||||
}
|
||||
|
|
|
@ -61,6 +61,10 @@ func NewUserAdminService(
|
|||
|
||||
// UpdateUserStatus update user
|
||||
func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.UpdateUserStatusReq) (err error) {
|
||||
// Admin cannot modify their status
|
||||
if req.UserID == req.LoginUserID {
|
||||
return errors.BadRequest(reason.AdminCannotModifySelfStatus)
|
||||
}
|
||||
userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -153,6 +157,10 @@ func (us *UserAdminService) AddUser(ctx context.Context, req *schema.AddUserReq)
|
|||
|
||||
// UpdateUserPassword update user password
|
||||
func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.UpdateUserPasswordReq) (err error) {
|
||||
// Users cannot modify their password
|
||||
if req.UserID == req.LoginUserID {
|
||||
return errors.BadRequest(reason.AdminCannotUpdateTheirPassword)
|
||||
}
|
||||
userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -86,19 +86,17 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
|
|||
}
|
||||
|
||||
func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) (
|
||||
resp *schema.GetOtherUserInfoResp, err error,
|
||||
resp *schema.GetOtherUserInfoByUsernameResp, err error,
|
||||
) {
|
||||
userInfo, exist, err := us.userRepo.GetByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp = &schema.GetOtherUserInfoResp{}
|
||||
if !exist {
|
||||
return resp, nil
|
||||
return nil, errors.NotFound(reason.UserNotFound)
|
||||
}
|
||||
resp.Has = true
|
||||
resp.Info = &schema.GetOtherUserInfoByUsernameResp{}
|
||||
resp.Info.GetFromUserEntity(userInfo)
|
||||
resp = &schema.GetOtherUserInfoByUsernameResp{}
|
||||
resp.GetFromUserEntity(userInfo)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
@ -149,13 +147,13 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
|
|||
}
|
||||
|
||||
// RetrievePassWord .
|
||||
func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRetrievePassWordRequest) (string, error) {
|
||||
func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRetrievePassWordRequest) error {
|
||||
userInfo, has, err := us.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return "", errors.BadRequest(reason.UserNotFound)
|
||||
return nil
|
||||
}
|
||||
|
||||
// send email
|
||||
|
@ -167,10 +165,10 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
|
|||
verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.getSiteUrl(ctx), code)
|
||||
title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString())
|
||||
return code, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// UseRePassword
|
||||
|
|
|
@ -35,6 +35,17 @@ func Markdown2HTML(source string) string {
|
|||
return buf.String()
|
||||
}
|
||||
|
||||
// Markdown2BasicHTML convert markdown to html ,Only basic syntax can be used
|
||||
func Markdown2BasicHTML(source string) string {
|
||||
content := Markdown2HTML(source)
|
||||
filter := bluemonday.NewPolicy()
|
||||
filter.AllowElements("p", "b", "br")
|
||||
filter.AllowAttrs("src").OnElements("img")
|
||||
filter.AddSpaceWhenStrippingTag(true)
|
||||
content = filter.Sanitize(content)
|
||||
return content
|
||||
}
|
||||
|
||||
type DangerousHTMLFilterExtension struct {
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,21 @@ export const LOGGED_USER_STORAGE_KEY = '_a_lui_';
|
|||
export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_';
|
||||
export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_';
|
||||
export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
|
||||
export const DRAFT_QUESTION_STORAGE_KEY = '_a_dq_';
|
||||
export const DRAFT_ANSWER_STORAGE_KEY = '_a_da_';
|
||||
export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|';
|
||||
|
||||
export const IGNORE_PATH_LIST = [
|
||||
'/users/login',
|
||||
'/users/register',
|
||||
'/users/account-recovery',
|
||||
'/users/change-email',
|
||||
'/users/password-reset',
|
||||
'/users/account-activation',
|
||||
'/users/account-activation/success',
|
||||
'/users/account-activation/failed',
|
||||
'/users/confirm-new-email',
|
||||
];
|
||||
|
||||
export const ADMIN_LIST_STATUS = {
|
||||
// normal;
|
||||
|
|
|
@ -5,7 +5,6 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import classNames from 'classnames';
|
||||
import { unionBy } from 'lodash';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import * as Types from '@/common/interface';
|
||||
import { Modal } from '@/components';
|
||||
|
@ -108,15 +107,11 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
const users = matchedUsers(item.value);
|
||||
const userNames = unionBy(users.map((user) => user.userName));
|
||||
const commentMarkDown = parseUserInfo(item.value);
|
||||
const html = marked.parse(commentMarkDown);
|
||||
// if (!commentMarkDown || !html) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
const params = {
|
||||
object_id: objectId,
|
||||
original_text: commentMarkDown,
|
||||
mention_username_list: userNames,
|
||||
parsed_text: html,
|
||||
...(item.type === 'reply'
|
||||
? {
|
||||
reply_comment_id: item.comment_id,
|
||||
|
@ -128,13 +123,13 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
return updateComment({
|
||||
...params,
|
||||
comment_id: item.comment_id,
|
||||
}).then(() => {
|
||||
}).then((res) => {
|
||||
setComments(
|
||||
comments.map((comment) => {
|
||||
if (comment.comment_id === item.comment_id) {
|
||||
comment.showEdit = false;
|
||||
comment.parsed_text = html;
|
||||
comment.original_text = item.value;
|
||||
comment.parsed_text = res.parsed_text;
|
||||
comment.original_text = res.original_text;
|
||||
}
|
||||
return comment;
|
||||
}),
|
||||
|
|
|
@ -69,7 +69,10 @@ const Index: FC<Props> = ({
|
|||
{objectType !== 'answer' && opts?.showTitle && (
|
||||
<h5
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: diffText(newData.title, oldData?.title),
|
||||
__html: diffText(
|
||||
newData.title?.replace(/</gi, '<'),
|
||||
oldData?.title?.replace(/</gi, '<'),
|
||||
),
|
||||
}}
|
||||
className="mb-3"
|
||||
/>
|
||||
|
|
|
@ -114,19 +114,8 @@ 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);
|
||||
});
|
||||
// remove change table style to htmlToReact function
|
||||
/**
|
||||
* @description: You modify the DOM with other scripts after React has rendered the DOM. This way, on the next render cycle (re-render), React cannot find the DOM node it rendered before, because it has been modified or removed by other scripts.
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -71,6 +71,10 @@ const Header: FC = () => {
|
|||
};
|
||||
const onLoginClick = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (location.pathname === '/users/login') {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
floppyNavigation.navigateToLogin((loginPath) => {
|
||||
navigate(loginPath, { replace: true });
|
||||
});
|
||||
|
|
|
@ -206,7 +206,6 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
const errors = requiredValidator();
|
||||
if (errors.length > 0) {
|
||||
formData = errors.reduce((acc, cur) => {
|
||||
console.log('schema.properties[cur]', cur);
|
||||
acc[cur] = {
|
||||
...formData[cur],
|
||||
isInvalid: true,
|
||||
|
|
|
@ -9,6 +9,7 @@ import useUserModal from './useUserModal';
|
|||
import useChangePasswordModal from './useChangePasswordModal';
|
||||
import usePageTags from './usePageTags';
|
||||
import usePromptWithUnload from './usePrompt';
|
||||
import useImgViewer from './useImgViewer';
|
||||
|
||||
export {
|
||||
useTagModal,
|
||||
|
@ -22,4 +23,5 @@ export {
|
|||
useChangePasswordModal,
|
||||
usePageTags,
|
||||
usePromptWithUnload,
|
||||
useImgViewer,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import { useLayoutEffect, useState, MouseEvent, useEffect } from 'react';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
const div = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(div);
|
||||
|
||||
const useImgViewer = () => {
|
||||
const location = useLocation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [imgSrc, setImgSrc] = useState('');
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
setImgSrc('');
|
||||
};
|
||||
|
||||
const checkIfInLink = (target) => {
|
||||
let ret = false;
|
||||
let el = target.parentElement;
|
||||
while (el) {
|
||||
if (el.nodeName.toLowerCase() === 'a') {
|
||||
ret = true;
|
||||
break;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
const checkClickForImgView = (evt: MouseEvent<HTMLElement>) => {
|
||||
const { target } = evt;
|
||||
// @ts-ignore
|
||||
if (target.nodeName.toLowerCase() !== 'img') {
|
||||
return;
|
||||
}
|
||||
const img = target as HTMLImageElement;
|
||||
if (!img.naturalWidth || !img.naturalHeight) {
|
||||
img.classList.add('broken');
|
||||
return;
|
||||
}
|
||||
const src = img.currentSrc || img.src;
|
||||
if (src && checkIfInLink(img) === false) {
|
||||
setImgSrc(src);
|
||||
setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
root.render(
|
||||
<Modal
|
||||
show={visible}
|
||||
fullscreen
|
||||
centered
|
||||
scrollable
|
||||
contentClassName="bg-transparent"
|
||||
onHide={onClose}>
|
||||
<Modal.Body onClick={onClose} className="p-0 d-flex">
|
||||
<img
|
||||
className="cursor-zoom-out img-fluid m-auto"
|
||||
src={imgSrc}
|
||||
alt={imgSrc}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>,
|
||||
);
|
||||
});
|
||||
useEffect(() => {
|
||||
onClose();
|
||||
}, [location]);
|
||||
return {
|
||||
onClose,
|
||||
checkClickForImgView,
|
||||
};
|
||||
};
|
||||
|
||||
export default useImgViewer;
|
|
@ -6,7 +6,7 @@ import {
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// https://gist.github.com/chaance/2f3c14ec2351a175024f62fd6ba64aa6
|
||||
// The link above is an example of implementing usePromt with useBlocer.
|
||||
// The link above is an example of implementing usePrompt with useBlocker.
|
||||
interface PromptProps {
|
||||
when: boolean;
|
||||
beforeUnload?: boolean;
|
||||
|
|
|
@ -47,7 +47,11 @@ const useReportModal = (callback?: () => void) => {
|
|||
setShow(true);
|
||||
});
|
||||
};
|
||||
|
||||
const asyncCallback = () => {
|
||||
setTimeout(() => {
|
||||
callback?.();
|
||||
});
|
||||
};
|
||||
const handleRadio = (val) => {
|
||||
setInvalidState(false);
|
||||
setContent({
|
||||
|
@ -93,8 +97,8 @@ const useReportModal = (callback?: () => void) => {
|
|||
close_type: reportType.type,
|
||||
close_msg: content.value,
|
||||
}).then(() => {
|
||||
callback?.();
|
||||
onClose();
|
||||
asyncCallback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -109,8 +113,8 @@ const useReportModal = (callback?: () => void) => {
|
|||
msg: t('flag_success', { keyPrefix: 'toast' }),
|
||||
variant: 'warning',
|
||||
});
|
||||
callback?.();
|
||||
onClose();
|
||||
asyncCallback();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -121,8 +125,8 @@ const useReportModal = (callback?: () => void) => {
|
|||
flagged_type: reportType.type,
|
||||
id: params.id,
|
||||
}).then(() => {
|
||||
callback?.();
|
||||
onClose();
|
||||
asyncCallback();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -120,6 +120,14 @@ a {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cursor-zoom-out {
|
||||
cursor: zoom-out !important;
|
||||
}
|
||||
|
||||
img:not(a img, img.broken) {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.resize-none {
|
||||
resize: none;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,30 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Container, Button } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_404' });
|
||||
useEffect(() => {
|
||||
// auto height of container
|
||||
const pageWrap = document.querySelector('.page-wrap');
|
||||
pageWrap.style.display = 'contents';
|
||||
|
||||
return () => {
|
||||
pageWrap.style.display = 'block';
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<Container className="d-flex flex-column justify-content-center align-items-center page-wrap">
|
||||
<Container
|
||||
className="d-flex flex-column justify-content-center align-items-center"
|
||||
style={{ flex: 1 }}>
|
||||
<div
|
||||
className="mb-4 text-secondary"
|
||||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
(=‘x‘=)
|
||||
</div>
|
||||
<div className="text-center mb-4">{t('desc')}</div>
|
||||
<h4 className="text-center">{t('http_error')}</h4>
|
||||
<div className="text-center mb-3 fs-5">{t('desc')}</div>
|
||||
<div className="text-center">
|
||||
<Button as={Link} to="/" variant="link">
|
||||
{t('back_home')}
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Container, Button } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_50X' });
|
||||
useEffect(() => {
|
||||
// auto height of container
|
||||
const pageWrap = document.querySelector('.page-wrap');
|
||||
pageWrap.style.display = 'contents';
|
||||
|
||||
return () => {
|
||||
pageWrap.style.display = 'block';
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<Container className="d-flex flex-column justify-content-center align-items-center page-wrap">
|
||||
<div
|
||||
|
@ -11,7 +21,9 @@ const Index = () => {
|
|||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
(=T^T=)
|
||||
</div>
|
||||
<div className="text-center mb-3">{t('desc')}</div>
|
||||
|
||||
<h4 className="text-center">{t('http_error')}</h4>
|
||||
<div className="text-center mb-3 fs-5">{t('desc')}</div>
|
||||
<div className="text-center">
|
||||
<Button as={Link} to="/" variant="link">
|
||||
{t('back_home')}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Form, Button } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
import Patern from '@/common/patern';
|
||||
import Progress from '../Progress';
|
||||
|
||||
interface Props {
|
||||
|
@ -54,8 +55,7 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
|
|||
};
|
||||
}
|
||||
|
||||
const mailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
|
||||
if (contact_email.value && !contact_email.value.match(mailReg)) {
|
||||
if (contact_email.value && !contact_email.value.match(Patern.mail)) {
|
||||
bol = false;
|
||||
data.contact_email = {
|
||||
value: contact_email.value,
|
||||
|
@ -91,7 +91,7 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
|
|||
};
|
||||
}
|
||||
|
||||
if (email.value && !email.value.match(mailReg)) {
|
||||
if (email.value && !email.value.match(Patern.mail)) {
|
||||
bol = false;
|
||||
data.email = {
|
||||
value: email.value,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { FC, memo, useEffect } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { toastStore, loginToContinueStore } from '@/stores';
|
||||
import { toastStore, loginToContinueStore, notFoundStore } from '@/stores';
|
||||
import {
|
||||
Header,
|
||||
Footer,
|
||||
|
@ -14,13 +14,23 @@ import {
|
|||
PageTags,
|
||||
} from '@/components';
|
||||
import { LoginToContinueModal } from '@/components/Modal';
|
||||
import { useImgViewer } from '@/hooks';
|
||||
import Component404 from '@/pages/404';
|
||||
|
||||
const Layout: FC = () => {
|
||||
const location = useLocation();
|
||||
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
||||
const closeToast = () => {
|
||||
toastClear();
|
||||
};
|
||||
const { visible: show404, hide: notFoundHide } = notFoundStore();
|
||||
|
||||
const imgViewer = useImgViewer();
|
||||
const { show: showLoginToContinueModal } = loginToContinueStore();
|
||||
|
||||
useEffect(() => {
|
||||
notFoundHide();
|
||||
}, [location]);
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<PageTags />
|
||||
|
@ -30,8 +40,11 @@ const Layout: FC = () => {
|
|||
revalidateOnFocus: false,
|
||||
}}>
|
||||
<Header />
|
||||
<div className="position-relative page-wrap">
|
||||
<Outlet />
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||
<div
|
||||
className="position-relative page-wrap"
|
||||
onClick={imgViewer.checkClickForImgView}>
|
||||
{show404 ? <Component404 /> : <Outlet />}
|
||||
</div>
|
||||
<Toast msg={toastMsg} variant={variant} onClose={closeToast} />
|
||||
<Footer />
|
||||
|
|
|
@ -10,6 +10,7 @@ import { isEqual } from 'lodash';
|
|||
import { usePageTags, usePromptWithUnload } from '@/hooks';
|
||||
import { Editor, EditorRef, TagSelector } from '@/components';
|
||||
import type * as Type from '@/common/interface';
|
||||
import { DRAFT_QUESTION_STORAGE_KEY } from '@/common/constants';
|
||||
import {
|
||||
saveQuestion,
|
||||
questionDetail,
|
||||
|
@ -19,7 +20,7 @@ import {
|
|||
useQueryQuestionByTitle,
|
||||
getTagsBySlugName,
|
||||
} from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
import { handleFormError, SaveDraft, storageExpires } from '@/utils';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
import SearchQuestion from './components/SearchQuestion';
|
||||
|
@ -32,6 +33,8 @@ interface FormDataItem {
|
|||
edit_summary: Type.FormValue<string>;
|
||||
}
|
||||
|
||||
const saveDraft = new SaveDraft({ type: 'question' });
|
||||
|
||||
const Ask = () => {
|
||||
const initFormData = {
|
||||
title: {
|
||||
|
@ -66,6 +69,7 @@ const Ask = () => {
|
|||
const [checked, setCheckState] = useState(false);
|
||||
const [contentChanged, setContentChanged] = useState(false);
|
||||
const [focusType, setForceType] = useState('');
|
||||
const [hasDraft, setHasDraft] = useState(false);
|
||||
const resetForm = () => {
|
||||
setFormData(initFormData);
|
||||
setCheckState(false);
|
||||
|
@ -98,6 +102,34 @@ const Ask = () => {
|
|||
isEdit ? '' : formData.title.value,
|
||||
);
|
||||
|
||||
const removeDraft = () => {
|
||||
saveDraft.save.cancel();
|
||||
saveDraft.remove();
|
||||
setHasDraft(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!qid) {
|
||||
initQueryTags();
|
||||
const draft = storageExpires.get(DRAFT_QUESTION_STORAGE_KEY);
|
||||
if (draft) {
|
||||
formData.title.value = draft.title;
|
||||
formData.content.value = draft.content;
|
||||
formData.tags.value = draft.tags;
|
||||
formData.answer.value = draft.answer;
|
||||
setCheckState(Boolean(draft.answer));
|
||||
setHasDraft(true);
|
||||
setFormData({ ...formData });
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
resetForm();
|
||||
};
|
||||
}, [qid]);
|
||||
|
||||
useEffect(() => {
|
||||
const { title, tags, content, answer } = formData;
|
||||
const { title: editTitle, tags: editTags, content: editContent } = immData;
|
||||
|
@ -118,11 +150,21 @@ const Ask = () => {
|
|||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// write
|
||||
if (title.value || tags.value.length > 0 || content.value || answer.value) {
|
||||
// save draft
|
||||
saveDraft.save({
|
||||
params: {
|
||||
title: title.value,
|
||||
tags: tags.value,
|
||||
content: content.value,
|
||||
answer: answer.value,
|
||||
},
|
||||
callback: () => setHasDraft(true),
|
||||
});
|
||||
setContentChanged(true);
|
||||
} else {
|
||||
removeDraft();
|
||||
setContentChanged(false);
|
||||
}
|
||||
}, [formData]);
|
||||
|
@ -131,12 +173,6 @@ const Ask = () => {
|
|||
when: contentChanged,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEdit) {
|
||||
resetForm();
|
||||
initQueryTags();
|
||||
}
|
||||
}, [isEdit]);
|
||||
const { data: revisions = [] } = useQueryRevisions(qid);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -191,6 +227,14 @@ const Ask = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const deleteDraft = () => {
|
||||
const res = window.confirm(t('discard_confirm', { keyPrefix: 'draft' }));
|
||||
if (res) {
|
||||
removeDraft();
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
setContentChanged(false);
|
||||
event.preventDefault();
|
||||
|
@ -248,6 +292,7 @@ const Ask = () => {
|
|||
navigate(pathFactory.questionLanding(id));
|
||||
}
|
||||
}
|
||||
removeDraft();
|
||||
}
|
||||
};
|
||||
const backPage = () => {
|
||||
|
@ -376,10 +421,17 @@ const Ask = () => {
|
|||
<Button type="submit" className="me-2">
|
||||
{isEdit ? t('btn_save_edits') : t('btn_post_question')}
|
||||
</Button>
|
||||
{isEdit && (
|
||||
<Button variant="link" onClick={backPage}>
|
||||
{t('cancel', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button variant="link" onClick={backPage}>
|
||||
{t('cancel', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
{hasDraft && (
|
||||
<Button variant="link" onClick={deleteDraft}>
|
||||
{t('discard_draft', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isEdit && (
|
||||
|
@ -411,7 +463,6 @@ const Ask = () => {
|
|||
}}
|
||||
/>
|
||||
<Form.Control
|
||||
value={formData.answer.value}
|
||||
type="text"
|
||||
isInvalid={formData.answer.isInvalid}
|
||||
hidden
|
||||
|
@ -424,9 +475,14 @@ const Ask = () => {
|
|||
</>
|
||||
)}
|
||||
{checked && (
|
||||
<Button type="submit" className="mt-3">
|
||||
{t('post_question&answer')}
|
||||
</Button>
|
||||
<div className="mt-3">
|
||||
<Button type="submit">{t('post_question&answer')}</Button>
|
||||
{hasDraft && (
|
||||
<Button variant="link" className="ms-2" onClick={deleteDraft}>
|
||||
{t('discard_draft', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</Col>
|
||||
|
|
|
@ -10,38 +10,38 @@ interface Props {
|
|||
const Index: FC<Props> = ({ data }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Alert className="mb-4" variant="info">
|
||||
<div>
|
||||
{data.operation_msg.indexOf('http') > -1 ? (
|
||||
<p>
|
||||
{data.operation_description}{' '}
|
||||
<a href={data.operation_msg} style={{ color: '#055160' }}>
|
||||
<strong>{t('question_detail.show_exist')}</strong>
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{data.operation_msg
|
||||
? data.operation_msg
|
||||
: data.operation_description}
|
||||
</p>
|
||||
)}
|
||||
<div className="fs-14">
|
||||
{t('question_detail.closed_in')}{' '}
|
||||
<time
|
||||
dateTime={dayjs.unix(data.operation_time).tz().toISOString()}
|
||||
title={dayjs
|
||||
.unix(data.operation_time)
|
||||
.tz()
|
||||
.format(t('dates.long_date_with_time'))}>
|
||||
{dayjs
|
||||
.unix(data.operation_time)
|
||||
.tz()
|
||||
.format(t('dates.long_date_with_year'))}
|
||||
</time>
|
||||
.
|
||||
<Alert className="mb-4" variant={data.level}>
|
||||
{data.level === 'info' ? (
|
||||
<div>
|
||||
{data.msg.indexOf('http') > -1 ? (
|
||||
<p>
|
||||
{data.description}{' '}
|
||||
<a href={data.msg} style={{ color: '#055160' }}>
|
||||
<strong>{t('question_detail.show_exist')}</strong>
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
<p>{data.msg ? data.msg : data.description}</p>
|
||||
)}
|
||||
<div className="fs-14">
|
||||
{t('question_detail.closed_in')}{' '}
|
||||
<time
|
||||
dateTime={dayjs.unix(data.time).tz().toISOString()}
|
||||
title={dayjs
|
||||
.unix(data.time)
|
||||
.tz()
|
||||
.format(t('dates.long_date_with_time'))}>
|
||||
{dayjs
|
||||
.unix(data.time)
|
||||
.tz()
|
||||
.format(t('dates.long_date_with_year'))}
|
||||
</time>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
data.msg
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { memo, FC, useEffect, useRef } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { Button, Alert } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
|
||||
|
@ -72,6 +72,11 @@ const Index: FC<Props> = ({
|
|||
}
|
||||
return (
|
||||
<div id={data.id} ref={answerRef} className="answer-item py-4">
|
||||
{data.status === 10 && (
|
||||
<Alert variant="danger" className="mb-4">
|
||||
{t('post_deleted', { keyPrefix: 'messages' })}
|
||||
</Alert>
|
||||
)}
|
||||
<article
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
className="fmt text-break text-wrap"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useState, FC } from 'react';
|
||||
import { memo, useState, FC, useEffect } from 'react';
|
||||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -9,7 +9,8 @@ import { usePromptWithUnload } from '@/hooks';
|
|||
import { Editor, Modal, TextArea } from '@/components';
|
||||
import { FormDataType } from '@/common/interface';
|
||||
import { postAnswer } from '@/services';
|
||||
import { guard, handleFormError } from '@/utils';
|
||||
import { guard, handleFormError, SaveDraft, storageExpires } from '@/utils';
|
||||
import { DRAFT_ANSWER_STORAGE_KEY } from '@/common/constants';
|
||||
|
||||
interface Props {
|
||||
visible?: boolean;
|
||||
|
@ -21,6 +22,8 @@ interface Props {
|
|||
callback?: (obj) => void;
|
||||
}
|
||||
|
||||
const saveDraft = new SaveDraft({ type: 'answer' });
|
||||
|
||||
const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'question_detail.write_answer',
|
||||
|
@ -35,11 +38,51 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
const [showEditor, setShowEditor] = useState<boolean>(visible);
|
||||
const [focusType, setFocusType] = useState('');
|
||||
const [editorFocusState, setEditorFocusState] = useState(false);
|
||||
const [hasDraft, setHasDraft] = useState(false);
|
||||
|
||||
usePromptWithUnload({
|
||||
when: Boolean(formData.content.value),
|
||||
});
|
||||
|
||||
const removeDraft = () => {
|
||||
// immediately remove debounced save
|
||||
saveDraft.save.cancel();
|
||||
saveDraft.remove();
|
||||
setHasDraft(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const draft = storageExpires.get(DRAFT_ANSWER_STORAGE_KEY);
|
||||
if (draft?.questionId === data.qid && draft?.content) {
|
||||
setFormData({
|
||||
content: {
|
||||
value: draft.content,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
setShowEditor(true);
|
||||
setHasDraft(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const draft = storageExpires.get(DRAFT_ANSWER_STORAGE_KEY);
|
||||
const { content } = formData;
|
||||
|
||||
if (content.value) {
|
||||
// save Draft
|
||||
saveDraft.save({
|
||||
questionId: data?.qid,
|
||||
content: content.value,
|
||||
});
|
||||
|
||||
setHasDraft(true);
|
||||
} else if (draft?.questionId === data.qid && !content.value) {
|
||||
removeDraft();
|
||||
}
|
||||
}, [formData.content.value]);
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { content } = formData;
|
||||
|
@ -65,6 +108,24 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
return bol;
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
content: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteDraft = () => {
|
||||
const res = window.confirm(t('discard_confirm', { keyPrefix: 'draft' }));
|
||||
if (res) {
|
||||
removeDraft();
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
return;
|
||||
|
@ -86,6 +147,7 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
removeDraft();
|
||||
callback?.(res.info);
|
||||
})
|
||||
.catch((ex) => {
|
||||
|
@ -128,7 +190,6 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
setShowEditor(true);
|
||||
setEditorFocusState(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form noValidate className="mt-4">
|
||||
{(!data.answered || showEditor) && (
|
||||
|
@ -187,6 +248,11 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
) : (
|
||||
<Button onClick={clickBtn}>{t('btn_name')}</Button>
|
||||
)}
|
||||
{hasDraft && (
|
||||
<Button variant="link" className="ms-2" onClick={deleteDraft}>
|
||||
{t('discard_draft', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -56,6 +56,7 @@ const Index = () => {
|
|||
const { setUsers } = usePageUsers();
|
||||
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||
const isAuthor = userInfo?.username === question?.user_info?.username;
|
||||
const isAdmin = userInfo?.is_admin;
|
||||
const isLogged = Boolean(userInfo?.access_token);
|
||||
const { state: locationState } = useLocation();
|
||||
|
||||
|
@ -76,7 +77,22 @@ const Index = () => {
|
|||
page_size: 999,
|
||||
});
|
||||
if (res) {
|
||||
setAnswers(res);
|
||||
res.list = res.list?.filter((v) => {
|
||||
// delete answers pnly show to author and admin and has searchparams aid
|
||||
if (v.status === 10) {
|
||||
if (
|
||||
(v?.user_info.username === userInfo?.username || isAdmin) &&
|
||||
aid === v.id
|
||||
) {
|
||||
return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return v;
|
||||
});
|
||||
|
||||
setAnswers({ ...res, count: res.list.length });
|
||||
if (page > 0 || order) {
|
||||
// scroll into view;
|
||||
const element = document.getElementById('answerHeader');
|
||||
|
@ -183,9 +199,7 @@ const Index = () => {
|
|||
<Container className="pt-4 mt-2 mb-5 questionDetailPage">
|
||||
<Row className="justify-content-center">
|
||||
<Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0">
|
||||
{question?.operation?.operation_type && (
|
||||
<Alert data={question.operation} />
|
||||
)}
|
||||
{question?.operation?.level && <Alert data={question.operation} />}
|
||||
{isLoading ? (
|
||||
<ContentLoader />
|
||||
) : (
|
||||
|
|
|
@ -18,7 +18,7 @@ const Index: FC<Props> = ({ visible, introduction, data }) => {
|
|||
<h5 className="mb-3">{t('about_me')}</h5>
|
||||
{introduction ? (
|
||||
<div
|
||||
className="mb-4 text-break"
|
||||
className="mb-4 text-break fmt"
|
||||
dangerouslySetInnerHTML={{ __html: introduction }}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
usePersonalTop,
|
||||
usePersonalListByTabName,
|
||||
} from '@/services';
|
||||
import type { UserInfoRes } from '@/common/interface';
|
||||
|
||||
import {
|
||||
UserInfo,
|
||||
|
@ -47,8 +48,8 @@ const Personal: FC = () => {
|
|||
tabName,
|
||||
);
|
||||
let pageTitle = '';
|
||||
if (userInfo) {
|
||||
pageTitle = `${userInfo.info.display_name} (${userInfo.info.username})`;
|
||||
if (userInfo?.username) {
|
||||
pageTitle = `${userInfo?.display_name} (${userInfo?.username})`;
|
||||
}
|
||||
const { count = 0, list = [] } = listData?.[tabName] || {};
|
||||
usePageTags({
|
||||
|
@ -57,11 +58,11 @@ const Personal: FC = () => {
|
|||
return (
|
||||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
{userInfo?.info?.status !== 'normal' && userInfo?.info?.status_msg && (
|
||||
<Alert data={userInfo?.info.status_msg} />
|
||||
{userInfo?.status !== 'normal' && userInfo?.status_msg && (
|
||||
<Alert data={userInfo?.status_msg} />
|
||||
)}
|
||||
<Col xxl={7} lg={8} sm={12}>
|
||||
<UserInfo data={userInfo?.info} />
|
||||
<UserInfo data={userInfo as UserInfoRes} />
|
||||
</Col>
|
||||
<Col
|
||||
xxl={3}
|
||||
|
@ -88,11 +89,11 @@ const Personal: FC = () => {
|
|||
<Col xxl={7} lg={8} sm={12}>
|
||||
<Overview
|
||||
visible={tabName === 'overview'}
|
||||
introduction={userInfo?.info?.bio_html}
|
||||
introduction={userInfo?.bio_html || ''}
|
||||
data={topData}
|
||||
/>
|
||||
<ListHead
|
||||
count={tabName === 'reputation' ? userInfo?.info?.rank : count}
|
||||
count={tabName === 'reputation' ? Number(userInfo?.rank) : count}
|
||||
sort={order}
|
||||
visible={tabName !== 'overview'}
|
||||
tabName={tabName}
|
||||
|
@ -120,17 +121,14 @@ const Personal: FC = () => {
|
|||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<h5 className="mb-3">{t('stats')}</h5>
|
||||
{userInfo?.info && (
|
||||
{userInfo?.created_at && (
|
||||
<>
|
||||
<div className="text-secondary">
|
||||
<FormatTime
|
||||
time={userInfo.info.created_at}
|
||||
preFix={t('joined')}
|
||||
/>
|
||||
<FormatTime time={userInfo.created_at} preFix={t('joined')} />
|
||||
</div>
|
||||
<div className="text-secondary">
|
||||
<FormatTime
|
||||
time={userInfo.info.last_login_date}
|
||||
time={userInfo.last_login_date}
|
||||
preFix={t('last_login')}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import Error50X from '@/pages/50X';
|
||||
// import Page404 from '@/pages/404';
|
||||
|
||||
const Index = () => {
|
||||
return <Error50X />;
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -2,9 +2,10 @@ import { Suspense, lazy } from 'react';
|
|||
import { RouteObject } from 'react-router-dom';
|
||||
|
||||
import Layout from '@/pages/Layout';
|
||||
import ErrorBoundary from '@/pages/50X';
|
||||
import baseRoutes, { RouteNode } from '@/router/routes';
|
||||
import RouteGuard from '@/router/RouteGuard';
|
||||
|
||||
import baseRoutes, { RouteNode } from './routes';
|
||||
import RouteGuard from './RouteGuard';
|
||||
import RouteErrorBoundary from './RouteErrorBoundary';
|
||||
|
||||
const routes: RouteNode[] = [];
|
||||
|
||||
|
@ -18,7 +19,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => {
|
|||
) : (
|
||||
<Layout />
|
||||
);
|
||||
rn.errorElement = <ErrorBoundary />;
|
||||
rn.errorElement = <RouteErrorBoundary />;
|
||||
} else {
|
||||
/**
|
||||
* cannot use a fully dynamic import statement
|
||||
|
@ -37,6 +38,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => {
|
|||
)}
|
||||
</Suspense>
|
||||
);
|
||||
rn.errorElement = <RouteErrorBoundary />;
|
||||
}
|
||||
root.push(rn);
|
||||
const children = Array.isArray(rn.children) ? rn.children : null;
|
||||
|
|
|
@ -8,7 +8,10 @@ export const usePersonalInfoByName = (username: string) => {
|
|||
const apiUrl = '/answer/api/v1/personal/user/info';
|
||||
const { data, error, mutate } = useSWR<Type.UserInfoRes, Error>(
|
||||
username ? `${apiUrl}?username=${username}` : null,
|
||||
request.instance.get,
|
||||
(url) =>
|
||||
request.get(url, {
|
||||
allow404: true,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
data,
|
||||
|
|
|
@ -47,7 +47,9 @@ export const useTagInfo = ({ id = '', name = '' }) => {
|
|||
name = encodeURIComponent(name);
|
||||
apiUrl = `/answer/api/v1/tag?name=${name}`;
|
||||
}
|
||||
const { data, error } = useSWR<Type.TagInfo>(apiUrl, request.instance.get);
|
||||
const { data, error } = useSWR<Type.TagInfo>(apiUrl, (url) =>
|
||||
request.get(url, { allow404: true }),
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !data && !error,
|
||||
|
|
|
@ -171,6 +171,7 @@ export const saveQuestion = (params: Type.QuestionParams) => {
|
|||
export const questionDetail = (id: string) => {
|
||||
return request.get<Type.QuestionDetailRes>(
|
||||
`/answer/api/v1/question/info?id=${id}`,
|
||||
{ allow404: true },
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import pageTagStore from './pageTags';
|
|||
import customizeStore from './customize';
|
||||
import themeSettingStore from './themeSetting';
|
||||
import loginToContinueStore from './loginToContinue';
|
||||
import notFoundStore from './notFound';
|
||||
|
||||
export {
|
||||
toastStore,
|
||||
|
@ -23,4 +24,5 @@ export {
|
|||
themeSettingStore,
|
||||
seoSettingStore,
|
||||
loginToContinueStore,
|
||||
notFoundStore,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import create from 'zustand';
|
||||
|
||||
interface NotFoundType {
|
||||
visible: boolean;
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
const notFound = create<NotFoundType>((set) => ({
|
||||
visible: false,
|
||||
show: () => {
|
||||
set(() => {
|
||||
return { visible: true };
|
||||
});
|
||||
},
|
||||
hide: () => {
|
||||
set(() => {
|
||||
return { visible: false };
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
export default notFound;
|
|
@ -237,7 +237,26 @@ function htmlToReact(html: string) {
|
|||
const cleanedHtml = DOMPurify.sanitize(html, {
|
||||
USE_PROFILES: { html: true },
|
||||
});
|
||||
return parse(cleanedHtml);
|
||||
|
||||
const ele = document.createElement('div');
|
||||
ele.innerHTML = cleanedHtml;
|
||||
|
||||
ele.querySelectorAll('table').forEach((table) => {
|
||||
if (
|
||||
(!table || (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);
|
||||
});
|
||||
return parse(ele.innerHTML);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -63,7 +63,7 @@ export const deriveLoginState = (): TLoginState => {
|
|||
return ls;
|
||||
};
|
||||
|
||||
const isIgnoredPath = (ignoredPath: string | string[]) => {
|
||||
export const isIgnoredPath = (ignoredPath: string | string[]) => {
|
||||
if (!Array.isArray(ignoredPath)) {
|
||||
ignoredPath = [ignoredPath];
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
export { default as request } from './request';
|
||||
export { default as Storage } from './storage';
|
||||
export { floppyNavigation } from './floppyNavigation';
|
||||
export { default as storageExpires } from './storageWithExpires';
|
||||
export { default as SaveDraft } from './saveDraft';
|
||||
|
||||
export * as guard from './guard';
|
||||
export * as localize from './localize';
|
||||
|
|
|
@ -2,19 +2,24 @@ import axios, { AxiosResponse } from 'axios';
|
|||
import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
|
||||
|
||||
import { Modal } from '@/components';
|
||||
import { loggedUserInfoStore, toastStore } from '@/stores';
|
||||
import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';
|
||||
import { loggedUserInfoStore, toastStore, notFoundStore } from '@/stores';
|
||||
import { LOGGED_TOKEN_STORAGE_KEY, IGNORE_PATH_LIST } from '@/common/constants';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import { getCurrentLang } from '@/utils/localize';
|
||||
|
||||
import Storage from './storage';
|
||||
import { floppyNavigation } from './floppyNavigation';
|
||||
import { isIgnoredPath } from './guard';
|
||||
|
||||
const baseConfig = {
|
||||
timeout: 10000,
|
||||
withCredentials: true,
|
||||
};
|
||||
|
||||
interface APIconfig extends AxiosRequestConfig {
|
||||
allow404: boolean;
|
||||
}
|
||||
|
||||
class Request {
|
||||
instance: AxiosInstance;
|
||||
|
||||
|
@ -49,6 +54,9 @@ class Request {
|
|||
(error) => {
|
||||
const { status, data: respData } = error.response || {};
|
||||
const { data = {}, msg = '', reason = '' } = respData || {};
|
||||
|
||||
console.log('response error:', error);
|
||||
|
||||
if (status === 400) {
|
||||
// show error message
|
||||
if (data instanceof Object && data.err_type) {
|
||||
|
@ -99,12 +107,14 @@ class Request {
|
|||
// 401: Re-login required
|
||||
if (status === 401) {
|
||||
// clear userinfo
|
||||
notFoundStore.getState().hide();
|
||||
loggedUserInfoStore.getState().clear();
|
||||
floppyNavigation.navigateToLogin();
|
||||
return Promise.reject(false);
|
||||
}
|
||||
if (status === 403) {
|
||||
// Permission interception
|
||||
notFoundStore.getState().hide();
|
||||
if (data?.type === 'url_expired') {
|
||||
// url expired
|
||||
floppyNavigation.navigate(RouteAlias.activationFailed, () => {
|
||||
|
@ -135,6 +145,14 @@ class Request {
|
|||
}
|
||||
return Promise.reject(false);
|
||||
}
|
||||
|
||||
if (status === 404 && error.config?.allow404) {
|
||||
if (isIgnoredPath(IGNORE_PATH_LIST)) {
|
||||
return Promise.reject(false);
|
||||
}
|
||||
notFoundStore.getState().show();
|
||||
return Promise.reject(false);
|
||||
}
|
||||
if (status >= 500) {
|
||||
console.error(
|
||||
`Request failed with status code ${status}, ${msg || ''}`,
|
||||
|
@ -149,7 +167,7 @@ class Request {
|
|||
return this.instance.request(config);
|
||||
}
|
||||
|
||||
public get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
public get<T = any>(url: string, config?: APIconfig): Promise<T> {
|
||||
return this.instance.get(url, config);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { debounce } from 'lodash';
|
||||
|
||||
import {
|
||||
DRAFT_QUESTION_STORAGE_KEY,
|
||||
DRAFT_ANSWER_STORAGE_KEY,
|
||||
} from '@/common/constants';
|
||||
import { storageExpires as storage } from '@/utils';
|
||||
|
||||
export type QuestionDraft = {
|
||||
params: {
|
||||
title: string;
|
||||
content: string;
|
||||
tags: any[];
|
||||
answer: string;
|
||||
};
|
||||
callback?: () => void;
|
||||
};
|
||||
|
||||
export type AnswerDraft = {
|
||||
questionId: string;
|
||||
content: string;
|
||||
callback?: () => void;
|
||||
};
|
||||
|
||||
type DraftType = {
|
||||
type: 'question' | 'answer';
|
||||
};
|
||||
|
||||
export type DraftParams = QuestionDraft | AnswerDraft;
|
||||
|
||||
class SaveDraft {
|
||||
type: DraftType['type'];
|
||||
|
||||
status: 'save' | 'remove';
|
||||
|
||||
constructor({ type = 'question' }: DraftType) {
|
||||
this.type = type;
|
||||
this.status = 'save';
|
||||
}
|
||||
|
||||
save = debounce((data: DraftParams) => {
|
||||
// TODO
|
||||
if (this.status === 'remove') {
|
||||
return;
|
||||
}
|
||||
if (this.type === 'question') {
|
||||
const { params, callback } = data as QuestionDraft;
|
||||
|
||||
this.storeDraft(params, callback);
|
||||
}
|
||||
|
||||
if (this.type === 'answer') {
|
||||
const { content, questionId, callback } = data as AnswerDraft;
|
||||
if (!questionId || !content) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storeDraft({ content, questionId }, callback);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
remove() {
|
||||
this.status = 'remove';
|
||||
const that = this;
|
||||
if (this.type === 'question') {
|
||||
storage.remove(DRAFT_QUESTION_STORAGE_KEY, () => {
|
||||
that.status = 'save';
|
||||
});
|
||||
}
|
||||
if (this.type === 'answer') {
|
||||
storage.remove(DRAFT_ANSWER_STORAGE_KEY, () => {
|
||||
that.status = 'save';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private storeDraft = (params: any, callback) => {
|
||||
const key =
|
||||
this.type === 'question'
|
||||
? DRAFT_QUESTION_STORAGE_KEY
|
||||
: DRAFT_ANSWER_STORAGE_KEY;
|
||||
storage.set(key, params);
|
||||
callback?.();
|
||||
};
|
||||
}
|
||||
|
||||
export default SaveDraft;
|
|
@ -0,0 +1,50 @@
|
|||
import { DRAFT_TIMESIGH_STORAGE_KEY as timeSign } from '@/common/constants';
|
||||
|
||||
const store = {
|
||||
storage: localStorage || window.localStorage,
|
||||
set(key: string, value, time?: number): void {
|
||||
const t = time || Date.now() + 1000 * 60 * 60 * 24 * 7; // default 7 days
|
||||
try {
|
||||
this.storage.setItem(key, `${t}${timeSign}${JSON.stringify(value)}`);
|
||||
} catch {
|
||||
// ignore
|
||||
console.error('set storage error: the key is', key);
|
||||
}
|
||||
},
|
||||
get(key: string): any {
|
||||
const timeSignLen = timeSign.length;
|
||||
let index = 0;
|
||||
let time = 0;
|
||||
let res;
|
||||
try {
|
||||
res = this.storage.getItem(key);
|
||||
} catch {
|
||||
console.error('get storage error: the key is', key);
|
||||
}
|
||||
if (res) {
|
||||
index = res.indexOf(timeSign);
|
||||
time = +res.slice(0, index);
|
||||
if (time > new Date().getTime()) {
|
||||
res = res.slice(index + timeSignLen);
|
||||
try {
|
||||
res = JSON.parse(res);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
// timeout remove storage
|
||||
res = null;
|
||||
this.storage.removeItem(key);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
remove(key: string, callback?: () => void): void {
|
||||
this.storage.removeItem(key);
|
||||
callback?.();
|
||||
},
|
||||
};
|
||||
|
||||
export default store;
|
|
@ -3,28 +3,28 @@
|
|||
<div class="justify-content-center row">
|
||||
<div class="col-xxl-7 col-lg-8 col-sm-12">
|
||||
<div class="d-flex flex-column flex-md-row mb-4">
|
||||
<a href="/users/{{.userinfo.Info.Username}}"><img
|
||||
src="{{.userinfo.Info.Avatar}}"
|
||||
<a href="/users/{{.userinfo.Username}}"><img
|
||||
src="{{.userinfo.Avatar}}"
|
||||
width="160px" height="160px" class="rounded" alt="" /></a>
|
||||
<div class="ms-0 ms-md-4 mt-4 mt-md-0">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<a class="link-dark h3 mb-0" href="/users/{{.userinfo.Info.Username}}">{{.userinfo.Info.DisplayName}}</a>
|
||||
<a class="link-dark h3 mb-0" href="/users/{{.userinfo.Username}}">{{.userinfo.DisplayName}}</a>
|
||||
|
||||
</div>
|
||||
<div class="text-secondary mb-4">@{{.userinfo.Info.Username}}</div>
|
||||
<div class="text-secondary mb-4">@{{.userinfo.Username}}</div>
|
||||
<div class="d-flex flex-wrap mb-3">
|
||||
<div class="me-3">
|
||||
<strong class="fs-5">{{.userinfo.Info.Rank}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_reputation"}}</span>
|
||||
<strong class="fs-5">{{.userinfo.Rank}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_reputation"}}</span>
|
||||
</div>
|
||||
<div class="me-3">
|
||||
<strong class="fs-5">{{.userinfo.Info.AnswerCount}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_answers"}}</span>
|
||||
<strong class="fs-5">{{.userinfo.AnswerCount}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_answers"}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong class="fs-5">{{.userinfo.Info.QuestionCount}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_questions"}}</span>
|
||||
<strong class="fs-5">{{.userinfo.QuestionCount}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_questions"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{if .userinfo.Info.Website }}
|
||||
<div class="d-flex align-items-center"><i class="br bi-house-door-fill me-2"></i><a class="link-secondary" href="{{.userinfo.Info.Website}}">{{.userinfo.Info.Website}}</a></div>
|
||||
{{if .userinfo.Website }}
|
||||
<div class="d-flex align-items-center"><i class="br bi-house-door-fill me-2"></i><a class="link-secondary" href="{{.userinfo.Website}}">{{.userinfo.Website}}</a></div>
|
||||
{{else}}
|
||||
{{end}}
|
||||
<div class="d-flex text-secondary"></div>
|
||||
|
|
Loading…
Reference in New Issue