diff --git a/.goreleaser.yaml b/.goreleaser.yaml index cf61b678..149a7495 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -15,7 +15,7 @@ builds: - id: build main: ./cmd/answer/. binary: answer - ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser + ldflags: -s -w -X github.com/answerdev/answer/cmd.Version={{.Version}} -X github.com/answerdev/answer/cmd.Revision={{.ShortCommit}} -X github.com/answerdev/answer/cmd.Time={{.Date}} -X main.BuildUser=goreleaser flags: -v goos: - linux @@ -26,7 +26,7 @@ builds: - id: build-windows main: ./cmd/answer/. binary: answer - ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser + ldflags: -s -w -X github.com/answerdev/answer/cmd.Version={{.Version}} -X github.com/answerdev/answer/cmd.Revision={{.ShortCommit}} -X github.com/answerdev/answer/cmd.Time={{.Date}} -X main.BuildUser=goreleaser flags: -v goos: - windows diff --git a/Makefile b/Makefile index 50c79f68..5fb96f68 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build clean ui -VERSION=1.0.7 +VERSION=1.0.8 BIN=answer DIR_SRC=./cmd/answer DOCKER_CMD=docker diff --git a/cmd/answer/main.go b/cmd/answer/main.go index 626eb2b2..06f8c47b 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -53,6 +53,7 @@ func runApp() { panic(err) } constant.Version = Version + constant.Revision = Revision schema.AppStartTime = time.Now() defer cleanup() diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index aafd3dc4..bbb6e1ec 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -165,8 +165,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo) answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo) questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService, dataData) - questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService) + questionController := controller.NewQuestionController(questionService, answerService, rankService) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchParser := search_parser.NewSearchParser(tagCommonService, userCommon) diff --git a/docs/docs.go b/docs/docs.go index 6955ff5f..4622228b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3063,6 +3063,45 @@ const docTemplate = `{ } } }, + "/answer/api/v1/question/answer": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add question and answer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "add question and answer", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionAddByAnswer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/api/v1/question/closemsglist": { "get": { "security": [ @@ -6751,6 +6790,41 @@ const docTemplate = `{ } } }, + "schema.QuestionAddByAnswer": { + "type": "object", + "required": [ + "answer_content", + "content", + "tags", + "title" + ], + "properties": { + "answer_content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "content": { + "description": "content", + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "tags": { + "description": "tags", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } + }, + "title": { + "description": "question title", + "type": "string", + "maxLength": 150, + "minLength": 6 + } + } + }, "schema.QuestionPageReq": { "type": "object", "properties": { @@ -7230,6 +7304,9 @@ const docTemplate = `{ "login": { "$ref": "#/definitions/schema.SiteLoginResp" }, + "revision": { + "type": "string" + }, "site_seo": { "$ref": "#/definitions/schema.SiteSeoReq" }, diff --git a/docs/swagger.json b/docs/swagger.json index 962004ee..240e543f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3051,6 +3051,45 @@ } } }, + "/answer/api/v1/question/answer": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add question and answer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "add question and answer", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionAddByAnswer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/api/v1/question/closemsglist": { "get": { "security": [ @@ -6739,6 +6778,41 @@ } } }, + "schema.QuestionAddByAnswer": { + "type": "object", + "required": [ + "answer_content", + "content", + "tags", + "title" + ], + "properties": { + "answer_content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "content": { + "description": "content", + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "tags": { + "description": "tags", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } + }, + "title": { + "description": "question title", + "type": "string", + "maxLength": 150, + "minLength": 6 + } + } + }, "schema.QuestionPageReq": { "type": "object", "properties": { @@ -7218,6 +7292,9 @@ "login": { "$ref": "#/definitions/schema.SiteLoginResp" }, + "revision": { + "type": "string" + }, "site_seo": { "$ref": "#/definitions/schema.SiteSeoReq" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 88129421..7aa79508 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1019,6 +1019,33 @@ definitions: - tags - title type: object + schema.QuestionAddByAnswer: + properties: + answer_content: + maxLength: 65535 + minLength: 6 + type: string + content: + description: content + maxLength: 65535 + minLength: 6 + type: string + tags: + description: tags + items: + $ref: '#/definitions/schema.TagItem' + type: array + title: + description: question title + maxLength: 150 + minLength: 6 + type: string + required: + - answer_content + - content + - tags + - title + type: object schema.QuestionPageReq: properties: orderCond: @@ -1354,6 +1381,8 @@ definitions: $ref: '#/definitions/schema.SiteInterfaceResp' login: $ref: '#/definitions/schema.SiteLoginResp' + revision: + type: string site_seo: $ref: '#/definitions/schema.SiteSeoReq' theme: @@ -3794,6 +3823,30 @@ paths: summary: update question tags: - Question + /answer/api/v1/question/answer: + post: + consumes: + - application/json + description: add question and answer + parameters: + - description: question + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.QuestionAddByAnswer' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: add question and answer + tags: + - Question /answer/api/v1/question/closemsglist: get: consumes: diff --git a/go.mod b/go.mod index db9780ca..6f600f4c 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/mojocn/base64Captcha v1.3.5 github.com/ory/dockertest/v3 v3.9.1 github.com/robfig/cron/v3 v3.0.1 + github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405 github.com/segmentfault/pacman v1.0.3 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221219081300-f734f4a16aa0 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 @@ -41,7 +42,7 @@ require ( 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.2.0 + golang.org/x/net v0.7.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.14.2 @@ -64,14 +65,20 @@ require ( github.com/docker/docker v20.10.7+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect + github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f // indirect + github.com/dsoprea/go-jpeg-image-structure v0.0.0-20190422055009-d6f9ba25cf48 // indirect + github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696 // indirect + github.com/dsoprea/go-png-image-structure v0.0.0-20190624104353-c9b28dcdc5c8 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-errors/errors v1.0.1 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/spec v0.20.7 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/geo v0.0.0-20190812012225-f41920e961ce // 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 @@ -121,8 +128,8 @@ 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.2.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect golang.org/x/tools v0.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index abbc8e1a..02402218 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,14 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f h1:vqfYiZ+xF0xJvl9SZ1kovmMgKjaGZIz/Hn8JDQdyd9A= +github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f/go.mod h1:DmMpU91/Ax6BAwoRkjgRCr2rmgEgS4tsmatfV7M+U+c= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20190422055009-d6f9ba25cf48 h1:9zARagUAxQJjibcDy+0+koUMR6sbX38L49Bk2Vni628= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20190422055009-d6f9ba25cf48/go.mod h1:H1hAaFyv9cRV1ywoHvaqVoNSThBvWZ0JarRBcV+FSnE= +github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696 h1:VGFnZAcLwPpt1sHlAxml+pGLZz9A2s+K/s1YNhPC91Y= +github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA= +github.com/dsoprea/go-png-image-structure v0.0.0-20190624104353-c9b28dcdc5c8 h1:SVQfy5rBFZXzvGkU2MZ0RzpS912/1sJrEJ+FMmeaC9U= +github.com/dsoprea/go-png-image-structure v0.0.0-20190624104353-c9b28dcdc5c8/go.mod h1:Bf0nmcDFFRQBjZwr9qY6c0zTxKQa+Q8YWZmlYxXGxY0= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -173,6 +181,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -229,6 +239,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/geo v0.0.0-20190812012225-f41920e961ce h1:rqIKPpIcEgiNn0KYNFYD34TbMO86l4woyhNzSP+Oegs= +github.com/golang/geo v0.0.0-20190812012225-f41920e961ce/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -605,6 +617,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405 h1:2ieGkj4z/YPXVyQ2ayZUg3GwE1pYWd5f1RB6DzAOXKM= +github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405/go.mod h1:rIxVzVLKlBwLxO+lC+k/I4HJfRQcemg/f/76Xmmzsec= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/segmentfault/pacman v1.0.3 h1:/K8LJHQMiCaCIvC/e8GQITpYTEG6RH4KTLTZjPTghl4= @@ -851,8 +865,9 @@ 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.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 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= @@ -945,12 +960,14 @@ 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.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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= @@ -960,8 +977,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1144,6 +1161,7 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gG gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index b4121379..3decada5 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -12,6 +12,17 @@ backend: other: Unauthorized. database_error: other: Data server error. + action: + report: + other: Flag + edit: + other: Edit + delete: + other: Delete + close: + other: Close + reopen: + other: Reopen role: name: user: @@ -100,6 +111,8 @@ backend: rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. + vote_fail_to_meet_the_condition: + other: Thanks for the feedback. You need at least {{ rank }} reputation to cast a vote. report: handle_failed: other: Report handle failed. @@ -303,6 +316,7 @@ ui: users: Users http_404: HTTP Error 404 http_50X: HTTP Error 500 + http_403: HTTP Error 403 notifications: title: Notifications inbox: Inbox @@ -525,6 +539,7 @@ ui: tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + tip_vote: It adds something useful to the post edit_answer: title: Edit Answer default_reason: Edit answer @@ -784,6 +799,11 @@ ui: answered: answered closed_in: Closed in show_exist: Show existing question. + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + answer_useful: It is useful + answer_un_useful: It is not useful answers: title: Answers score: Score @@ -801,10 +821,19 @@ ui: edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened + delete: title: Delete this post question: >- @@ -1029,13 +1058,11 @@ ui: votes: votes 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. + page_error: + http_error: HTTP Error {{ code }} + desc_403: You don’t have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." @@ -1421,8 +1448,8 @@ ui: no_data: "We couldn't find anything." users: title: Users - users_with_the_most_reputation: Users with the highest reputation scores - users_with_the_most_vote: Users who voted the most + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week staffs: Our community staff reputation: reputation votes: votes diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 991a8131..77abeedf 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -11,6 +11,17 @@ backend: other: 未授权。 database_error: other: 数据服务器错误。 + action: + report: + other: 举报 + edit: + other: 编辑 + delete: + other: 删除 + close: + other: 关闭 + reopen: + other: 重新打开 role: name: user: @@ -99,6 +110,8 @@ backend: rank: fail_to_meet_the_condition: other: 级别不符合条件 + vote_fail_to_meet_the_condition: + other: 感谢您的投票。您至少需要{{ rank }}声望才能投票。 report: handle_failed: other: 报告处理失败 @@ -985,13 +998,11 @@ ui: votes: 个点赞 answers: 个回答 accepted: 已被采纳 - page_404: - http_error: HTTP 错误 404 - desc: "很抱歉,此页面不存在。" - back_home: 回到主页 - page_50X: - http_error: HTTP 错误 500 - desc: 服务器遇到了一个错误,无法完成你的请求。 + page_error: + http_error: HTTP Error {{ code }} + desc_403: 你无权访问此页面。 + desc_404: 很抱歉,此页面不存在。 + desc_50X: 服务器遇到了一个错误,无法完成你的请求。 back_home: 回到主页 page_maintenance: desc: "我们正在进行维护,我们将很快回来。" @@ -1374,8 +1385,8 @@ ui: no_data: "空空如也" users: title: 用户 - users_with_the_most_reputation: 信誉积分最高的用户 - users_with_the_most_vote: 投票最多的用户 + users_with_the_most_reputation: 本周信誉积分最高的用户 + users_with_the_most_vote: 本周投票最多的用户 staffs: 我们的社区工作人员 reputation: 声望值 votes: 投票 diff --git a/i18n/zh_TW.yaml b/i18n/zh_TW.yaml index 8e73f0e1..84ebe6d2 100644 --- a/i18n/zh_TW.yaml +++ b/i18n/zh_TW.yaml @@ -985,13 +985,11 @@ ui: votes: 得票 answers: 回答 accepted: 已採納 - page_404: - http_error: HTTP Error 404 - desc: "很抱歉,此頁面不存在。" - back_home: 回到首頁 - page_50X: - http_error: HTTP Error 500 - desc: 伺服器遇到了一個錯誤,無法完成你的請求。 + page_error: + http_error: HTTP Error {{ code }} + desc_403: 你无权访问此頁面。 + desc_404: 很抱歉,此頁面不存在。 + desc_50X: 伺服器遇到了一個錯誤,無法完成你的請求。 back_home: 回到首頁 page_maintenance: desc: "我們正在維護中,很快就會回來。" diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 40990b93..a71d1a9c 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -30,7 +30,8 @@ const ( // object TagID AnswerList // key equal database's table name var ( - Version string = "" + Version string = "" + Revision string = "" PathIgnoreMap map[string]bool diff --git a/internal/base/handler/handler.go b/internal/base/handler/handler.go index c3b6025e..22784a81 100644 --- a/internal/base/handler/handler.go +++ b/internal/base/handler/handler.go @@ -3,6 +3,7 @@ package handler import ( "errors" "net/http" + "strings" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" @@ -73,3 +74,10 @@ func BindAndCheckReturnErr(ctx *gin.Context, data interface{}) (errFields []*val errFields, _ = validator.GetValidatorByLang(lang).Check(data) return errFields } + +func MsgWithParameter(msg string, list map[string]string) string { + for k, v := range list { + msg = strings.Replace(msg, "{{ "+k+" }}", v, -1) + } + return msg +} diff --git a/internal/base/middleware/auth.go b/internal/base/middleware/auth.go index b2150e38..ed941c0f 100644 --- a/internal/base/middleware/auth.go +++ b/internal/base/middleware/auth.go @@ -124,7 +124,7 @@ func (am *AuthUserMiddleware) AdminAuth() gin.HandlerFunc { } userInfo, err := am.authService.GetAdminUserCacheInfo(ctx, token) if err != nil { - handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + handler.HandleResponse(ctx, errors.Forbidden(reason.UnauthorizedError), nil) ctx.Abort() return } diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 2e4c8c82..5f9e316a 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -48,6 +48,7 @@ const ( TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete" TagAlreadyExist = "error.tag.already_exist" RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition" + VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition" ThemeNotFound = "error.theme.not_found" LangNotFound = "error.lang.not_found" ReportHandleFailed = "error.report.handle_failed" diff --git a/internal/base/server/http_funcmap.go b/internal/base/server/http_funcmap.go index c5f58f45..8116842a 100644 --- a/internal/base/server/http_funcmap.go +++ b/internal/base/server/http_funcmap.go @@ -9,6 +9,7 @@ import ( "time" "github.com/answerdev/answer/internal/base/translator" + "github.com/answerdev/answer/internal/controller" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/pkg/day" @@ -41,6 +42,9 @@ var funcMap = template.FuncMap{ "templateHTML": func(data string) template.HTML { return template.HTML(data) }, + "formatLinkNofollow": func(data string) template.HTML { + return template.HTML(FormatLinkNofollow(data)) + }, "translator": func(la i18n.Language, data string, params ...interface{}) string { trans := translator.GlobalTrans.Tr(la, data) @@ -116,3 +120,21 @@ var funcMap = template.FuncMap{ return htmltext.UrlTitle(title) }, } + +func FormatLinkNofollow(html string) string { + var hrefRegexp = regexp.MustCompile("(?m).*?") + match := hrefRegexp.FindAllString(html, -1) + if match != nil { + for _, v := range match { + hasNofollow := strings.Contains(v, "rel=\"nofollow\"") + hasSiteUrl := strings.Contains(v, controller.SiteUrl) + if !hasSiteUrl { + if !hasNofollow { + nofollowUrl := strings.Replace(v, " 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) + return + } + + resp, err := qc.questionService.AddQuestion(ctx, questionReq) + if err != nil { + errlist, ok := resp.([]*validator.FormErrorField) + if ok { + errFields = append(errFields, errlist...) + } + } + + if len(errFields) > 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) + return + } + //add the question id to the answer + questionInfo, ok := resp.(*schema.QuestionInfo) + if ok { + answerReq := &schema.AnswerAddReq{} + answerReq.QuestionID = uid.DeShortID(questionInfo.ID) + answerReq.UserID = middleware.GetLoginUserIDFromContext(ctx) + answerReq.Content = req.AnswerContent + answerReq.HTML = req.AnswerHTML + answerID, err := qc.answerService.Insert(ctx, answerReq) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + info, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !has { + handler.HandleResponse(ctx, nil, nil) + return + } + handler.HandleResponse(ctx, err, gin.H{ + "info": info, + "question": questionInfo, + }) + return + } + + handler.HandleResponse(ctx, err, resp) +} + // UpdateQuestion update question // @Summary update question // @Description update question diff --git a/internal/controller/revision_controller.go b/internal/controller/revision_controller.go index dfac4519..bbfd3139 100644 --- a/internal/controller/revision_controller.go +++ b/internal/controller/revision_controller.go @@ -5,6 +5,7 @@ import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/middleware" "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" "github.com/answerdev/answer/internal/service/permission" @@ -52,7 +53,13 @@ func (rc *RevisionController) GetRevisionList(ctx *gin.Context) { } resp, err := rc.revisionListService.GetRevisionList(ctx, req) - handler.HandleResponse(ctx, err, resp) + list := make([]schema.GetRevisionResp, 0) + for _, item := range resp { + if item.Status == entity.RevisioNnormalStatus || item.Status == entity.RevisionReviewPassStatus { + list = append(list, item) + } + } + handler.HandleResponse(ctx, err, list) } // GetUnreviewedRevisionList godoc diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index b2a95bdf..b0ec1315 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -31,7 +31,7 @@ func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonServic // @Router /answer/api/v1/siteinfo [get] func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) { var err error - resp := &schema.SiteInfoResp{Version: constant.Version} + resp := &schema.SiteInfoResp{Version: constant.Version, Revision: constant.Revision} resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Error(err) @@ -103,6 +103,7 @@ func (sc *SiteinfoController) GetManifestJson(ctx *gin.Context) { resp := &schema.GetManifestJsonResp{ ManifestVersion: 3, Version: constant.Version, + Revision: constant.Revision, ShortName: "Answer", Name: "Answer.dev", Icons: map[string]string{ diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index aa9e702a..0d4bd3f0 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -23,6 +23,8 @@ import ( "github.com/segmentfault/pacman/log" ) +var SiteUrl = "" + type TemplateController struct { scriptPath string cssPath string @@ -67,6 +69,7 @@ func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInf if err != nil { log.Error(err) } + SiteUrl = resp.General.SiteUrl resp.Interface, err = tc.siteInfoService.GetSiteInterface(ctx) if err != nil { log.Error(err) @@ -471,6 +474,7 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter data["Version"] = constant.Version + data["Revision"] = constant.Revision _, ok := data["path"] if !ok { data["path"] = "" diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index a1489e41..4fb7f8a6 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -529,9 +529,9 @@ func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) { return } - err := uc.userService.UserChangeEmailVerify(ctx, req.Content) + resp, err := uc.userService.UserChangeEmailVerify(ctx, req.Content) uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) - handler.HandleResponse(ctx, err, nil) + handler.HandleResponse(ctx, err, resp) } // UserRanking get user ranking diff --git a/internal/controller/vote_controller.go b/internal/controller/vote_controller.go index e36fd08c..46fe8e32 100644 --- a/internal/controller/vote_controller.go +++ b/internal/controller/vote_controller.go @@ -1,9 +1,12 @@ package controller import ( + "fmt" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/middleware" "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service" "github.com/answerdev/answer/internal/service/rank" @@ -41,13 +44,16 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) { } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) - can, _, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true) + can, rank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { - handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + lang := handler.GetLang(ctx) + msg := translator.Tr(lang, reason.VoteRankFailToMeetTheCondition) + msg = handler.MsgWithParameter(msg, map[string]string{"rank": fmt.Sprintf("%d", rank)}) + handler.HandleResponse(ctx, errors.Forbidden(reason.VoteRankFailToMeetTheCondition).WithMsg(msg), nil) return } @@ -78,13 +84,16 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) { } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) - can, _, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false) + can, rank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { - handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + lang := handler.GetLang(ctx) + msg := translator.Tr(lang, reason.VoteRankFailToMeetTheCondition) + msg = handler.MsgWithParameter(msg, map[string]string{"rank": fmt.Sprintf("%d", rank)}) + handler.HandleResponse(ctx, errors.Forbidden(reason.VoteRankFailToMeetTheCondition).WithMsg(msg), nil) return } diff --git a/internal/entity/revision_entity.go b/internal/entity/revision_entity.go index 46bfd228..34edc7f8 100644 --- a/internal/entity/revision_entity.go +++ b/internal/entity/revision_entity.go @@ -5,6 +5,8 @@ import ( ) const ( + // RevisioNnormalStatus this revision is Nnormal + RevisioNnormalStatus = 0 // RevisionUnreviewedStatus this revision is unreviewed RevisionUnreviewedStatus = 1 // RevisionReviewPassStatus this revision is reviewed and approved by operator diff --git a/internal/repo/tag_common/tag_common_repo.go b/internal/repo/tag_common/tag_common_repo.go index 0c357c32..02e1c6ae 100644 --- a/internal/repo/tag_common/tag_common_repo.go +++ b/internal/repo/tag_common/tag_common_repo.go @@ -2,6 +2,7 @@ package tag_common import ( "context" + "fmt" "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/pager" @@ -45,7 +46,7 @@ func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tag // GetTagBySlugName get tag by slug name func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) { tagInfo = &entity.Tag{} - session := tr.data.DB.Where("slug_name = ?", slugName) + session := tr.data.DB.Where("slug_name = LOWER(?)", slugName) session.Where(builder.Eq{"status": entity.TagStatusAvailable}) exist, err = session.Get(tagInfo) if err != nil { @@ -60,7 +61,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 ? or display_name LIKE ?", name+"%", name+"%") + session.Where("slug_name LIKE LOWER(?) or display_name LIKE ?", name+"%", name+"%") } else { session.UseBool("recommend") cond.Recommend = true @@ -106,6 +107,7 @@ func (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*ent // GetTagListByNames get tag list all like name func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) { + tagList = make([]*entity.Tag, 0) session := tr.data.DB.In("slug_name", names).UseBool("recommend", "reserved") // session.Where(builder.Eq{"status": entity.TagStatusAvailable}) @@ -140,7 +142,7 @@ func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag session := tr.data.DB.NewSession() if len(tag.SlugName) > 0 { - session.Where(builder.Or(builder.Like{"slug_name", tag.SlugName}, builder.Like{"display_name", tag.SlugName})) + session.Where(builder.Or(builder.Like{"slug_name", fmt.Sprintf("LOWER(%s)", tag.SlugName)}, builder.Like{"display_name", tag.SlugName})) tag.SlugName = "" } session.Where(builder.Eq{"status": entity.TagStatusAvailable}) diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index dc3c271d..3ff5f873 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -186,6 +186,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { // question r.POST("/question", a.questionController.AddQuestion) + r.POST("/question/answer", a.questionController.AddQuestionByAnswer) r.PUT("/question", a.questionController.UpdateQuestion) r.DELETE("/question", a.questionController.RemoveQuestion) r.PUT("/question/status", a.questionController.CloseQuestion) diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index 1ed09010..88afed90 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -27,6 +27,7 @@ type DashboardInfo struct { type DashboardInfoVersion struct { Version string `json:"version"` + Revision string `json:"revision"` RemoteVersion string `json:"remote_version"` } diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index c743577b..19a2c3fd 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -63,6 +63,33 @@ func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err erro return nil, nil } +type QuestionAddByAnswer struct { + // question title + Title string `validate:"required,notblank,gte=6,lte=150" json:"title"` + // content + Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` + // html + HTML string `json:"-"` + AnswerContent string `validate:"required,notblank,gte=6,lte=65535" json:"answer_content"` + AnswerHTML string `json:"-"` + // tags + Tags []*TagItem `validate:"required,dive" json:"tags"` + // user id + UserID string `json:"-"` + QuestionPermission +} + +func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField, err error) { + req.HTML = converter.Markdown2HTML(req.Content) + req.AnswerHTML = converter.Markdown2HTML(req.AnswerContent) + for _, tag := range req.Tags { + if len(tag.OriginalText) > 0 { + tag.ParsedText = converter.Markdown2HTML(tag.OriginalText) + } + } + return nil, nil +} + type QuestionPermission struct { // whether user can add it CanAdd bool `json:"-"` diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 19f3d9fb..4f786009 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -170,6 +170,7 @@ type SiteInfoResp struct { CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"` SiteSeo *SiteSeoReq `json:"site_seo"` Version string `json:"version"` + Revision string `json:"revision"` } type TemplateSiteInfoResp struct { General *SiteGeneralResp `json:"general"` @@ -225,6 +226,7 @@ type GetSMTPConfigResp struct { type GetManifestJsonResp struct { ManifestVersion int `json:"manifest_version"` Version string `json:"version"` + Revision string `json:"revision"` ShortName string `json:"short_name"` Name string `json:"name"` Icons map[string]string `json:"icons"` diff --git a/internal/service/activity/activity.go b/internal/service/activity/activity.go index 4fe151b9..f971583b 100644 --- a/internal/service/activity/activity.go +++ b/internal/service/activity/activity.go @@ -18,6 +18,7 @@ import ( usercommon "github.com/answerdev/answer/internal/service/user_common" "github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/pkg/obj" + "github.com/answerdev/answer/pkg/uid" "github.com/segmentfault/pacman/log" ) @@ -91,6 +92,10 @@ func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.Ge item.CancelledAt = act.CancelledAt.Unix() } + if item.ObjectType == constant.QuestionObjectType || item.ObjectType == constant.AnswerObjectType { + item.ObjectID = uid.EnShortID(act.ObjectID) + } + // database save activity type is number, change to activity type string is like "question.asked". // so we need to cut the front part of '.' _, item.ActivityType, _ = strings.Cut(config.ID2KeyMapping[act.ActivityType], ".") diff --git a/internal/service/answer_service.go b/internal/service/answer_service.go index 38e0826f..39433b81 100644 --- a/internal/service/answer_service.go +++ b/internal/service/answer_service.go @@ -326,6 +326,7 @@ func (as *AnswerService) UpdateAccepted(ctx context.Context, req *schema.AnswerA if err != nil { return err } + newAnswerInfo.ID = uid.DeShortID(newAnswerInfo.ID) if !newAnswerInfoexist { return errors.BadRequest(reason.AnswerNotFound) } @@ -335,12 +336,13 @@ func (as *AnswerService) UpdateAccepted(ctx context.Context, req *schema.AnswerA if err != nil { return err } + questionInfo.ID = uid.DeShortID(questionInfo.ID) if !exist { return errors.BadRequest(reason.QuestionNotFound) } - if questionInfo.UserID != req.UserID { - return fmt.Errorf("no permission to set answer") - } + // if questionInfo.UserID != req.UserID { + // return fmt.Errorf("no permission to set answer") + // } if questionInfo.AcceptedAnswerID == req.AnswerID { return nil } @@ -351,6 +353,7 @@ func (as *AnswerService) UpdateAccepted(ctx context.Context, req *schema.AnswerA if err != nil { return err } + oldAnswerInfo.ID = uid.DeShortID(oldAnswerInfo.ID) } err = as.answerRepo.UpdateAccepted(ctx, req.AnswerID, req.QuestionID) diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index 38595229..1b65c48d 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -20,7 +20,6 @@ import ( "github.com/answerdev/answer/pkg/encryption" "github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/pkg/uid" - "github.com/davecgh/go-spew/spew" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" @@ -448,7 +447,6 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s if err != nil { log.Error(err) } else { - spew.Dump("==", objInfo) commentResp.ObjectType = objInfo.ObjectType commentResp.Title = objInfo.Title commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title) diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index e47645fe..d8fcc8cb 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -90,6 +90,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 + dashboardInfo.VersionInfo.Revision = constant.Revision return dashboardInfo, nil } @@ -194,6 +195,7 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) dashboardInfo.TimeZone = siteInfoInterface.TimeZone dashboardInfo.VersionInfo.Version = constant.Version + dashboardInfo.VersionInfo.Revision = constant.Revision dashboardInfo.VersionInfo.RemoteVersion = ds.RemoteVersion(ctx) return dashboardInfo, nil } diff --git a/internal/service/object_info/object_info.go b/internal/service/object_info/object_info.go index ab961d26..5fbf55f7 100644 --- a/internal/service/object_info/object_info.go +++ b/internal/service/object_info/object_info.go @@ -11,6 +11,7 @@ import ( questioncommon "github.com/answerdev/answer/internal/service/question_common" tagcommon "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/pkg/obj" + "github.com/answerdev/answer/pkg/uid" "github.com/segmentfault/pacman/errors" ) @@ -50,6 +51,7 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st if err != nil { return nil, err } + questionInfo.ID = uid.EnShortID(questionInfo.ID) if !exist { break } @@ -85,6 +87,7 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st if !exist { break } + questionInfo.ID = uid.EnShortID(questionInfo.ID) objInfo = &schema.UnreviewedRevisionInfoInfo{ ObjectID: answerInfo.ID, Title: questionInfo.Title, diff --git a/internal/service/permission/answer_permission.go b/internal/service/permission/answer_permission.go index 57913c99..7a1a6eb5 100644 --- a/internal/service/permission/answer_permission.go +++ b/internal/service/permission/answer_permission.go @@ -3,24 +3,27 @@ package permission import ( "context" + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/schema" ) // GetAnswerPermission get answer permission func GetAnswerPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) ( actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if len(userID) > 0 { actions = append(actions, &schema.PermissionMemberAction{ Action: "report", - Name: "Flag", + Name: translator.Tr(lang, reportActionName), Type: "reason", }) } if canEdit || userID == creatorUserID { actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", - Name: "Edit", + Name: translator.Tr(lang, editActionName), Type: "edit", }) } @@ -28,7 +31,7 @@ func GetAnswerPermission(ctx context.Context, userID string, creatorUserID strin if canDelete || userID == creatorUserID { actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", - Name: "Delete", + Name: translator.Tr(lang, deleteActionName), Type: "confirm", }) } diff --git a/internal/service/permission/comment_permission.go b/internal/service/permission/comment_permission.go index 7f8839ae..d721c1d6 100644 --- a/internal/service/permission/comment_permission.go +++ b/internal/service/permission/comment_permission.go @@ -5,17 +5,20 @@ import ( "time" "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/schema" ) // GetCommentPermission get comment permission func GetCommentPermission(ctx context.Context, userID string, creatorUserID string, createdAt time.Time, canEdit, canDelete bool) (actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if len(userID) > 0 { actions = append(actions, &schema.PermissionMemberAction{ Action: "report", - Name: "Flag", + Name: translator.Tr(lang, reportActionName), Type: "reason", }) } @@ -23,7 +26,7 @@ func GetCommentPermission(ctx context.Context, userID string, creatorUserID stri if canEdit || (userID == creatorUserID && time.Now().Before(deadline)) { actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", - Name: "Edit", + Name: translator.Tr(lang, editActionName), Type: "edit", }) } @@ -31,7 +34,7 @@ func GetCommentPermission(ctx context.Context, userID string, creatorUserID stri if canDelete || userID == creatorUserID { actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", - Name: "Delete", + Name: translator.Tr(lang, deleteActionName), Type: "reason", }) } diff --git a/internal/service/permission/permission_name.go b/internal/service/permission/permission_name.go index 45c91278..4a62ec86 100644 --- a/internal/service/permission/permission_name.go +++ b/internal/service/permission/permission_name.go @@ -36,3 +36,11 @@ const ( TagAudit = "tag.audit" TagUseReservedTag = "tag.use_reserved_tag" ) + +const ( + reportActionName = "action.report" + editActionName = "action.edit" + deleteActionName = "action.delete" + closeActionName = "action.close" + reopenActionName = "action.reopen" +) diff --git a/internal/service/permission/question_permission.go b/internal/service/permission/question_permission.go index 4d1e1a42..1321af45 100644 --- a/internal/service/permission/question_permission.go +++ b/internal/service/permission/question_permission.go @@ -3,6 +3,8 @@ package permission import ( "context" + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/schema" ) @@ -10,39 +12,40 @@ import ( func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete, canClose, canReopen bool) ( actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if len(userID) > 0 { actions = append(actions, &schema.PermissionMemberAction{ Action: "report", - Name: "Flag", + Name: translator.Tr(lang, reportActionName), Type: "reason", }) } if canEdit || userID == creatorUserID { actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", - Name: "Edit", + Name: translator.Tr(lang, editActionName), Type: "edit", }) } if canClose { actions = append(actions, &schema.PermissionMemberAction{ Action: "close", - Name: "Close", + Name: translator.Tr(lang, closeActionName), Type: "confirm", }) } if canReopen { actions = append(actions, &schema.PermissionMemberAction{ Action: "reopen", - Name: "Reopen", + Name: translator.Tr(lang, reopenActionName), Type: "confirm", }) } if canDelete || userID == creatorUserID { actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", - Name: "Delete", + Name: translator.Tr(lang, deleteActionName), Type: "confirm", }) } diff --git a/internal/service/permission/tag_permission.go b/internal/service/permission/tag_permission.go index b4b156b4..d06ea92b 100644 --- a/internal/service/permission/tag_permission.go +++ b/internal/service/permission/tag_permission.go @@ -3,17 +3,20 @@ package permission import ( "context" + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/schema" ) // GetTagPermission get tag permission func GetTagPermission(ctx context.Context, canEdit, canDelete bool) ( actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if canEdit { actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", - Name: "Edit", + Name: translator.Tr(lang, editActionName), Type: "edit", }) } @@ -21,7 +24,7 @@ func GetTagPermission(ctx context.Context, canEdit, canDelete bool) ( if canDelete { actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", - Name: "Delete", + Name: translator.Tr(lang, deleteActionName), Type: "reason", }) } @@ -31,11 +34,12 @@ func GetTagPermission(ctx context.Context, canEdit, canDelete bool) ( // GetTagSynonymPermission get tag synonym permission func GetTagSynonymPermission(ctx context.Context, canEdit bool) ( actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if canEdit { actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", - Name: "Edit", + Name: translator.Tr(lang, editActionName), Type: "edit", }) } diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index 8e33c43e..a0d039fc 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -335,12 +335,15 @@ func (qs *QuestionCommon) FormatQuestionsPage( } else { item.Tags = make([]*schema.TagResp, 0) } - userInfo := userInfoMap[item.Operator.ID] - if userInfo != nil { - item.Operator.DisplayName = userInfo.DisplayName - item.Operator.Username = userInfo.Username - item.Operator.Rank = userInfo.Rank + userInfo, ok := userInfoMap[item.Operator.ID] + if ok { + if userInfo != nil { + item.Operator.DisplayName = userInfo.DisplayName + item.Operator.Username = userInfo.Username + item.Operator.Rank = userInfo.Rank + } } + } return formattedQuestions, nil } @@ -414,6 +417,11 @@ func (qs *QuestionCommon) RemoveQuestion(ctx context.Context, req *schema.Remove if !has { return nil } + + if questionInfo.Status == entity.QuestionStatusDeleted { + return nil + } + questionInfo.Status = entity.QuestionStatusDeleted err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo) if err != nil { diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 8193eeaa..5c4b2f3f 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -572,6 +572,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest // It's not you or the administrator that needs to be reviewed if !canUpdate { revisionDTO.Status = entity.RevisionUnreviewedStatus + revisionDTO.UserID = req.UserID //use revision userid } else { //Direct modification revisionDTO.Status = entity.RevisionReviewPassStatus diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index c1a418c1..c136838e 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -85,13 +85,42 @@ func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.Searc return } ts.TagsFormatRecommendAndReserved(ctx, tags) + mainTagId := make([]string, 0) for _, tag := range tags { - item := schema.SearchTagLikeResp{} - item.SlugName = tag.SlugName - item.DisplayName = tag.DisplayName - item.Recommend = tag.Recommend - item.Reserved = tag.Reserved - resp = append(resp, item) + if tag.MainTagID != 0 { + mainTagId = append(mainTagId, converter.IntToString(tag.MainTagID)) + } + } + mainTagList, err := ts.tagCommonRepo.GetTagListByIDs(ctx, mainTagId) + if err != nil { + return + } + mainTagMap := make(map[string]*entity.Tag) + for _, tag := range mainTagList { + mainTagMap[tag.ID] = tag + } + for _, tag := range tags { + if tag.MainTagID != 0 { + _, ok := mainTagMap[converter.IntToString(tag.MainTagID)] + if ok { + tag.SlugName = mainTagMap[converter.IntToString(tag.MainTagID)].SlugName + tag.DisplayName = mainTagMap[converter.IntToString(tag.MainTagID)].DisplayName + tag.Reserved = mainTagMap[converter.IntToString(tag.MainTagID)].Reserved + tag.Recommend = mainTagMap[converter.IntToString(tag.MainTagID)].Recommend + } + } + } + RepetitiveTag := make(map[string]bool) + for _, tag := range tags { + if _, ok := RepetitiveTag[tag.SlugName]; !ok { + item := schema.SearchTagLikeResp{} + item.SlugName = tag.SlugName + item.DisplayName = tag.DisplayName + item.Recommend = tag.Recommend + item.Reserved = tag.Reserved + resp = append(resp, item) + RepetitiveTag[tag.SlugName] = true + } } return resp, nil } @@ -233,8 +262,10 @@ func (ts *TagCommonService) AddTag(ctx context.Context, req *schema.AddTagReq) ( if exist { return nil, errors.BadRequest(reason.TagAlreadyExist) } + SlugName := strings.ReplaceAll(req.SlugName, " ", "-") + SlugName = strings.ToLower(SlugName) tagInfo := &entity.Tag{ - SlugName: strings.ReplaceAll(req.SlugName, " ", "-"), + SlugName: SlugName, DisplayName: req.DisplayName, OriginalText: req.OriginalText, ParsedText: req.ParsedText, @@ -535,7 +566,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) } diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index f438e7dd..980e82e2 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "mime/multipart" "net/http" "os" @@ -19,6 +20,7 @@ import ( "github.com/answerdev/answer/pkg/uid" "github.com/disintegration/imaging" "github.com/gin-gonic/gin" + exifremove "github.com/scottleedavis/go-exif-remove" "github.com/segmentfault/pacman/errors" ) @@ -192,6 +194,7 @@ func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } defer src.Close() + Dexif(filePath, filePath) if !checker.IsSupportedImageFile(src, filepath.Ext(fileSubPath)) { return "", errors.BadRequest(reason.UploadFileUnsupportedFileFormat) @@ -200,3 +203,19 @@ func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath) return url, nil } + +func Dexif(filepath string, destpath string) error { + img, err := ioutil.ReadFile(filepath) + if err != nil { + return err + } + noExifBytes, err := exifremove.Remove(img) + if err != nil { + return err + } + err = os.WriteFile(destpath, noExifBytes, 0644) + if err != nil { + return err + } + return nil +} diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 5aef83ef..f6a6f093 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -535,37 +535,66 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. } // UserChangeEmailVerify user change email verify code -func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string) (err error) { +func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string) (resp *schema.GetUserResp, err error) { data := &schema.EmailCodeContent{} err = data.FromJSONString(content) if err != nil { - return errors.BadRequest(reason.EmailVerifyURLExpired) + return nil, errors.BadRequest(reason.EmailVerifyURLExpired) } _, exist, err := us.userRepo.GetByEmail(ctx, data.Email) if err != nil { - return err + return nil, err } if exist { - return errors.BadRequest(reason.EmailDuplicate) + return nil, errors.BadRequest(reason.EmailDuplicate) } - _, exist, err = us.userRepo.GetByUserID(ctx, data.UserID) + userInfo, exist, err := us.userRepo.GetByUserID(ctx, data.UserID) if err != nil { - return err + return nil, err } if !exist { - return errors.BadRequest(reason.UserNotFound) + return nil, errors.BadRequest(reason.UserNotFound) } err = us.userRepo.UpdateEmail(ctx, data.UserID, data.Email) if err != nil { - return errors.BadRequest(reason.UserNotFound) + return nil, errors.BadRequest(reason.UserNotFound) } err = us.userRepo.UpdateEmailStatus(ctx, data.UserID, entity.EmailStatusAvailable) if err != nil { - return err + return nil, err } - return nil + + roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID) + if err != nil { + log.Error(err) + } + + resp = &schema.GetUserResp{} + resp.GetFromUserEntity(userInfo) + userCacheInfo := &entity.UserCacheInfo{ + UserID: userInfo.ID, + EmailStatus: entity.EmailStatusAvailable, + UserStatus: userInfo.Status, + RoleID: roleID, + } + resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) + if err != nil { + return nil, err + } + // User verified email will update user email status. So user status cache should be updated. + if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil { + return nil, err + } + resp.RoleID = userCacheInfo.RoleID + if resp.RoleID == role.RoleAdminID { + err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID}) + if err != nil { + return nil, err + } + } + return resp, nil } // getSiteUrl get site url diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go index f3eef836..d066b030 100644 --- a/pkg/converter/markdown.go +++ b/pkg/converter/markdown.go @@ -34,6 +34,10 @@ func Markdown2HTML(source string) string { } html := buf.String() filter := bluemonday.UGCPolicy() + filter.AllowStyling() + filter.RequireNoFollowOnLinks(false) + filter.RequireParseableURLs(false) + filter.RequireNoFollowOnFullyQualifiedLinks(false) html = filter.Sanitize(html) return html } @@ -110,6 +114,7 @@ func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node n := node.(*ast.Link) if entering && r.renderLinkIsUrl(string(n.Destination)) { _, _ = w.WriteString("=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + /@babel/compat-data/7.19.1: resolution: {integrity: sha512-72a9ghR0gnESIa7jBN53U32FOVCEoztyIlKaNoU05zRhEecduGK9L9c3ww7Mp06JiR+0ls0GBPFJQwwtjn9ksg==} engines: {node: '>=6.9.0'} - /@babel/compat-data/7.20.14: - resolution: {integrity: sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw==} + /@babel/compat-data/7.21.4: + resolution: {integrity: sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==} engines: {node: '>=6.9.0'} /@babel/core/7.19.1: @@ -204,20 +206,20 @@ packages: transitivePeerDependencies: - supports-color - /@babel/core/7.20.12: - resolution: {integrity: sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==} + /@babel/core/7.21.4: + resolution: {integrity: sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.14 - '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 - '@babel/helper-module-transforms': 7.20.11 - '@babel/helpers': 7.20.13 - '@babel/parser': 7.20.15 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.4 + '@babel/helper-compilation-targets': 7.21.4_@babel+core@7.21.4 + '@babel/helper-module-transforms': 7.21.2 + '@babel/helpers': 7.21.0 + '@babel/parser': 7.21.4 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.13 - '@babel/types': 7.20.7 + '@babel/traverse': 7.21.4 + '@babel/types': 7.21.4 convert-source-map: 1.9.0 debug: 4.3.4 gensync: 1.0.0-beta.2 @@ -247,12 +249,13 @@ packages: '@jridgewell/gen-mapping': 0.3.2 jsesc: 2.5.2 - /@babel/generator/7.20.14: - resolution: {integrity: sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==} + /@babel/generator/7.21.4: + resolution: {integrity: sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.4 '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.17 jsesc: 2.5.2 /@babel/helper-annotate-as-pure/7.18.6: @@ -280,15 +283,15 @@ packages: browserslist: 4.21.4 semver: 6.3.0 - /@babel/helper-compilation-targets/7.20.7_@babel+core@7.20.12: - resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} + /@babel/helper-compilation-targets/7.21.4_@babel+core@7.21.4: + resolution: {integrity: sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/compat-data': 7.20.14 - '@babel/core': 7.20.12 - '@babel/helper-validator-option': 7.18.6 + '@babel/compat-data': 7.21.4 + '@babel/core': 7.21.4 + '@babel/helper-validator-option': 7.21.0 browserslist: 4.21.5 lru-cache: 5.1.1 semver: 6.3.0 @@ -352,6 +355,13 @@ packages: '@babel/template': 7.18.10 '@babel/types': 7.19.0 + /@babel/helper-function-name/7.21.0: + resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.20.7 + '@babel/types': 7.21.4 + /@babel/helper-hoist-variables/7.18.6: resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} engines: {node: '>=6.9.0'} @@ -370,6 +380,12 @@ packages: dependencies: '@babel/types': 7.19.0 + /@babel/helper-module-imports/7.21.4: + resolution: {integrity: sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.4 + /@babel/helper-module-transforms/7.19.0: resolution: {integrity: sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==} engines: {node: '>=6.9.0'} @@ -385,18 +401,18 @@ packages: transitivePeerDependencies: - supports-color - /@babel/helper-module-transforms/7.20.11: - resolution: {integrity: sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==} + /@babel/helper-module-transforms/7.21.2: + resolution: {integrity: sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.18.6 + '@babel/helper-module-imports': 7.21.4 '@babel/helper-simple-access': 7.20.2 '@babel/helper-split-export-declaration': 7.18.6 '@babel/helper-validator-identifier': 7.19.1 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.13 - '@babel/types': 7.20.7 + '@babel/traverse': 7.21.4 + '@babel/types': 7.21.4 transitivePeerDependencies: - supports-color @@ -450,7 +466,7 @@ packages: resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.4 /@babel/helper-skip-transparent-expression-wrappers/7.18.9: resolution: {integrity: sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==} @@ -480,6 +496,10 @@ packages: resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} engines: {node: '>=6.9.0'} + /@babel/helper-validator-option/7.21.0: + resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} + engines: {node: '>=6.9.0'} + /@babel/helper-wrap-function/7.19.0: resolution: {integrity: sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==} engines: {node: '>=6.9.0'} @@ -501,13 +521,13 @@ packages: transitivePeerDependencies: - supports-color - /@babel/helpers/7.20.13: - resolution: {integrity: sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==} + /@babel/helpers/7.21.0: + resolution: {integrity: sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==} engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.20.7 - '@babel/traverse': 7.20.13 - '@babel/types': 7.20.7 + '@babel/traverse': 7.21.4 + '@babel/types': 7.21.4 transitivePeerDependencies: - supports-color @@ -526,12 +546,12 @@ packages: dependencies: '@babel/types': 7.19.0 - /@babel/parser/7.20.15: - resolution: {integrity: sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==} + /@babel/parser/7.21.4: + resolution: {integrity: sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.4 /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.19.1: resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} @@ -804,14 +824,14 @@ packages: '@babel/core': 7.19.1 '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-flow/7.18.6_@babel+core@7.20.12: - resolution: {integrity: sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==} + /@babel/plugin-syntax-flow/7.21.4_@babel+core@7.21.4: + resolution: {integrity: sha512-l9xd3N+XG4fZRxEP3vXdK6RW7vN1Uf5dxzRC/09wV86wqZ/YYQooBIGNsiRdfNR3/q2/5pPzV4B54J/9ctX5jw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/core': 7.21.4 + '@babel/helper-plugin-utils': 7.20.2 /@babel/plugin-syntax-import-assertions/7.18.6_@babel+core@7.19.1: resolution: {integrity: sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==} @@ -847,14 +867,14 @@ packages: '@babel/core': 7.19.1 '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.20.12: - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} + /@babel/plugin-syntax-jsx/7.21.4_@babel+core@7.21.4: + resolution: {integrity: sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/core': 7.21.4 + '@babel/helper-plugin-utils': 7.20.2 /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.19.1: resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} @@ -1228,18 +1248,18 @@ packages: '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.19.1 '@babel/types': 7.19.0 - /@babel/plugin-transform-react-jsx/7.20.13_@babel+core@7.20.12: - resolution: {integrity: sha512-MmTZx/bkUrfJhhYAYt3Urjm+h8DQGrPrnKQ94jLo7NLuOU+T89a7IByhKmrb8SKhrIYIQ0FN0CHMbnFRen4qNw==} + /@babel/plugin-transform-react-jsx/7.21.0_@babel+core@7.21.4: + resolution: {integrity: sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 + '@babel/core': 7.21.4 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 + '@babel/helper-module-imports': 7.21.4 '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.12 - '@babel/types': 7.20.7 + '@babel/plugin-syntax-jsx': 7.21.4_@babel+core@7.21.4 + '@babel/types': 7.21.4 /@babel/plugin-transform-react-pure-annotations/7.18.6_@babel+core@7.19.1: resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} @@ -1513,9 +1533,9 @@ packages: resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/parser': 7.20.15 - '@babel/types': 7.20.7 + '@babel/code-frame': 7.21.4 + '@babel/parser': 7.21.4 + '@babel/types': 7.21.4 /@babel/traverse/7.19.1: resolution: {integrity: sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA==} @@ -1534,18 +1554,18 @@ packages: transitivePeerDependencies: - supports-color - /@babel/traverse/7.20.13: - resolution: {integrity: sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==} + /@babel/traverse/7.21.4: + resolution: {integrity: sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.14 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.4 '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 + '@babel/helper-function-name': 7.21.0 '@babel/helper-hoist-variables': 7.18.6 '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.20.15 - '@babel/types': 7.20.7 + '@babel/parser': 7.21.4 + '@babel/types': 7.21.4 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: @@ -1559,8 +1579,8 @@ packages: '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 - /@babel/types/7.20.7: - resolution: {integrity: sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==} + /@babel/types/7.21.4: + resolution: {integrity: sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.19.4 @@ -1659,11 +1679,11 @@ packages: '@types/node': 14.18.29 chalk: 4.1.2 cosmiconfig: 7.0.1 - cosmiconfig-typescript-loader: 4.1.0_3owiowz3ujipd4k6pbqn3n7oui + cosmiconfig-typescript-loader: 4.1.0_2uclxasecupgvdn72amnhmyg7y lodash: 4.17.21 resolve-from: 5.0.0 - ts-node: 10.9.1_ao52im6kiihokc7tdj7weudhra - typescript: 4.8.3 + ts-node: 10.9.1_5bkdw6noa5sa7givrguqy7ejvm + typescript: 4.9.5 transitivePeerDependencies: - '@swc/core' - '@swc/wasm' @@ -2208,6 +2228,12 @@ packages: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + /@jridgewell/trace-mapping/0.3.17: + resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + /@jridgewell/trace-mapping/0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: @@ -2283,6 +2309,10 @@ packages: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} dev: false + /@popperjs/core/2.11.7: + resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==} + dev: false + /@react-aria/ssr/3.3.0_react@18.2.0: resolution: {integrity: sha512-yNqUDuOVZIUGP81R87BJVi/ZUZp/nYOBXbPsRe7oltJOfErQZD+UezMpw4vM2KRz18cURffvmC8tJ6JTeyDtaQ==} peerDependencies: @@ -2821,7 +2851,7 @@ packages: dependencies: '@types/yargs-parser': 21.0.0 - /@typescript-eslint/eslint-plugin/5.38.0_wsb62dxj2oqwgas4kadjymcmry: + /@typescript-eslint/eslint-plugin/5.38.0_j3kyhwzdxzxnwkyezapk4ib6dm: resolution: {integrity: sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2832,33 +2862,33 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/parser': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy '@typescript-eslint/scope-manager': 5.38.0 - '@typescript-eslint/type-utils': 5.38.0_irgkl5vooow2ydyo6aokmferha - '@typescript-eslint/utils': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/type-utils': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy + '@typescript-eslint/utils': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy debug: 4.3.4 eslint: 8.23.1 ignore: 5.2.0 regexpp: 3.2.0 semver: 7.3.8 - tsutils: 3.21.0_typescript@4.8.3 - typescript: 4.8.3 + tsutils: 3.21.0_typescript@4.9.5 + typescript: 4.9.5 transitivePeerDependencies: - supports-color - /@typescript-eslint/experimental-utils/5.38.0_irgkl5vooow2ydyo6aokmferha: + /@typescript-eslint/experimental-utils/5.38.0_4dcepvmun56gjdctx7q2wwbdvy: resolution: {integrity: sha512-kzXBRfvGlicgGk4CYuRUqKvwc2s3wHXNssUWWJU18bhMRxriFm3BZWyQ6vEHBRpEIMKB6b7MIQHO+9lYlts19w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/utils': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy eslint: 8.23.1 transitivePeerDependencies: - supports-color - typescript - /@typescript-eslint/parser/5.38.0_irgkl5vooow2ydyo6aokmferha: + /@typescript-eslint/parser/5.38.0_4dcepvmun56gjdctx7q2wwbdvy: resolution: {integrity: sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2870,10 +2900,10 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.38.0 '@typescript-eslint/types': 5.38.0 - '@typescript-eslint/typescript-estree': 5.38.0_typescript@4.8.3 + '@typescript-eslint/typescript-estree': 5.38.0_typescript@4.9.5 debug: 4.3.4 eslint: 8.23.1 - typescript: 4.8.3 + typescript: 4.9.5 transitivePeerDependencies: - supports-color @@ -2884,7 +2914,7 @@ packages: '@typescript-eslint/types': 5.38.0 '@typescript-eslint/visitor-keys': 5.38.0 - /@typescript-eslint/type-utils/5.38.0_irgkl5vooow2ydyo6aokmferha: + /@typescript-eslint/type-utils/5.38.0_4dcepvmun56gjdctx7q2wwbdvy: resolution: {integrity: sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2894,12 +2924,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.38.0_typescript@4.8.3 - '@typescript-eslint/utils': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/typescript-estree': 5.38.0_typescript@4.9.5 + '@typescript-eslint/utils': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy debug: 4.3.4 eslint: 8.23.1 - tsutils: 3.21.0_typescript@4.8.3 - typescript: 4.8.3 + tsutils: 3.21.0_typescript@4.9.5 + typescript: 4.9.5 transitivePeerDependencies: - supports-color @@ -2907,7 +2937,7 @@ packages: resolution: {integrity: sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - /@typescript-eslint/typescript-estree/5.38.0_typescript@4.8.3: + /@typescript-eslint/typescript-estree/5.38.0_typescript@4.9.5: resolution: {integrity: sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2922,12 +2952,12 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0_typescript@4.8.3 - typescript: 4.8.3 + tsutils: 3.21.0_typescript@4.9.5 + typescript: 4.9.5 transitivePeerDependencies: - supports-color - /@typescript-eslint/utils/5.38.0_irgkl5vooow2ydyo6aokmferha: + /@typescript-eslint/utils/5.38.0_4dcepvmun56gjdctx7q2wwbdvy: resolution: {integrity: sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2936,7 +2966,7 @@ packages: '@types/json-schema': 7.0.11 '@typescript-eslint/scope-manager': 5.38.0 '@typescript-eslint/types': 5.38.0 - '@typescript-eslint/typescript-estree': 5.38.0_typescript@4.8.3 + '@typescript-eslint/typescript-estree': 5.38.0_typescript@4.9.5 eslint: 8.23.1 eslint-scope: 5.1.1 eslint-utils: 3.0.0_eslint@8.23.1 @@ -3606,12 +3636,12 @@ packages: resolution: {integrity: sha512-PTPYadRn1AMGr+QTSxe4ZCc+Wzv9DGZxbi3lNse/dajqV31n2/wl/7NX78ZpkvFgRNmH4ogdIQPQmxAfhEV6nA==} dev: false - /bootstrap/5.2.1_@popperjs+core@2.11.6: + /bootstrap/5.2.1_@popperjs+core@2.11.7: resolution: {integrity: sha512-UQi3v2NpVPEi1n35dmRRzBJFlgvWHYwyem6yHhuT6afYF+sziEt46McRbT//kVXZ7b1YUYEVGdXEH74Nx3xzGA==} peerDependencies: '@popperjs/core': ^2.11.6 dependencies: - '@popperjs/core': 2.11.6 + '@popperjs/core': 2.11.7 dev: false /brace-expansion/1.1.11: @@ -3649,8 +3679,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001452 - electron-to-chromium: 1.4.295 + caniuse-lite: 1.0.30001474 + electron-to-chromium: 1.4.350 node-releases: 2.0.10 update-browserslist-db: 1.0.10_browserslist@4.21.5 @@ -3728,8 +3758,8 @@ packages: /caniuse-lite/1.0.30001408: resolution: {integrity: sha512-DdUCktgMSM+1ndk9EFMZcavsGszV7zxV9O7MtOHniTa/iyAIwJCF0dFVBdU9SijJbfh29hC9bCs07wu8pjnGJQ==} - /caniuse-lite/1.0.30001452: - resolution: {integrity: sha512-Lkp0vFjMkBB3GTpLR8zk4NwW5EdRdnitwYJHDOOKIU85x4ckYCPQ+9WlVvSVClHxVReefkUMtWZH2l9KGlD51w==} + /caniuse-lite/1.0.30001474: + resolution: {integrity: sha512-iaIZ8gVrWfemh5DG3T9/YqarVZoYf0r188IjaGwx68j4Pf0SGY6CQkmJUIE+NZHkkecQGohzXmBGEwWDr9aM3Q==} /case-sensitive-paths-webpack-plugin/2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} @@ -4037,7 +4067,7 @@ packages: /core-util-is/1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - /cosmiconfig-typescript-loader/4.1.0_3owiowz3ujipd4k6pbqn3n7oui: + /cosmiconfig-typescript-loader/4.1.0_2uclxasecupgvdn72amnhmyg7y: resolution: {integrity: sha512-HbWIuR5O+XO5Oj9SZ5bzgrD4nN+rfhrm2PMb0FVx+t+XIvC45n8F0oTNnztXtspWGw0i2IzHaUWFD5LzV1JB4A==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -4048,8 +4078,8 @@ packages: dependencies: '@types/node': 14.18.29 cosmiconfig: 7.0.1 - ts-node: 10.9.1_ao52im6kiihokc7tdj7weudhra - typescript: 4.8.3 + ts-node: 10.9.1_5bkdw6noa5sa7givrguqy7ejvm + typescript: 4.9.5 dev: true /cosmiconfig/6.0.0: @@ -5066,8 +5096,8 @@ packages: /electron-to-chromium/1.4.256: resolution: {integrity: sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw==} - /electron-to-chromium/1.4.295: - resolution: {integrity: sha512-lEO94zqf1bDA3aepxwnWoHUjA8sZ+2owgcSZjYQy0+uOSEclJX0VieZC+r+wLpSxUHRd6gG32znTWmr+5iGzFw==} + /electron-to-chromium/1.4.350: + resolution: {integrity: sha512-XnXcWpVnOfHZ4C3NPiL+SubeoGV8zc/pg8GEubRtc1dPA/9jKS2vsOPmtClJHhWxUb2RSGC1OBLCbgNUJMtZPw==} /emittery/0.10.2: resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} @@ -5106,7 +5136,7 @@ packages: resolution: {integrity: sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==} engines: {node: '>=10.13.0'} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 tapable: 2.2.1 dev: true @@ -5221,8 +5251,8 @@ packages: eslint: ^7.32.0 || ^8.2.0 eslint-plugin-import: ^2.25.3 dependencies: - '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/eslint-plugin': 5.38.0_j3kyhwzdxzxnwkyezapk4ib6dm + '@typescript-eslint/parser': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy eslint: 8.23.1 eslint-config-airbnb-base: 15.0.0_hdzsmr7kawaomymueo2tso6fjq eslint-plugin-import: 2.26.0_cxqatnnjiq7ozd2bkspxnuicdq @@ -5257,7 +5287,7 @@ packages: eslint: 8.23.1 dev: true - /eslint-config-react-app/7.0.1_n6qatnnbpoccspltxi3idzpmam: + /eslint-config-react-app/7.0.1_3wjbtgciw64wc2xmhxyprqr3jm: resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -5270,19 +5300,19 @@ packages: '@babel/core': 7.19.1 '@babel/eslint-parser': 7.19.1_zdglor7vg7osicr5spasq6cc5a '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/eslint-plugin': 5.38.0_j3kyhwzdxzxnwkyezapk4ib6dm + '@typescript-eslint/parser': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy babel-preset-react-app: 10.0.1 confusing-browser-globals: 1.0.11 eslint: 8.23.1 - eslint-plugin-flowtype: 8.0.3_kpcs5p4zm4a4w6zxhbzwfayc7m + eslint-plugin-flowtype: 8.0.3_is75u4gug36f5e4hn2ov7dma7i eslint-plugin-import: 2.26.0_cxqatnnjiq7ozd2bkspxnuicdq - eslint-plugin-jest: 25.7.0_hpujes4m5fznz335nz2hgbshme + eslint-plugin-jest: 25.7.0_ysljusnkykxn4g7zog7cbtn2c4 eslint-plugin-jsx-a11y: 6.6.1_eslint@8.23.1 eslint-plugin-react: 7.31.8_eslint@8.23.1 eslint-plugin-react-hooks: 4.6.0_eslint@8.23.1 - eslint-plugin-testing-library: 5.6.4_irgkl5vooow2ydyo6aokmferha - typescript: 4.8.3 + eslint-plugin-testing-library: 5.6.4_4dcepvmun56gjdctx7q2wwbdvy + typescript: 4.9.5 transitivePeerDependencies: - '@babel/plugin-syntax-flow' - '@babel/plugin-transform-react-jsx' @@ -5291,7 +5321,7 @@ packages: - jest - supports-color - /eslint-config-standard-with-typescript/22.0.0_fsqc7gnfr7ufpr4slszrtm5abq: + /eslint-config-standard-with-typescript/22.0.0_rgg5nvqzvmbqsu6p3s4ukwchpy: resolution: {integrity: sha512-VA36U7UlFpwULvkdnh6MQj5GAV2Q+tT68ALLAwJP0ZuNXU2m0wX07uxX4qyLRdHgSzH4QJ73CveKBuSOYvh7vQ==} peerDependencies: '@typescript-eslint/eslint-plugin': ^5.0.0 @@ -5301,14 +5331,14 @@ packages: eslint-plugin-promise: ^6.0.0 typescript: '*' dependencies: - '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/eslint-plugin': 5.38.0_j3kyhwzdxzxnwkyezapk4ib6dm + '@typescript-eslint/parser': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy eslint: 8.23.1 eslint-config-standard: 17.0.0_4nulviyjkaspo7v2xlghuwxbf4 eslint-plugin-import: 2.26.0_cxqatnnjiq7ozd2bkspxnuicdq eslint-plugin-n: 15.2.5_eslint@8.23.1 eslint-plugin-promise: 6.0.1_eslint@8.23.1 - typescript: 4.8.3 + typescript: 4.9.5 transitivePeerDependencies: - supports-color dev: true @@ -5356,7 +5386,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/parser': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy debug: 3.2.7 eslint: 8.23.1 eslint-import-resolver-node: 0.3.6 @@ -5374,7 +5404,7 @@ packages: regexpp: 3.2.0 dev: true - /eslint-plugin-flowtype/8.0.3_kpcs5p4zm4a4w6zxhbzwfayc7m: + /eslint-plugin-flowtype/8.0.3_is75u4gug36f5e4hn2ov7dma7i: resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -5382,8 +5412,8 @@ packages: '@babel/plugin-transform-react-jsx': ^7.14.9 eslint: ^8.1.0 dependencies: - '@babel/plugin-syntax-flow': 7.18.6_@babel+core@7.20.12 - '@babel/plugin-transform-react-jsx': 7.20.13_@babel+core@7.20.12 + '@babel/plugin-syntax-flow': 7.21.4_@babel+core@7.21.4 + '@babel/plugin-transform-react-jsx': 7.21.0_@babel+core@7.21.4 eslint: 8.23.1 lodash: 4.17.21 string-natural-compare: 3.0.1 @@ -5398,7 +5428,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/parser': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy array-includes: 3.1.5 array.prototype.flat: 1.3.0 debug: 2.6.9 @@ -5418,7 +5448,7 @@ packages: - eslint-import-resolver-webpack - supports-color - /eslint-plugin-jest/25.7.0_hpujes4m5fznz335nz2hgbshme: + /eslint-plugin-jest/25.7.0_ysljusnkykxn4g7zog7cbtn2c4: resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} peerDependencies: @@ -5431,8 +5461,8 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry - '@typescript-eslint/experimental-utils': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/eslint-plugin': 5.38.0_j3kyhwzdxzxnwkyezapk4ib6dm + '@typescript-eslint/experimental-utils': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy eslint: 8.23.1 jest: 27.5.1_ts-node@10.9.1 transitivePeerDependencies: @@ -5533,13 +5563,13 @@ packages: semver: 6.3.0 string.prototype.matchall: 4.0.7 - /eslint-plugin-testing-library/5.6.4_irgkl5vooow2ydyo6aokmferha: + /eslint-plugin-testing-library/5.6.4_4dcepvmun56gjdctx7q2wwbdvy: resolution: {integrity: sha512-0oW3tC5NNT2WexmJ3848a/utawOymw4ibl3/NkwywndVAz2hT9+ab70imA7ccg3RaScQgMvJT60OL00hpmJvrg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} peerDependencies: eslint: ^7.5.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/utils': 5.38.0_4dcepvmun56gjdctx7q2wwbdvy eslint: 8.23.1 transitivePeerDependencies: - supports-color @@ -5912,7 +5942,7 @@ packages: debug: optional: true - /fork-ts-checker-webpack-plugin/6.5.2_npfwkgbcmgrbevrxnqgustqabe: + /fork-ts-checker-webpack-plugin/6.5.2_h33pxmj7ihkiaxhnzwyj3mdwbi: resolution: {integrity: sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==} engines: {node: '>=10', yarn: '>=1.0.0'} peerDependencies: @@ -5940,7 +5970,7 @@ packages: schema-utils: 2.7.0 semver: 7.3.8 tapable: 1.1.3 - typescript: 4.8.3 + typescript: 4.9.5 webpack: 5.74.0 /form-data/3.0.1: @@ -6131,6 +6161,10 @@ packages: /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + /graceful-fs/4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + /grapheme-splitter/1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} @@ -6787,7 +6821,7 @@ packages: pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1_ao52im6kiihokc7tdj7weudhra + ts-node: 10.9.1_5bkdw6noa5sa7givrguqy7ejvm transitivePeerDependencies: - bufferutil - canvas @@ -7787,7 +7821,7 @@ packages: jsonp: 0.2.1 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 - react-scripts: 5.0.1_mml7drqgt2b2zrp2ma74zmeoqy + react-scripts: 5.0.1_z72bcl2gkg6v3fmxqtnfgirxda transitivePeerDependencies: - supports-color dev: false @@ -8417,7 +8451,7 @@ packages: dependencies: lilconfig: 2.0.6 postcss: 8.4.16 - ts-node: 10.9.1_ao52im6kiihokc7tdj7weudhra + ts-node: 10.9.1_5bkdw6noa5sa7givrguqy7ejvm yaml: 1.10.2 /postcss-loader/6.2.1_qjv4cptcpse3y5hrjkrbb7drda: @@ -8950,13 +8984,13 @@ packages: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} engines: {node: '>=6'} - /purgecss-webpack-plugin/4.1.3_webpack@5.75.0: + /purgecss-webpack-plugin/4.1.3_webpack@5.77.0: resolution: {integrity: sha512-1OHS0WE935w66FjaFSlV06ycmn3/A8a6Q+iVUmmCYAujQ1HPdX+psMXUhASEW0uF1PYEpOlhMc5ApigVqYK08g==} peerDependencies: webpack: '*' dependencies: purgecss: 4.1.3 - webpack: 5.75.0 + webpack: 5.77.0 webpack-sources: 3.2.3 dev: true @@ -9042,7 +9076,7 @@ packages: peerDependencies: react-scripts: '>=2.1.3' dependencies: - react-scripts: 5.0.1_mml7drqgt2b2zrp2ma74zmeoqy + react-scripts: 5.0.1_z72bcl2gkg6v3fmxqtnfgirxda semver: 5.7.1 dev: true @@ -9073,7 +9107,7 @@ packages: warning: 4.0.3 dev: false - /react-dev-utils/12.0.1_npfwkgbcmgrbevrxnqgustqabe: + /react-dev-utils/12.0.1_h33pxmj7ihkiaxhnzwyj3mdwbi: resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} engines: {node: '>=14'} peerDependencies: @@ -9092,7 +9126,7 @@ packages: escape-string-regexp: 4.0.0 filesize: 8.0.7 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.2_npfwkgbcmgrbevrxnqgustqabe + fork-ts-checker-webpack-plugin: 6.5.2_h33pxmj7ihkiaxhnzwyj3mdwbi global-modules: 2.0.0 globby: 11.1.0 gzip-size: 6.0.0 @@ -9107,7 +9141,7 @@ packages: shell-quote: 1.7.3 strip-ansi: 6.0.1 text-table: 0.2.0 - typescript: 4.8.3 + typescript: 4.9.5 webpack: 5.74.0 transitivePeerDependencies: - eslint @@ -9205,7 +9239,7 @@ packages: react: 18.2.0 dev: false - /react-scripts/5.0.1_mml7drqgt2b2zrp2ma74zmeoqy: + /react-scripts/5.0.1_z72bcl2gkg6v3fmxqtnfgirxda: resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -9233,7 +9267,7 @@ packages: dotenv: 10.0.0 dotenv-expand: 5.1.0 eslint: 8.23.1 - eslint-config-react-app: 7.0.1_n6qatnnbpoccspltxi3idzpmam + eslint-config-react-app: 7.0.1_3wjbtgciw64wc2xmhxyprqr3jm eslint-webpack-plugin: 3.2.0_cnsurwdbw57xgwxuf5k544xt5e file-loader: 6.2.0_webpack@5.74.0 fs-extra: 10.1.0 @@ -9251,7 +9285,7 @@ packages: prompts: 2.4.2 react: 18.2.0 react-app-polyfill: 3.0.0 - react-dev-utils: 12.0.1_npfwkgbcmgrbevrxnqgustqabe + react-dev-utils: 12.0.1_h33pxmj7ihkiaxhnzwyj3mdwbi react-refresh: 0.11.0 resolve: 1.22.1 resolve-url-loader: 4.0.0 @@ -9261,7 +9295,7 @@ packages: style-loader: 3.3.1_webpack@5.74.0 tailwindcss: 3.1.8_57znarxsqwmnneadci5z5fd5gu terser-webpack-plugin: 5.3.6_webpack@5.74.0 - typescript: 4.8.3 + typescript: 4.9.5 webpack: 5.74.0 webpack-dev-server: 4.11.1_webpack@5.74.0 webpack-manifest-plugin: 4.1.1_webpack@5.74.0 @@ -9742,6 +9776,12 @@ packages: dependencies: randombytes: 2.1.0 + /serialize-javascript/6.0.1: + resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} + dependencies: + randombytes: 2.1.0 + dev: true + /serve-index/1.9.1: resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} engines: {node: '>= 0.8.0'} @@ -10291,8 +10331,8 @@ packages: terser: 5.15.0 webpack: 5.74.0 - /terser-webpack-plugin/5.3.6_webpack@5.75.0: - resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==} + /terser-webpack-plugin/5.3.7_webpack@5.77.0: + resolution: {integrity: sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -10307,12 +10347,12 @@ packages: uglify-js: optional: true dependencies: - '@jridgewell/trace-mapping': 0.3.15 + '@jridgewell/trace-mapping': 0.3.17 jest-worker: 27.5.1 schema-utils: 3.1.1 - serialize-javascript: 6.0.0 - terser: 5.15.0 - webpack: 5.75.0 + serialize-javascript: 6.0.1 + terser: 5.16.8 + webpack: 5.77.0 dev: true /terser/5.15.0: @@ -10325,6 +10365,17 @@ packages: commander: 2.20.3 source-map-support: 0.5.21 + /terser/5.16.8: + resolution: {integrity: sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.2 + acorn: 8.8.2 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + /test-exclude/6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -10406,7 +10457,7 @@ packages: /tryer/1.0.1: resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} - /ts-node/10.9.1_ao52im6kiihokc7tdj7weudhra: + /ts-node/10.9.1_5bkdw6noa5sa7givrguqy7ejvm: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -10432,7 +10483,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.8.3 + typescript: 4.9.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 @@ -10450,14 +10501,14 @@ packages: /tslib/2.4.0: resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - /tsutils/3.21.0_typescript@4.8.3: + /tsutils/3.21.0_typescript@4.9.5: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 4.8.3 + typescript: 4.9.5 /type-check/0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} @@ -10514,8 +10565,8 @@ packages: dependencies: is-typedarray: 1.0.0 - /typescript/4.8.3: - resolution: {integrity: sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==} + /typescript/4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} hasBin: true @@ -10853,8 +10904,8 @@ packages: - esbuild - uglify-js - /webpack/5.75.0: - resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==} + /webpack/5.77.0: + resolution: {integrity: sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -10877,14 +10928,14 @@ packages: eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 loader-runner: 4.3.0 mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 3.1.1 tapable: 2.2.1 - terser-webpack-plugin: 5.3.6_webpack@5.75.0 + terser-webpack-plugin: 5.3.7_webpack@5.77.0 watchpack: 2.4.0 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -11303,16 +11354,6 @@ packages: domhandler: registry.npmjs.org/domhandler/4.3.1 entities: registry.npmjs.org/entities/2.2.0 - registry.npmjs.org/dom-serializer/2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz} - name: dom-serializer - version: 2.0.0 - dependencies: - domelementtype: registry.npmjs.org/domelementtype/2.3.0 - domhandler: registry.npmjs.org/domhandler/5.0.3 - entities: registry.npmjs.org/entities/4.4.0 - dev: false - registry.npmjs.org/domelementtype/1.3.1: resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz} name: domelementtype @@ -11331,27 +11372,12 @@ packages: dependencies: domelementtype: registry.npmjs.org/domelementtype/2.3.0 - registry.npmjs.org/domhandler/5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz} - name: domhandler - version: 5.0.3 - engines: {node: '>= 4'} - dependencies: - domelementtype: registry.npmjs.org/domelementtype/2.3.0 - dev: false - registry.npmjs.org/dompurify/2.4.0: resolution: {integrity: sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz} name: dompurify version: 2.4.0 dev: false - registry.npmjs.org/dompurify/2.4.3: - resolution: {integrity: sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz} - name: dompurify - version: 2.4.3 - dev: false - registry.npmjs.org/domutils/1.7.0: resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz} name: domutils @@ -11369,52 +11395,11 @@ packages: domelementtype: registry.npmjs.org/domelementtype/2.3.0 domhandler: registry.npmjs.org/domhandler/4.3.1 - registry.npmjs.org/domutils/3.0.1: - resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz} - name: domutils - version: 3.0.1 - dependencies: - dom-serializer: registry.npmjs.org/dom-serializer/2.0.0 - domelementtype: registry.npmjs.org/domelementtype/2.3.0 - domhandler: registry.npmjs.org/domhandler/5.0.3 - dev: false - registry.npmjs.org/entities/2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/entities/-/entities-2.2.0.tgz} name: entities version: 2.2.0 - registry.npmjs.org/entities/4.4.0: - resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/entities/-/entities-4.4.0.tgz} - name: entities - version: 4.4.0 - engines: {node: '>=0.12'} - dev: false - - registry.npmjs.org/html-dom-parser/3.1.3: - resolution: {integrity: sha512-fI0yyNlIeSboxU+jnrA4v8qj4+M8SI9/q6AKYdwCY2qki22UtKCDTxvagHniECu7sa5/o2zFRdLleA67035lsA==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-3.1.3.tgz} - name: html-dom-parser - version: 3.1.3 - dependencies: - domhandler: registry.npmjs.org/domhandler/5.0.3 - htmlparser2: registry.npmjs.org/htmlparser2/8.0.1 - dev: false - - registry.npmjs.org/html-react-parser/3.0.8_react@18.2.0: - resolution: {integrity: sha512-eIxPq/3Ja3+nZkx6X/BNpFy0lNuW+v3V4nzABzuL8KkRVdYY/2KyXO42epKonsEVVRSBxm2zsxWcOkT6fYL+iQ==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/html-react-parser/-/html-react-parser-3.0.8.tgz} - id: registry.npmjs.org/html-react-parser/3.0.8 - name: html-react-parser - version: 3.0.8 - peerDependencies: - react: 0.14 || 15 || 16 || 17 || 18 - dependencies: - domhandler: registry.npmjs.org/domhandler/5.0.3 - html-dom-parser: registry.npmjs.org/html-dom-parser/3.1.3 - react: 18.2.0 - react-property: registry.npmjs.org/react-property/2.0.0 - style-to-js: registry.npmjs.org/style-to-js/1.1.3 - dev: false - registry.npmjs.org/htmlparser2/6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz} name: htmlparser2 @@ -11424,42 +11409,3 @@ packages: domhandler: registry.npmjs.org/domhandler/4.3.1 domutils: registry.npmjs.org/domutils/2.8.0 entities: registry.npmjs.org/entities/2.2.0 - - registry.npmjs.org/htmlparser2/8.0.1: - resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz} - name: htmlparser2 - version: 8.0.1 - dependencies: - domelementtype: registry.npmjs.org/domelementtype/2.3.0 - domhandler: registry.npmjs.org/domhandler/5.0.3 - domutils: registry.npmjs.org/domutils/3.0.1 - entities: registry.npmjs.org/entities/4.4.0 - dev: false - - registry.npmjs.org/inline-style-parser/0.1.1: - resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz} - name: inline-style-parser - version: 0.1.1 - dev: false - - registry.npmjs.org/react-property/2.0.0: - resolution: {integrity: sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz} - name: react-property - version: 2.0.0 - dev: false - - registry.npmjs.org/style-to-js/1.1.3: - resolution: {integrity: sha512-zKI5gN/zb7LS/Vm0eUwjmjrXWw8IMtyA8aPBJZdYiQTXj4+wQ3IucOLIOnF7zCHxvW8UhIGh/uZh/t9zEHXNTQ==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.3.tgz} - name: style-to-js - version: 1.1.3 - dependencies: - style-to-object: registry.npmjs.org/style-to-object/0.4.1 - dev: false - - registry.npmjs.org/style-to-object/0.4.1: - resolution: {integrity: sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz} - name: style-to-object - version: 0.4.1 - dependencies: - inline-style-parser: registry.npmjs.org/inline-style-parser/0.1.1 - dev: false diff --git a/ui/public/index.html b/ui/public/index.html index 3e1eddd8..cb72d38a 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -86,6 +86,9 @@ { name: 'Safari', version: '15' + }, + { + name: 'IE', } ]; function getBrowerTypeAndVersion(){ @@ -95,6 +98,7 @@ }; var ua = navigator.userAgent.toLowerCase(); var s; + ((ua.indexOf("compatible") > -1 && ua.indexOf("MSIE") > -1) || (ua.indexOf('Trident') > -1 && ua.indexOf("rv:11.0") > -1)) ? brower = { name: 'IE', version: '' } : (s = ua.match(/edge\/([\d\.]+)/)) ? brower = { name: 'Edge', version: s[1] } : (s = ua.match(/firefox\/([\d\.]+)/)) ? brower = { name: 'Firefox', version: s[1] } : (s = ua.match(/chrome\/([\d\.]+)/)) ? brower = { name: 'Chrome', version: s[1] } : @@ -126,16 +130,24 @@ } const browerInfo = getBrowerTypeAndVersion(); - const notSupport = defaultList.some(item => { - if (item.name === browerInfo.name) { - return compareVersion(browerInfo.version, item.version) === -1; - } - return false; - }); - if (notSupport) { + + if (browerInfo.name === 'IE') { const div = document.getElementById('protect-brower'); - div.innerText = 'The current browser version is too low, in order not to affect the normal use of the function, please upgrade the browser to the latest version.' + div.innerText = 'Sorry, this site does not support Internet Explorer. In order to avoid affecting the normal use of our features, please use a more modern browser such as Edge, Firefox, Chrome, or Safari.' + } else { + const notSupport = defaultList.some(item => { + if (item.name === browerInfo.name) { + return compareVersion(browerInfo.version, item.version) === -1; + } + return false; + }); + if (notSupport) { + const div = document.getElementById('protect-brower'); + div.innerText = 'The current browser version is too low, in order not to affect the normal use of the function, please upgrade the browser to the latest version.' + } } + + diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 507f16a5..cb6b63e3 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -56,10 +56,13 @@ export interface QuestionParams { title: string; url_title?: string; content: string; - html?: string; tags: Tag[]; } +export interface QuestionWithAnswer extends QuestionParams { + answer_content: string; +} + export interface ListResult { count: number; list: T[]; diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx index 8ba6a1d4..268ff9d1 100644 --- a/ui/src/components/Actions/index.tsx +++ b/ui/src/components/Actions/index.tsx @@ -12,6 +12,7 @@ import { bookmark, postVote } from '@/services'; interface Props { className?: string; + source: 'question' | 'answer'; data: { id: string; votesCount: number; @@ -24,7 +25,7 @@ interface Props { }; } -const Index: FC = ({ className, data }) => { +const Index: FC = ({ className, data, source }) => { const [votes, setVotes] = useState(0); const [like, setLike] = useState(false); const [hate, setHated] = useState(false); @@ -102,6 +103,11 @@ const Index: FC = ({ className, data }) => {
-
- - ); -}; - -export default Index; diff --git a/ui/src/pages/404/index.tsx b/ui/src/pages/404/index.tsx new file mode 100644 index 00000000..e6d800ab --- /dev/null +++ b/ui/src/pages/404/index.tsx @@ -0,0 +1,7 @@ +import { HttpErrorContent } from '@/components'; + +const Index = () => { + return ; +}; + +export default Index; diff --git a/ui/src/pages/50X/index.jsx b/ui/src/pages/50X/index.jsx deleted file mode 100644 index 33a8a693..00000000 --- a/ui/src/pages/50X/index.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useEffect } from 'react'; -import { Container, Button } from 'react-bootstrap'; -import { Link } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; - -// eslint-disable-next-line import/no-unresolved -import { usePageTags } from '@/hooks'; - -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'; - }; - }, []); - - usePageTags({ - title: t('http_50X', { keyPrefix: 'page_title' }), - }); - return ( - -
- (=T^T=) -
- -

{t('http_error')}

-
{t('desc')}
-
- -
-
- ); -}; - -export default Index; diff --git a/ui/src/pages/50X/index.tsx b/ui/src/pages/50X/index.tsx new file mode 100644 index 00000000..0dcc49f9 --- /dev/null +++ b/ui/src/pages/50X/index.tsx @@ -0,0 +1,7 @@ +import { HttpErrorContent } from '@/components'; + +const Index = () => { + return ; +}; + +export default Index; diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index cf53e269..39a7946f 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -58,7 +58,7 @@ const Answers: FC = () => { content: item.accepted === 2 ? t('answer_accepted', { keyPrefix: 'delete' }) - : `

${t('other', { keyPrefix: 'delete' })}

`, + : t('other', { keyPrefix: 'delete' }), cancelBtnVariant: 'link', confirmBtnVariant: 'danger', confirmText: t('delete', { keyPrefix: 'btns' }), diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 124fd307..d11043b2 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -67,8 +67,8 @@ const Questions: FC = () => { title: t('title', { keyPrefix: 'delete' }), content: item.answer_count > 0 - ? `

${t('question', { keyPrefix: 'delete' })}

` - : `

${t('other', { keyPrefix: 'delete' })}

`, + ? t('question', { keyPrefix: 'delete' }) + : t('other', { keyPrefix: 'delete' }), cancelBtnVariant: 'link', confirmBtnVariant: 'danger', confirmText: t('delete', { keyPrefix: 'btns' }), diff --git a/ui/src/pages/Admin/Seo/index.tsx b/ui/src/pages/Admin/Seo/index.tsx index ffabb59b..6c979dc0 100644 --- a/ui/src/pages/Admin/Seo/index.tsx +++ b/ui/src/pages/Admin/Seo/index.tsx @@ -19,14 +19,14 @@ const Index: FC = () => { type: 'number', title: t('permalink.label'), description: t('permalink.text'), - enum: [1, 2, 3, 4], + enum: [4, 3, 2, 1], enumNames: [ - '/questions/10010000000000001/post-title', - '/questions/10010000000000001', - '/questions/D1D1/post-title', '/questions/D1D1', + '/questions/D1D1/post-title', + '/questions/10010000000000001', + '/questions/10010000000000001/post-title', ], - default: 1, + default: 4, }, robots: { type: 'string', diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index a09bf113..97a682f2 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -4,7 +4,7 @@ import { HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; -import { toastStore, loginToContinueStore, errorCode } from '@/stores'; +import { toastStore, loginToContinueStore, errorCodeStore } from '@/stores'; import { Header, Footer, @@ -12,11 +12,10 @@ import { Customize, CustomizeTheme, PageTags, + HttpErrorContent, } from '@/components'; import { LoginToContinueModal } from '@/components/Modal'; import { useImgViewer } from '@/hooks'; -import Component404 from '@/pages/404'; -import Component50X from '@/pages/50X'; const Layout: FC = () => { const location = useLocation(); @@ -24,8 +23,7 @@ const Layout: FC = () => { const closeToast = () => { toastClear(); }; - const { code: httpStatusCode, reset: httpStatusReset } = errorCode(); - + const { code: httpStatusCode, reset: httpStatusReset } = errorCodeStore(); const imgViewer = useImgViewer(); const { show: showLoginToContinueModal } = loginToContinueStore(); @@ -45,10 +43,8 @@ const Layout: FC = () => {
- {httpStatusCode === '404' ? ( - - ) : httpStatusCode === '50X' ? ( - + {httpStatusCode ? ( + ) : ( )} diff --git a/ui/src/pages/Legal/Privacy/index.tsx b/ui/src/pages/Legal/Privacy/index.tsx index f8fc2ace..3cd251a6 100644 --- a/ui/src/pages/Legal/Privacy/index.tsx +++ b/ui/src/pages/Legal/Privacy/index.tsx @@ -1,8 +1,9 @@ -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { usePageTags } from '@/hooks'; import { useLegalPrivacy } from '@/services'; +import { htmlRender } from '@/components'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); @@ -12,6 +13,15 @@ const Index: FC = () => { const { data: privacy } = useLegalPrivacy(); const contentText = privacy?.privacy_policy_original_text; let matchUrl: URL | undefined; + + useEffect(() => { + const fmt = document.querySelector('.fmt') as HTMLElement; + if (!fmt) { + return; + } + htmlRender(fmt); + }, [privacy?.privacy_policy_parsed_text]); + try { if (contentText) { matchUrl = new URL(contentText); diff --git a/ui/src/pages/Legal/Tos/index.tsx b/ui/src/pages/Legal/Tos/index.tsx index bbac2b50..32aeb11d 100644 --- a/ui/src/pages/Legal/Tos/index.tsx +++ b/ui/src/pages/Legal/Tos/index.tsx @@ -1,8 +1,9 @@ -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { usePageTags } from '@/hooks'; import { useLegalTos } from '@/services'; +import { htmlRender } from '@/components'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); @@ -12,6 +13,15 @@ const Index: FC = () => { const { data: tos } = useLegalTos(); const contentText = tos?.terms_of_service_original_text; let matchUrl: URL | undefined; + + useEffect(() => { + const fmt = document.querySelector('.fmt') as HTMLElement; + if (!fmt) { + return; + } + htmlRender(fmt); + }, [tos?.terms_of_service_parsed_text]); + try { if (contentText) { matchUrl = new URL(contentText); @@ -22,8 +32,9 @@ const Index: FC = () => { window.location.replace(matchUrl.toString()); return null; } + return ( - <> +

{t('tos')}

{ __html: tos?.terms_of_service_parsed_text || '', }} /> - +
); }; diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index 6f12a519..ffc1bfbb 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -16,9 +16,10 @@ import { questionDetail, modifyQuestion, useQueryRevisions, - postAnswer, + // postAnswer, useQueryQuestionByTitle, getTagsBySlugName, + saveQuestionWidthAnaser, } from '@/services'; import { handleFormError, SaveDraft, storageExpires } from '@/utils'; import { pathFactory } from '@/router/pathFactory'; @@ -29,7 +30,7 @@ interface FormDataItem { title: Type.FormValue; tags: Type.FormValue; content: Type.FormValue; - answer: Type.FormValue; + answer_content: Type.FormValue; edit_summary: Type.FormValue; } @@ -52,7 +53,7 @@ const Ask = () => { isInvalid: false, errorMsg: '', }, - answer: { + answer_content: { value: '', isInvalid: false, errorMsg: '', @@ -92,7 +93,7 @@ const Ask = () => { return; } getTagsBySlugName(queryTags).then((tags) => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define + // eslint-disable-next-line handleTagsChange(tags); }); }; @@ -116,8 +117,8 @@ const Ask = () => { formData.title.value = draft.title; formData.content.value = draft.content; formData.tags.value = draft.tags; - formData.answer.value = draft.answer; - setCheckState(Boolean(draft.answer)); + formData.answer_content.value = draft.answer_content; + setCheckState(Boolean(draft.answer_content)); setHasDraft(true); setFormData({ ...formData }); } else { @@ -131,7 +132,7 @@ const Ask = () => { }, [qid]); useEffect(() => { - const { title, tags, content, answer } = formData; + const { title, tags, content, answer_content } = formData; const { title: editTitle, tags: editTags, content: editContent } = immData; // edited @@ -151,14 +152,19 @@ const Ask = () => { return; } // write - if (title.value || tags.value.length > 0 || content.value || answer.value) { + if ( + title.value || + tags.value.length > 0 || + content.value || + answer_content.value + ) { // save draft saveDraft.save({ params: { title: title.value, tags: tags.value, content: content.value, - answer: answer.value, + answer_content: answer_content.value, }, callback: () => setHasDraft(true), }); @@ -215,7 +221,7 @@ const Ask = () => { const handleAnswerChange = (value: string) => setFormData({ ...formData, - answer: { ...formData.answer, value, errorMsg: '' }, + answer_content: { ...formData.answer_content, value, errorMsg: '' }, }); const handleSummaryChange = (evt: React.ChangeEvent) => @@ -263,31 +269,30 @@ const Ask = () => { } }); } else { - const res = await saveQuestion(params).catch((err) => { - if (err.isError) { - const data = handleFormError(err, formData); - setFormData({ ...data }); - } - }); + let res; + if (checked) { + res = await saveQuestionWidthAnaser({ + ...params, + answer_content: formData.answer_content.value, + }).catch((err) => { + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + }); + } else { + res = await saveQuestion(params).catch((err) => { + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + }); + } - const id = res?.id; + const id = res?.id || res?.question?.id; if (id) { if (checked) { - postAnswer({ - question_id: id, - content: formData.answer.value, - }) - .then(() => { - navigate(pathFactory.questionLanding(id, params.url_title)); - }) - .catch((err) => { - if (err.isError) { - const data = handleFormError(err, formData, [ - { from: 'content', to: 'answer' }, - ]); - setFormData({ ...data }); - } - }); + navigate(pathFactory.questionLanding(id, res?.question?.url_title)); } else { navigate(pathFactory.questionLanding(id)); } @@ -448,7 +453,7 @@ const Ask = () => { {t('form.fields.answer.label')} { /> )} diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index 7da9f028..89be1784 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -1,5 +1,5 @@ import { memo, FC, useEffect, useRef } from 'react'; -import { Button, Alert } from 'react-bootstrap'; +import { Button, Alert, Badge } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link, useSearchParams } from 'react-router-dom'; @@ -20,8 +20,7 @@ interface Props { data: AnswerItem; /** router answer id */ aid?: string; - /** is author */ - isAuthor: boolean; + canAccept: boolean; questionTitle: string; slugTitle: string; isLogged: boolean; @@ -30,11 +29,11 @@ interface Props { const Index: FC = ({ aid, data, - isAuthor, isLogged, questionTitle = '', slugTitle, callback, + canAccept = false, }) => { const { t } = useTranslation('translation', { keyPrefix: 'question_detail', @@ -77,12 +76,21 @@ const Index: FC = ({ {t('post_deleted', { keyPrefix: 'messages' })} )} + {data?.accepted === 2 && ( +
+ + + Best answer + +
+ )}
= ({ }} /> - {data?.accepted === 2 && ( + {canAccept && ( - )} - - {isAuthor && data.accepted === 1 && ( - )}
diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index 9eeb66e1..94a42981 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -108,12 +108,13 @@ const Index: FC = ({ data, initPage, hasAnswer, isLogged }) => {
void; } @@ -39,6 +40,7 @@ const Index: FC = ({ visible = false, data, callback }) => { const [focusType, setFocusType] = useState(''); const [editorFocusState, setEditorFocusState] = useState(false); const [hasDraft, setHasDraft] = useState(false); + const [showTips, setShowTips] = useState(data.loggedUserRank < 100); usePromptWithUnload({ when: Boolean(formData.content.value), @@ -212,29 +214,58 @@ const Index: FC = ({ visible = false, data, callback }) => {
)} {showEditor && ( - { - setFormData({ - content: { - value: val, - isInvalid: false, - errorMsg: '', - }, - }); - }} - onFocus={() => { - setFocusType('answer'); - }} - onBlur={() => { - setFocusType(''); - }} - /> + <> + { + setFormData({ + content: { + value: val, + isInvalid: false, + errorMsg: '', + }, + }); + }} + onFocus={() => { + setFocusType('answer'); + }} + onBlur={() => { + setFocusType(''); + }} + /> + + setShowTips(false)} + dismissible + className="mt-3"> +

{t('tips.header_1')}

+
    +
  • + }} + /> +
  • +
  • {t('tips.li1_2')}
  • +
+

+ }} + /> +

+
    +
  • {t('tips.li2_1')}
  • +
+
+ )} diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index a6b3cc2d..7397c527 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -56,7 +56,9 @@ const Index = () => { const userInfo = loggedUserInfoStore((state) => state.user); const isAuthor = userInfo?.username === question?.user_info?.username; const isAdmin = userInfo?.role_id === 2; + const isModerator = userInfo?.role_id === 3; const isLogged = Boolean(userInfo?.access_token); + const loggedUserRank = userInfo?.rank; const { state: locationState } = useLocation(); useEffect(() => { @@ -221,7 +223,7 @@ const Index = () => { data={item} questionTitle={question?.title || ''} slugTitle={question?.url_title} - isAuthor={isAuthor} + canAccept={isAuthor || isAdmin || isModerator} callback={initPage} isLogged={isLogged} /> @@ -247,6 +249,7 @@ const Index = () => { data={{ qid, answered: question?.answered, + loggedUserRank, }} callback={writeAnswerCallback} /> diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index 680547f0..9875af96 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'; import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap'; import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import classNames from 'classnames'; -import { handleFormError } from '@/utils'; +import { handleFormError, scrollToDocTop } from '@/utils'; import { usePageTags, usePromptWithUnload } from '@/hooks'; import { pathFactory } from '@/router/pathFactory'; -import { Editor, EditorRef, Icon } from '@/components'; +import { Editor, EditorRef, Icon, htmlRender } from '@/components'; import type * as Type from '@/common/interface'; import { useQueryAnswerInfo, @@ -23,31 +23,47 @@ interface FormDataItem { content: Type.FormValue; description: Type.FormValue; } -const initFormData = { - content: { - value: '', - isInvalid: false, - errorMsg: '', - }, - description: { - value: '', - isInvalid: false, - errorMsg: '', - }, -}; + const Index = () => { const { aid = '', qid = '' } = useParams(); const [focusType, setForceType] = useState(''); + useLayoutEffect(() => { + scrollToDocTop(); + }, []); const { t } = useTranslation('translation', { keyPrefix: 'edit_answer' }); const navigate = useNavigate(); + const initFormData = { + content: { + value: '', + isInvalid: false, + errorMsg: '', + }, + description: { + value: '', + isInvalid: false, + errorMsg: '', + }, + }; + const { data } = useQueryAnswerInfo(aid); const [formData, setFormData] = useState(initFormData); const [immData, setImmData] = useState(initFormData); const [contentChanged, setContentChanged] = useState(false); - initFormData.content.value = data?.info.content || ''; + useLayoutEffect(() => { + if (data?.info?.content) { + setFormData({ + ...formData, + content: { + value: data.info.content, + isInvalid: false, + errorMsg: '', + }, + }); + } + }, [data?.info?.content]); const { data: revisions = [] } = useQueryRevisions(aid); @@ -57,6 +73,13 @@ const Index = () => { const questionContentRef = useRef(null); + useEffect(() => { + if (!questionContentRef?.current) { + return; + } + htmlRender(questionContentRef.current); + }, [questionContentRef]); + usePromptWithUnload({ when: contentChanged, }); @@ -147,9 +170,11 @@ const Index = () => { const handleSelectedRevision = (e) => { const index = e.target.value; const revision = revisions[index]; - formData.content.value = revision.content.content; - setImmData({ ...formData }); - setFormData({ ...formData }); + if (revision?.content) { + formData.content.value = revision.content.content; + setImmData({ ...formData }); + setFormData({ ...formData }); + } }; const backPage = () => { @@ -192,7 +217,7 @@ const Index = () => {
{t('form.fields.revision.label')} - + {revisions.map(({ create_at, reason, user_info }, index) => { const date = dayjs(create_at * 1000) .tz() diff --git a/ui/src/pages/Questions/index.tsx b/ui/src/pages/Questions/index.tsx index 8c5a1957..56a198e5 100644 --- a/ui/src/pages/Questions/index.tsx +++ b/ui/src/pages/Questions/index.tsx @@ -14,7 +14,7 @@ const Questions: FC = () => { const { user: loggedUser } = loggedUserInfoStore((_) => _); const [urlSearchParams] = useSearchParams(); const curPage = Number(urlSearchParams.get('page')) || 1; - const curOrder = urlSearchParams.get('order') || 'newest'; + const curOrder = urlSearchParams.get('order') || 'active'; const reqParams: Type.QueryQuestionsReq = { page_size: 20, page: curPage, diff --git a/ui/src/pages/Search/components/SearchHead/index.tsx b/ui/src/pages/Search/components/SearchHead/index.tsx index 3dbada0f..9902603a 100644 --- a/ui/src/pages/Search/components/SearchHead/index.tsx +++ b/ui/src/pages/Search/components/SearchHead/index.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { QueryGroup } from '@/components'; -const sortBtns = ['relevance', 'newest', 'active', 'score']; +const sortBtns = ['active', 'newest', 'relevance', 'score']; interface Props { count: number; diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index 3d51021e..e911f941 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -21,7 +21,7 @@ const Index = () => { const [searchParams] = useSearchParams(); const page = searchParams.get('page') || 1; const q = searchParams.get('q') || ''; - const order = searchParams.get('order') || 'relevance'; + const order = searchParams.get('order') || 'active'; const { data, isLoading } = useSearch({ q, diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index 8e5ffc0a..84bd7fe2 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -28,7 +28,7 @@ const Questions: FC = () => { const routeParams = useParams(); const curTagName = routeParams.tagName || ''; const [urlSearchParams] = useSearchParams(); - const curOrder = urlSearchParams.get('order') || 'newest'; + const curOrder = urlSearchParams.get('order') || 'active'; const curPage = Number(urlSearchParams.get('page')) || 1; const reqParams: Type.QueryQuestionsReq = { page_size: 20, diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index 7a1e2d4c..3f747f49 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import { usePageTags } from '@/hooks'; -import { Tag, TagSelector, FormatTime, Modal } from '@/components'; +import { Tag, TagSelector, FormatTime, Modal, htmlRender } from '@/components'; import { useTagInfo, useQuerySynonymsTags, @@ -44,6 +44,15 @@ const TagIntroduction = () => { }); } }, [locationState]); + + useEffect(() => { + const fmt = document.querySelector('.content.fmt') as HTMLElement; + if (!fmt) { + return; + } + htmlRender(fmt); + }, [tagInfo?.parsed_text]); + if (!tagInfo) { return null; } @@ -108,7 +117,9 @@ const TagIntroduction = () => { confirmText: t('delete', { keyPrefix: 'btns' }), confirmBtnVariant: 'danger', onConfirm: () => { - deleteTag(tagInfo.tag_id); + deleteTag(tagInfo.tag_id).then(() => { + navigate('/tags', { replace: true }); + }); }, }); }; @@ -143,7 +154,7 @@ const TagIntroduction = () => {
@@ -204,7 +215,8 @@ const TagIntroduction = () => { data={{ slug_name: tagName || '', main_tag_slug_name: '', - display_name: '', + display_name: + tagInfo?.display_name || tagInfo?.slug_name || '', recommend: false, reserved: false, }} diff --git a/ui/src/pages/Tags/index.tsx b/ui/src/pages/Tags/index.tsx index d20b4b0b..b8779364 100644 --- a/ui/src/pages/Tags/index.tsx +++ b/ui/src/pages/Tags/index.tsx @@ -27,7 +27,7 @@ const Tags = () => { const { role_id } = loggedUserInfoStore((_) => _.user); const page = Number(urlSearch.get('page')) || 1; - const sort = urlSearch.get('sort'); + const sort = urlSearch.get('sort') || sortBtns[0]; const pageSize = 20; const { diff --git a/ui/src/pages/Users/ConfirmNewEmail/index.tsx b/ui/src/pages/Users/ConfirmNewEmail/index.tsx index 16d09c9c..c99620db 100644 --- a/ui/src/pages/Users/ConfirmNewEmail/index.tsx +++ b/ui/src/pages/Users/ConfirmNewEmail/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { usePageTags } from '@/hooks'; import { loggedUserInfoStore, siteInfoStore } from '@/stores'; -import { changeEmailVerify, getLoggedUserInfo } from '@/services'; +import { changeEmailVerify } from '@/services'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'account_result' }); @@ -20,12 +20,12 @@ const Index: FC = () => { if (code) { // do changeEmailVerify({ code }) - .then(() => { + .then((res) => { setStep('success'); - getLoggedUserInfo().then((res) => { + if (res?.access_token) { // update user info updateUser(res); - }); + } }) .catch(() => { setStep('invalid'); diff --git a/ui/src/router/RouteErrorBoundary.tsx b/ui/src/router/RouteErrorBoundary.tsx index d70683e3..2fe5b1fa 100644 --- a/ui/src/router/RouteErrorBoundary.tsx +++ b/ui/src/router/RouteErrorBoundary.tsx @@ -1,8 +1,7 @@ -import Error50X from '@/pages/50X'; -// import Page404 from '@/pages/404'; +import { HttpErrorContent } from '@/components'; -const Index = () => { - return ; +const Index = ({ errCode = '50X', errMsg = '' }) => { + return ; }; export default Index; diff --git a/ui/src/router/RouteGuard.tsx b/ui/src/router/RouteGuard.tsx index 60d1249c..153bca51 100644 --- a/ui/src/router/RouteGuard.tsx +++ b/ui/src/router/RouteGuard.tsx @@ -1,41 +1,53 @@ import { FC, ReactNode, useEffect } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate, useLoaderData } from 'react-router-dom'; import { floppyNavigation } from '@/utils'; import { TGuardFunc } from '@/utils/guard'; -const Index: FC<{ +import RouteErrorBoundary from './RouteErrorBoundary'; + +const RouteGuard: FC<{ children: ReactNode; - onEnter?: TGuardFunc; + onEnter: TGuardFunc; path?: string; -}> = ({ - children, - onEnter, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - path, -}) => { + page?: string; +}> = ({ children, onEnter, path, page }) => { const navigate = useNavigate(); const location = useLocation(); - const callGuards = () => { - if (onEnter) { - const gr = onEnter(); - const redirectUrl = gr.redirect; - if (redirectUrl) { - floppyNavigation.navigate(redirectUrl, () => { - navigate(redirectUrl, { replace: true }); - }); - } + const loaderData = useLoaderData(); + const gr = onEnter({ + loaderData, + path, + page, + }); + + let guardError; + const errCode = gr.error?.code; + if (errCode === '403' || errCode === '404' || errCode === '50X') { + guardError = { + code: errCode, + msg: gr.error?.msg, + }; + } + const handleGuardRedirect = () => { + const redirectUrl = gr.redirect; + if (redirectUrl) { + floppyNavigation.navigate(redirectUrl, () => { + navigate(redirectUrl, { replace: true }); + }); } }; useEffect(() => { - callGuards(); + handleGuardRedirect(); }, [location]); return ( <> - {/* Route Guard */} - {children} + {gr.ok ? children : null} + {!gr.ok && guardError ? ( + + ) : null} ); }; -export default Index; +export default RouteGuard; diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 2e30ce75..50b6d2ef 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -13,7 +13,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => { routeNodes.forEach((rn) => { if (rn.page === 'pages/Layout') { rn.element = rn.guard ? ( - + ) : ( @@ -30,7 +30,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => { rn.element = ( {rn.guard ? ( - + ) : ( diff --git a/ui/src/router/pathFactory.ts b/ui/src/router/pathFactory.ts index fbf4e0ca..acf6b690 100644 --- a/ui/src/router/pathFactory.ts +++ b/ui/src/router/pathFactory.ts @@ -19,6 +19,9 @@ const tagEdit = (tagId: string) => { }; const questionLanding = (questionId: string, slugTitle: string = '') => { const { seo } = seoSettingStore.getState(); + if (!questionId) { + return slugTitle ? `/questions/null/${slugTitle}` : '/questions/null'; + } // @ts-ignore if (/[13]/.test(seo.permalink) && slugTitle) { return urlcat('/questions/:questionId/:slugPermalink', { diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 3b5b2e3b..5a350d75 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -2,6 +2,8 @@ import type { IndexRouteObject, NonIndexRouteObject } from 'react-router-dom'; import { guard } from '@/utils'; import type { TGuardFunc } from '@/utils/guard'; +import { editCheck } from '@/services'; +import { isEditable } from '@/utils/guard'; type IndexRouteNode = Omit; type NonIndexRouteNode = Omit; @@ -70,6 +72,13 @@ const routes: RouteNode[] = [ { path: 'posts/:qid/:aid/edit', page: 'pages/Questions/EditAnswer', + loader: async ({ params }) => { + const ret = await editCheck(params.aid as string, true); + return ret; + }, + guard: (args) => { + return isEditable(args); + }, }, { path: '/search', diff --git a/ui/src/services/client/notification.ts b/ui/src/services/client/notification.ts index b499bbda..cc2f32b0 100644 --- a/ui/src/services/client/notification.ts +++ b/ui/src/services/client/notification.ts @@ -34,7 +34,7 @@ export const useQueryNotificationStatus = () => { return useSWR( tryLoggedAndActivated().ok ? apiUrl : null, - request.instance.get, + (url) => request.get(url, { ignoreError: '50X' }), { refreshInterval: 3000, }, diff --git a/ui/src/services/client/revision.ts b/ui/src/services/client/revision.ts index 06b5ce49..41201cb3 100644 --- a/ui/src/services/client/revision.ts +++ b/ui/src/services/client/revision.ts @@ -1,9 +1,11 @@ import request from '@/utils/request'; import * as Type from '@/common/interface'; -export const editCheck = (id: string) => { +export const editCheck = (id: string, passingError: boolean = false) => { const apiUrl = `/answer/api/v1/revisions/edit/check?id=${id}`; - return request.get(apiUrl); + return request.get(apiUrl, { + passingError, + }); }; export const revisionAudit = (id: string, operation: 'approve' | 'reject') => { diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index a7b41552..bab13222 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -274,3 +274,7 @@ export const markdownToHtml = (content: string) => { const apiUrl = '/answer/api/v1/post/render'; return request.post(apiUrl, { content }); }; + +export const saveQuestionWidthAnaser = (params: Type.QuestionWithAnswer) => { + return request.post('/answer/api/v1/question/answer', params); +}; diff --git a/ui/src/stores/errorCode.ts b/ui/src/stores/errorCode.ts index 336a213b..aa2bcea1 100644 --- a/ui/src/stores/errorCode.ts +++ b/ui/src/stores/errorCode.ts @@ -1,25 +1,27 @@ import create from 'zustand'; -type codeType = '404' | '50X' | ''; +type codeType = '403' | '404' | '50X' | ''; -interface NotFoundType { +interface ErrorCodeType { code: codeType; - update: (code: codeType) => void; + msg: string; + update: (code: codeType, msg?: string) => void; reset: () => void; } -const notFound = create((set) => ({ +const Index = create((set) => ({ code: '', - update: (code: codeType) => { + msg: '', + update: (code: codeType, msg: string = '') => { set(() => { - return { code }; + return { code, msg }; }); }, reset: () => { set(() => { - return { code: '' }; + return { code: '', msg: '' }; }); }, })); -export default notFound; +export default Index; diff --git a/ui/src/stores/index.ts b/ui/src/stores/index.ts index a6c3b143..247f1d64 100644 --- a/ui/src/stores/index.ts +++ b/ui/src/stores/index.ts @@ -10,7 +10,7 @@ import pageTagStore from './pageTags'; import customizeStore from './customize'; import themeSettingStore from './themeSetting'; import loginToContinueStore from './loginToContinue'; -import errorCode from './errorCode'; +import errorCodeStore from './errorCode'; export { toastStore, @@ -24,5 +24,5 @@ export { themeSettingStore, seoSettingStore, loginToContinueStore, - errorCode, + errorCodeStore, }; diff --git a/ui/src/stores/seoSetting.ts b/ui/src/stores/seoSetting.ts index 6c15e9cf..c714a8f5 100644 --- a/ui/src/stores/seoSetting.ts +++ b/ui/src/stores/seoSetting.ts @@ -7,7 +7,7 @@ interface IProps { update: (params: AdminSettingsSeo) => void; } -const siteInfo = create((set) => ({ +const Index = create((set) => ({ seo: { robots: '', permalink: 1, @@ -25,4 +25,4 @@ const siteInfo = create((set) => ({ }), })); -export default siteInfo; +export default Index; diff --git a/ui/src/stores/userInfo.ts b/ui/src/stores/userInfo.ts index 52976253..14511b0c 100644 --- a/ui/src/stores/userInfo.ts +++ b/ui/src/stores/userInfo.ts @@ -32,7 +32,7 @@ const initUser: UserInfoRes = { const loggedUserInfoStore = create((set) => ({ user: initUser, update: (params) => { - if (!params.language) { + if (!params?.language) { params.language = 'Default'; } set(() => { diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index bd2f61a3..ce950ab7 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -1,6 +1,4 @@ import i18next from 'i18next'; -import parse from 'html-react-parser'; -import * as DOMPurify from 'dompurify'; const Diff = require('diff'); @@ -235,32 +233,6 @@ function diffText(newText: string, oldText?: string): string { return result.join(''); } -function htmlToReact(html: string) { - const cleanedHtml = DOMPurify.sanitize(html, { - USE_PROFILES: { html: true }, - }); - - 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 { thousandthDivision, formatCount, @@ -276,5 +248,4 @@ export { labelStyle, handleFormError, diffText, - htmlToReact, }; diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index 280df4df..5ff081af 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -30,8 +30,16 @@ type TLoginState = { export type TGuardResult = { ok: boolean; redirect?: string; + error?: { + code?: number | string; + msg?: string; + }; }; -export type TGuardFunc = () => TGuardResult; +export type TGuardFunc = (args: { + loaderData?: any; + path?: string; + page?: string; +}) => TGuardResult; export const deriveLoginState = (): TLoginState => { const ls: TLoginState = { @@ -174,7 +182,11 @@ export const admin = () => { const us = deriveLoginState(); if (gr.ok && !us.isAdmin) { gr.ok = false; - gr.redirect = RouteAlias.home; + gr.error = { + code: '403', + msg: '', + }; + gr.redirect = ''; } return gr; }; @@ -184,7 +196,24 @@ export const isAdminOrModerator = () => { const us = deriveLoginState(); if (gr.ok && !us.isAdmin && !us.isModerator) { gr.ok = false; - gr.redirect = RouteAlias.home; + gr.error = { + code: '403', + msg: '', + }; + gr.redirect = ''; + } + return gr; +}; + +export const isEditable = (args) => { + const loaderData = args?.loaderData || {}; + const gr: TGuardResult = { ok: true }; + if (loaderData.code === 400) { + gr.ok = false; + gr.error = { + code: '403', + msg: loaderData.msg, + }; } return gr; }; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index 0e58da26..e2b706a4 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import { Modal } from '@/components'; -import { loggedUserInfoStore, toastStore, errorCode } from '@/stores'; +import { loggedUserInfoStore, toastStore, errorCodeStore } from '@/stores'; import { LOGGED_TOKEN_STORAGE_KEY, IGNORE_PATH_LIST } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; import { getCurrentLang } from '@/utils/localize'; @@ -16,8 +16,12 @@ const baseConfig = { withCredentials: true, }; -interface APIconfig extends AxiosRequestConfig { - allow404: boolean; +interface ApiConfig extends AxiosRequestConfig { + // Configure whether to allow takeover of 404 errors + allow404?: boolean; + ignoreError?: '403' | '50X'; + // Configure whether to pass errors directly + passingError?: boolean; } class Request { @@ -52,14 +56,17 @@ class Request { return data; }, (error) => { - const { status, data: respData } = error.response || {}; - const { data = {}, msg = '', reason = '' } = respData || {}; - - console.log('response error:', error); - + const { + status, + data: errModel, + config: errConfig, + } = error.response || {}; + const { data = {}, msg = '' } = errModel || {}; if (status === 400) { - // show error message - if (data instanceof Object && data.err_type) { + if (data?.err_type && errConfig?.passingError) { + return errModel; + } + if (data?.err_type) { if (data.err_type === 'toast') { // toast error message toastStore.getState().show({ @@ -90,7 +97,6 @@ class Request { return Promise.reject({ code: status, msg, - reason, isError: true, list: data, }); @@ -107,14 +113,13 @@ class Request { // 401: Re-login required if (status === 401) { // clear userinfo - errorCode.getState().reset(); + errorCodeStore.getState().reset(); loggedUserInfoStore.getState().clear(); floppyNavigation.navigateToLogin(); return Promise.reject(false); } if (status === 403) { // Permission interception - errorCode.getState().reset(); if (data?.type === 'url_expired') { // url expired floppyNavigation.navigate(RouteAlias.activationFailed, () => { @@ -137,6 +142,14 @@ class Request { return Promise.reject(false); } + if (isIgnoredPath(IGNORE_PATH_LIST)) { + return Promise.reject(false); + } + if (error.config?.url.includes('/admin/api')) { + errorCodeStore.getState().update('403'); + return Promise.reject(false); + } + if (msg) { toastStore.getState().show({ msg, @@ -150,14 +163,18 @@ class Request { if (isIgnoredPath(IGNORE_PATH_LIST)) { return Promise.reject(false); } - errorCode.getState().update('404'); + errorCodeStore.getState().update('404'); return Promise.reject(false); } if (status >= 500) { if (isIgnoredPath(IGNORE_PATH_LIST)) { return Promise.reject(false); } - errorCode.getState().update('50X'); + + if (error.config?.ignoreError !== '50X') { + errorCodeStore.getState().update('50X'); + } + console.error( `Request failed with status code ${status}, ${msg || ''}`, ); @@ -171,7 +188,7 @@ class Request { return this.instance.request(config); } - public get(url: string, config?: APIconfig): Promise { + public get(url: string, config?: ApiConfig): Promise { return this.instance.get(url, config); } diff --git a/ui/src/utils/saveDraft.ts b/ui/src/utils/saveDraft.ts index 65a3df12..2d579f65 100644 --- a/ui/src/utils/saveDraft.ts +++ b/ui/src/utils/saveDraft.ts @@ -11,7 +11,7 @@ export type QuestionDraft = { title: string; content: string; tags: any[]; - answer: string; + answer_content: string; }; callback?: () => void; }; diff --git a/ui/template/comment.html b/ui/template/comment.html index 26936b2d..3ebd8ba1 100644 --- a/ui/template/comment.html +++ b/ui/template/comment.html @@ -3,7 +3,7 @@