mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-03 15:56:22 +00:00
⚡️ feat: refactor sorting parameters in post listing API and components
- Renamed `orderedBy` to `orderBy` and `reverse` to `desc` in ListPostsParams interface and related functions. - Updated all usages of the sorting parameters in the post listing logic to reflect the new naming convention. feat: add user-related API functions - Implemented `getLoginUser` and `getUserById` functions in the user API to fetch user details. - Enhanced user model to include `language` property. feat: integrate next-intl for internationalization - Added `next-intl` plugin to Next.js configuration for improved localization support. - Removed previous i18n implementation and replaced it with a new structure using JSON files for translations. - Created locale files for English, Japanese, and Chinese with basic translations. - Implemented a request configuration to handle user locales and messages dynamically. fix: clean up unused imports and code - Removed unused i18n utility functions and language settings from device context. - Cleaned up commented-out code in blog card component and sidebar. chore: update dependencies - Added `deepmerge` for merging locale messages. - Updated package.json and pnpm-lock.yaml to reflect new dependencies.
This commit is contained in:
@ -1 +1,91 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
|
"github.com/snowykami/neo-blog/internal/dto"
|
||||||
|
"github.com/snowykami/neo-blog/internal/service"
|
||||||
|
"github.com/snowykami/neo-blog/pkg/errs"
|
||||||
|
"github.com/snowykami/neo-blog/pkg/resps"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommentController struct {
|
||||||
|
service *service.CommentService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommentController() *CommentController {
|
||||||
|
return &CommentController{
|
||||||
|
service: service.NewCommentService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *CommentController) CreateComment(ctx context.Context, c *app.RequestContext) {
|
||||||
|
var req dto.CreateCommentReq
|
||||||
|
if err := c.BindAndValidate(&req); err != nil {
|
||||||
|
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := cc.service.CreateComment(ctx, &req)
|
||||||
|
if err != nil {
|
||||||
|
serviceErr := errs.AsServiceError(err)
|
||||||
|
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resps.Ok(c, resps.Success, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *CommentController) UpdateComment(ctx context.Context, c *app.RequestContext) {
|
||||||
|
var req dto.UpdateCommentReq
|
||||||
|
if err := c.BindAndValidate(&req); err != nil {
|
||||||
|
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := c.Param("id")
|
||||||
|
idInt, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.CommentID = uint(idInt)
|
||||||
|
|
||||||
|
err = cc.service.UpdateComment(ctx, &req)
|
||||||
|
if err != nil {
|
||||||
|
serviceErr := errs.AsServiceError(err)
|
||||||
|
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resps.Ok(c, resps.Success, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *CommentController) DeleteComment(ctx context.Context, c *app.RequestContext) {
|
||||||
|
id := c.Param("id")
|
||||||
|
err := cc.service.DeleteComment(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
serviceErr := errs.AsServiceError(err)
|
||||||
|
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resps.Ok(c, resps.Success, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *CommentController) GetComment(ctx context.Context, c *app.RequestContext) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := cc.service.GetComment(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
serviceErr := errs.AsServiceError(err)
|
||||||
|
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resps.Ok(c, resps.Success, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *CommentController) GetCommentList(ctx context.Context, c *app.RequestContext) {
|
||||||
|
// pagenation := ctxutils.GetPaginationParams(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *CommentController) ReactComment(ctx context.Context, c *app.RequestContext) {}
|
||||||
|
@ -1 +1,16 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LikeController struct{}
|
||||||
|
|
||||||
|
func NewLikeController() *LikeController {
|
||||||
|
return &LikeController{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LikeController) ToggleLike(ctx context.Context, c *app.RequestContext) {
|
||||||
|
// Implementation for creating a like
|
||||||
|
}
|
||||||
|
@ -96,21 +96,21 @@ func (p *PostController) Update(ctx context.Context, c *app.RequestContext) {
|
|||||||
func (p *PostController) List(ctx context.Context, c *app.RequestContext) {
|
func (p *PostController) List(ctx context.Context, c *app.RequestContext) {
|
||||||
pagination := ctxutils.GetPaginationParams(c)
|
pagination := ctxutils.GetPaginationParams(c)
|
||||||
fmt.Println(pagination)
|
fmt.Println(pagination)
|
||||||
if pagination.OrderedBy == "" {
|
if pagination.OrderBy == "" {
|
||||||
pagination.OrderedBy = constant.OrderedByUpdatedAt
|
pagination.OrderBy = constant.OrderByUpdatedAt
|
||||||
}
|
}
|
||||||
if pagination.OrderedBy != "" && !slices.Contains(constant.OrderedByEnumPost, pagination.OrderedBy) {
|
if pagination.OrderBy != "" && !slices.Contains(constant.OrderByEnumPost, pagination.OrderBy) {
|
||||||
resps.BadRequest(c, "无效的排序字段")
|
resps.BadRequest(c, "无效的排序字段")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
keywords := c.Query("keywords")
|
keywords := c.Query("keywords")
|
||||||
keywordsArray := strings.Split(keywords, ",")
|
keywordsArray := strings.Split(keywords, ",")
|
||||||
req := &dto.ListPostReq{
|
req := &dto.ListPostReq{
|
||||||
Keywords: keywordsArray,
|
Keywords: keywordsArray,
|
||||||
Page: pagination.Page,
|
Page: pagination.Page,
|
||||||
Size: pagination.Size,
|
Size: pagination.Size,
|
||||||
OrderedBy: pagination.OrderedBy,
|
OrderBy: pagination.OrderBy,
|
||||||
Reverse: pagination.Reverse,
|
Desc: pagination.Desc,
|
||||||
}
|
}
|
||||||
posts, err := p.service.ListPosts(ctx, req)
|
posts, err := p.service.ListPosts(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -6,28 +6,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PaginationParams struct {
|
type PaginationParams struct {
|
||||||
Page uint64
|
Page uint64
|
||||||
Size uint64
|
Size uint64
|
||||||
OrderedBy string
|
OrderBy string
|
||||||
Reverse bool // 默认是从大值到小值
|
Desc bool // 默认是从大值到小值
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPaginationParams(c *app.RequestContext) *PaginationParams {
|
func GetPaginationParams(c *app.RequestContext) *PaginationParams {
|
||||||
page := c.Query("page")
|
page := c.Query("page")
|
||||||
size := c.Query("size")
|
size := c.Query("size")
|
||||||
orderedBy := c.Query("ordered_by")
|
orderBy := c.Query("order_by")
|
||||||
reverse := c.Query("reverse")
|
desc := c.Query("desc")
|
||||||
if page == "" {
|
if page == "" {
|
||||||
page = "1"
|
page = "1"
|
||||||
}
|
}
|
||||||
if size == "" {
|
if size == "" {
|
||||||
size = "10"
|
size = "10"
|
||||||
}
|
}
|
||||||
var reverseBool bool
|
var descBool bool
|
||||||
if reverse == "" || reverse == "false" || reverse == "0" {
|
if desc == "" || desc == "false" || desc == "0" {
|
||||||
reverseBool = false
|
descBool = false
|
||||||
} else {
|
} else {
|
||||||
reverseBool = true
|
descBool = true
|
||||||
}
|
}
|
||||||
pageNum, err := strconv.ParseUint(page, 10, 64)
|
pageNum, err := strconv.ParseUint(page, 10, 64)
|
||||||
if err != nil || pageNum < 1 {
|
if err != nil || pageNum < 1 {
|
||||||
@ -38,9 +38,9 @@ func GetPaginationParams(c *app.RequestContext) *PaginationParams {
|
|||||||
sizeNum = 10
|
sizeNum = 10
|
||||||
}
|
}
|
||||||
return &PaginationParams{
|
return &PaginationParams{
|
||||||
Page: pageNum,
|
Page: pageNum,
|
||||||
Size: sizeNum,
|
Size: sizeNum,
|
||||||
OrderedBy: orderedBy,
|
OrderBy: orderBy,
|
||||||
Reverse: reverseBool,
|
Desc: descBool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,25 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
|
type CommentDto struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
TargetID uint `json:"target_id"`
|
||||||
|
TargetType string `json:"target_type"` // 目标类型,如 "post", "page"
|
||||||
|
Content string `json:"content"`
|
||||||
|
ReplyID uint `json:"reply_id"` // 回复的评论ID
|
||||||
|
Depth int `json:"depth"` // 评论的层级深度
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
User UserDto `json:"user"` // 评论的
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCommentReq struct {
|
||||||
|
TargetID uint `json:"target_id" binding:"required"` // 目标ID
|
||||||
|
TargetType string `json:"target_type" binding:"required"` // 目标类型,如 "post", "page"
|
||||||
|
Content string `json:"content" binding:"required"` // 评论内容
|
||||||
|
ReplyID uint `json:"reply_id"` // 回复的评论ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateCommentReq struct {
|
||||||
|
CommentID uint `json:"comment_id" binding:"required"` // 评论ID
|
||||||
|
Content string `json:"content" binding:"required"` // 评论内容
|
||||||
|
}
|
||||||
|
@ -29,16 +29,16 @@ type CreateOrUpdatePostReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ListPostReq struct {
|
type ListPostReq struct {
|
||||||
Keywords []string `json:"keywords"` // 关键词列表
|
Keywords []string `json:"keywords"` // 关键词列表
|
||||||
OrderedBy string `json:"ordered_by"` // 排序方式
|
OrderBy string `json:"order_by"` // 排序方式
|
||||||
Page uint64 `json:"page"` // 页码
|
Page uint64 `json:"page"` // 页码
|
||||||
Size uint64 `json:"size"`
|
Size uint64 `json:"size"`
|
||||||
Reverse bool `json:"reverse"`
|
Desc bool `json:"desc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListPostResp struct {
|
type ListPostResp struct {
|
||||||
Posts []PostDto `json:"posts"`
|
Posts []PostDto `json:"posts"`
|
||||||
Total uint64 `json:"total"` // 总数
|
Total uint64 `json:"total"` // 总数
|
||||||
OrderedBy string `json:"ordered_by"` // 排序方式
|
OrderBy string `json:"order_by"` // 排序方式
|
||||||
Reverse bool `json:"reverse"`
|
Desc bool `json:"desc"`
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ type UserDto struct {
|
|||||||
Email string `json:"email"` // 邮箱
|
Email string `json:"email"` // 邮箱
|
||||||
Gender string `json:"gender"`
|
Gender string `json:"gender"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
|
Language string `json:"language"` // 语言
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserOidcConfigDto struct {
|
type UserOidcConfigDto struct {
|
||||||
|
@ -8,30 +8,42 @@ import (
|
|||||||
"github.com/snowykami/neo-blog/pkg/constant"
|
"github.com/snowykami/neo-blog/pkg/constant"
|
||||||
"github.com/snowykami/neo-blog/pkg/resps"
|
"github.com/snowykami/neo-blog/pkg/resps"
|
||||||
"github.com/snowykami/neo-blog/pkg/utils"
|
"github.com/snowykami/neo-blog/pkg/utils"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func UseAuth(block bool) app.HandlerFunc {
|
func UseAuth(block bool) app.HandlerFunc {
|
||||||
return func(ctx context.Context, c *app.RequestContext) {
|
return func(ctx context.Context, c *app.RequestContext) {
|
||||||
// For cookie
|
// For cookie
|
||||||
token := string(c.Cookie("token"))
|
tokenFromCookie := string(c.Cookie("tokenFromCookie"))
|
||||||
|
tokenFromHeader := strings.TrimPrefix(string(c.GetHeader("Authorization")), "Bearer ")
|
||||||
refreshToken := string(c.Cookie("refresh_token"))
|
refreshToken := string(c.Cookie("refresh_token"))
|
||||||
|
|
||||||
// 尝试用普通 token 认证
|
// 尝试用普通 tokenFromCookie 认证
|
||||||
tokenClaims, err := utils.Jwt.ParseJsonWebTokenWithoutState(token)
|
if tokenFromCookie != "" {
|
||||||
if err == nil && tokenClaims != nil {
|
tokenClaims, err := utils.Jwt.ParseJsonWebTokenWithoutState(tokenFromCookie)
|
||||||
ctx = context.WithValue(ctx, constant.ContextKeyUserID, tokenClaims.UserID)
|
if err == nil && tokenClaims != nil {
|
||||||
c.Next(ctx)
|
ctx = context.WithValue(ctx, constant.ContextKeyUserID, tokenClaims.UserID)
|
||||||
return
|
c.Next(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// tokenFromCookie 认证失败,尝试用 Bearer tokenFromHeader 认证
|
||||||
// token 失效 使用 refresh token 重新签发和鉴权
|
if tokenFromHeader != "" {
|
||||||
|
tokenClaims, err := utils.Jwt.ParseJsonWebTokenWithoutState(tokenFromHeader)
|
||||||
|
if err == nil && tokenClaims != nil {
|
||||||
|
ctx = context.WithValue(ctx, constant.ContextKeyUserID, tokenClaims.UserID)
|
||||||
|
c.Next(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// tokenFromCookie 失效 使用 refresh tokenFromCookie 重新签发和鉴权
|
||||||
refreshTokenClaims, err := utils.Jwt.ParseJsonWebTokenWithoutState(refreshToken)
|
refreshTokenClaims, err := utils.Jwt.ParseJsonWebTokenWithoutState(refreshToken)
|
||||||
if err == nil && refreshTokenClaims != nil {
|
if err == nil && refreshTokenClaims != nil {
|
||||||
ok, err := isStatefulJwtValid(refreshTokenClaims)
|
ok, err := isStatefulJwtValid(refreshTokenClaims)
|
||||||
if err == nil && ok {
|
if err == nil && ok {
|
||||||
ctx = context.WithValue(ctx, constant.ContextKeyUserID, refreshTokenClaims.UserID)
|
ctx = context.WithValue(ctx, constant.ContextKeyUserID, refreshTokenClaims.UserID)
|
||||||
// 生成新 token
|
// 生成新 tokenFromCookie
|
||||||
newTokenClaims := utils.Jwt.NewClaims(
|
newTokenClaims := utils.Jwt.NewClaims(
|
||||||
refreshTokenClaims.UserID,
|
refreshTokenClaims.UserID,
|
||||||
refreshTokenClaims.SessionKey,
|
refreshTokenClaims.SessionKey,
|
||||||
|
@ -11,6 +11,7 @@ type Comment struct {
|
|||||||
ReplyID uint `gorm:"index"` // 回复的评论ID
|
ReplyID uint `gorm:"index"` // 回复的评论ID
|
||||||
Content string `gorm:"type:text"` // 评论内容
|
Content string `gorm:"type:text"` // 评论内容
|
||||||
Depth int `gorm:"default:0"` // 评论的层级深度
|
Depth int `gorm:"default:0"` // 评论的层级深度
|
||||||
|
IsPrivate bool `gorm:"default:false"` // 是否为私密评论,私密评论只有评论者和被评论对象所有者可见
|
||||||
LikeCount uint64
|
LikeCount uint64
|
||||||
CommentCount uint64
|
CommentCount uint64
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ type Post struct {
|
|||||||
CategoryID uint `gorm:"index"` // 帖子分类ID
|
CategoryID uint `gorm:"index"` // 帖子分类ID
|
||||||
Category Category `gorm:"foreignKey:CategoryID;references:ID"` // 关联的分类
|
Category Category `gorm:"foreignKey:CategoryID;references:ID"` // 关联的分类
|
||||||
Labels []Label `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 关联的标签
|
Labels []Label `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 关联的标签
|
||||||
IsPrivate bool `gorm:"default:false"` // 是否为私密帖子
|
IsPrivate bool `gorm:"default:false"`
|
||||||
LikeCount uint64
|
LikeCount uint64
|
||||||
CommentCount uint64
|
CommentCount uint64
|
||||||
ViewCount uint64
|
ViewCount uint64
|
||||||
@ -61,3 +61,14 @@ func (p *Post) ToDto() dto.PostDto {
|
|||||||
UpdatedAt: p.UpdatedAt,
|
UpdatedAt: p.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToDtoWithShortContent 返回一个简化的 DTO,内容可以根据需要截断
|
||||||
|
func (p *Post) ToDtoWithShortContent(contentLength int) dto.PostDto {
|
||||||
|
dtoPost := p.ToDto()
|
||||||
|
if len(p.Content) > contentLength {
|
||||||
|
dtoPost.Content = p.Content[:contentLength] + "..."
|
||||||
|
} else {
|
||||||
|
dtoPost.Content = p.Content
|
||||||
|
}
|
||||||
|
return dtoPost
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ type User struct {
|
|||||||
Email string `gorm:"uniqueIndex"`
|
Email string `gorm:"uniqueIndex"`
|
||||||
Gender string
|
Gender string
|
||||||
Role string `gorm:"default:'user'"`
|
Role string `gorm:"default:'user'"`
|
||||||
|
Language string `gorm:"default:'en'"`
|
||||||
Password string // 密码,存储加密后的值
|
Password string // 密码,存储加密后的值
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,5 +34,6 @@ func (user *User) ToDto() *dto.UserDto {
|
|||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Gender: user.Gender,
|
Gender: user.Gender,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
|
Language: user.Language,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,33 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
|
import "github.com/snowykami/neo-blog/internal/model"
|
||||||
|
|
||||||
|
type CommentRepo struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var Comment = &CommentRepo{}
|
||||||
|
|
||||||
|
func (cr *CommentRepo) CreateComment(comment *model.Comment) error {
|
||||||
|
// Implementation for creating a comment
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *CommentRepo) UpdateComment(comment *model.Comment) error {
|
||||||
|
// Implementation for updating a comment
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *CommentRepo) DeleteComment(commentID string) error {
|
||||||
|
// Implementation for deleting a comment
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *CommentRepo) GetComment(commentID string) (*model.Comment, error) {
|
||||||
|
// Implementation for getting a comment by ID
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *CommentRepo) ListComments(currentUserID uint, page, size uint, orderBy string, desc bool) ([]model.Comment, error) {
|
||||||
|
// Implementation for listing comments for a post
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
@ -101,10 +101,8 @@ func initPostgres(config DBConfig, gormConfig *gorm.Config) (db *gorm.DB, err er
|
|||||||
if config.Host == "" || config.User == "" || config.Password == "" || config.DBName == "" {
|
if config.Host == "" || config.User == "" || config.Password == "" || config.DBName == "" {
|
||||||
err = errors.New("PostgreSQL configuration is incomplete: host, user, password, and dbname are required")
|
err = errors.New("PostgreSQL configuration is incomplete: host, user, password, and dbname are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode)
|
config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode)
|
||||||
|
|
||||||
db, err = gorm.Open(postgres.Open(dsn), gormConfig)
|
db, err = gorm.Open(postgres.Open(dsn), gormConfig)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -11,38 +11,53 @@ type likeRepo struct{}
|
|||||||
|
|
||||||
var Like = &likeRepo{}
|
var Like = &likeRepo{}
|
||||||
|
|
||||||
// Like 用户点赞,幂等
|
func (l *likeRepo) ToggleLike(userID, targetID uint, targetType string) error {
|
||||||
func (l *likeRepo) Like(userID, targetID uint, targetType string) error {
|
|
||||||
err := l.checkTargetType(targetType)
|
err := l.checkTargetType(targetType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var existingLike model.Like
|
return GetDB().Transaction(func(tx *gorm.DB) error {
|
||||||
err = GetDB().Where("target_type = ? AND target_id = ? AND user_id = ?", targetType, targetID, userID).First(&existingLike).Error
|
// 判断是否已点赞
|
||||||
if err == nil {
|
isLiked, err := l.IsLiked(userID, targetID, targetType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isLiked {
|
||||||
|
// 已点赞,执行取消点赞逻辑
|
||||||
|
if err := tx.Where("target_type = ? AND target_id = ? AND user_id = ?", targetType, targetID, userID).Delete(&model.Like{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 未点赞,执行新增点赞逻辑
|
||||||
|
like := &model.Like{
|
||||||
|
TargetType: targetType,
|
||||||
|
TargetID: targetID,
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
if err := tx.Create(like).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 重新计算点赞数量
|
||||||
|
var count int64
|
||||||
|
if err := tx.Model(&model.Like{}).Where("target_type = ? AND target_id = ?", targetType, targetID).Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 更新目标的点赞数量
|
||||||
|
switch targetType {
|
||||||
|
case constant.TargetTypePost:
|
||||||
|
if err := tx.Model(&model.Post{}).Where("id = ?", targetID).Update("like_count", count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case constant.TargetTypeComment:
|
||||||
|
if err := tx.Model(&model.Comment{}).Where("id = ?", targetID).Update("like_count", count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("invalid target type")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
})
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
like := &model.Like{
|
|
||||||
TargetType: targetType,
|
|
||||||
TargetID: targetID,
|
|
||||||
UserID: userID,
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetDB().Create(like).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnLike 取消点赞
|
|
||||||
func (l *likeRepo) UnLike(userID, targetID uint, targetType string) error {
|
|
||||||
err := l.checkTargetType(targetType)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return GetDB().Where("target_type = ? AND target_id = ? AND user_id = ?",
|
|
||||||
targetType, targetID, userID).Delete(&model.Like{}).Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLiked 检查是否点赞
|
// IsLiked 检查是否点赞
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/snowykami/neo-blog/internal/model"
|
"github.com/snowykami/neo-blog/internal/model"
|
||||||
"github.com/snowykami/neo-blog/pkg/constant"
|
"github.com/snowykami/neo-blog/pkg/constant"
|
||||||
"github.com/snowykami/neo-blog/pkg/errs"
|
"github.com/snowykami/neo-blog/pkg/errs"
|
||||||
@ -48,16 +47,9 @@ func (p *postRepo) UpdatePost(post *model.Post) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *postRepo) ListPosts(currentUserID uint, keywords []string, page, size uint64, orderedBy string, reverse bool) ([]model.Post, error) {
|
func (p *postRepo) ListPosts(currentUserID uint, keywords []string, page, size uint64, orderBy string, desc bool) ([]model.Post, error) {
|
||||||
var posts []model.Post
|
if !slices.Contains(constant.OrderByEnumPost, orderBy) {
|
||||||
if !slices.Contains(constant.OrderedByEnumPost, orderedBy) {
|
return nil, errs.New(http.StatusBadRequest, "invalid order_by parameter", nil)
|
||||||
return nil, errs.New(http.StatusBadRequest, "invalid ordered_by parameter", nil)
|
|
||||||
}
|
|
||||||
order := orderedBy
|
|
||||||
if reverse {
|
|
||||||
order += " ASC"
|
|
||||||
} else {
|
|
||||||
order += " DESC"
|
|
||||||
}
|
}
|
||||||
query := GetDB().Model(&model.Post{}).Preload("User")
|
query := GetDB().Model(&model.Post{}).Preload("User")
|
||||||
if currentUserID > 0 {
|
if currentUserID > 0 {
|
||||||
@ -65,7 +57,6 @@ func (p *postRepo) ListPosts(currentUserID uint, keywords []string, page, size u
|
|||||||
} else {
|
} else {
|
||||||
query = query.Where("is_private = ?", false)
|
query = query.Where("is_private = ?", false)
|
||||||
}
|
}
|
||||||
fmt.Println(keywords)
|
|
||||||
if len(keywords) > 0 {
|
if len(keywords) > 0 {
|
||||||
for _, keyword := range keywords {
|
for _, keyword := range keywords {
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
@ -75,9 +66,20 @@ func (p *postRepo) ListPosts(currentUserID uint, keywords []string, page, size u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
query = query.Order(order).Offset(int((page - 1) * size)).Limit(int(size))
|
items, _, err := PaginateQuery[model.Post](query, page, size, orderBy, desc)
|
||||||
if err := query.Find(&posts).Error; err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return posts, nil
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *postRepo) ToggleLikePost(postID uint, userID uint) error {
|
||||||
|
if postID == 0 || userID == 0 {
|
||||||
|
return errs.New(http.StatusBadRequest, "invalid post ID or user ID", nil)
|
||||||
|
}
|
||||||
|
err := Like.ToggleLike(userID, postID, constant.TargetTypePost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1 +1,39 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/snowykami/neo-blog/pkg/constant"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PaginateQuery[T any](db *gorm.DB, page, limit uint64, orderBy string, desc bool, conditions ...any) (items []T, total int64, err error) {
|
||||||
|
countDB := db
|
||||||
|
if len(conditions) > 0 {
|
||||||
|
countDB = countDB.Where(conditions[0], conditions[1:]...)
|
||||||
|
}
|
||||||
|
err = countDB.Model(new(T)).Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = constant.PageLimitDefault
|
||||||
|
}
|
||||||
|
queryDB := db
|
||||||
|
if len(conditions) > 0 {
|
||||||
|
queryDB = queryDB.Where(conditions[0], conditions[1:]...)
|
||||||
|
}
|
||||||
|
if page > 0 {
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
queryDB = queryDB.Offset(int(offset))
|
||||||
|
}
|
||||||
|
orderStr := orderBy
|
||||||
|
if orderStr == "" {
|
||||||
|
orderStr = "id"
|
||||||
|
}
|
||||||
|
if desc {
|
||||||
|
orderStr += " DESC"
|
||||||
|
} else {
|
||||||
|
orderStr += " ASC"
|
||||||
|
}
|
||||||
|
err = queryDB.Limit(int(limit)).Order(orderStr).Find(&items).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -1,7 +1,21 @@
|
|||||||
package apiv1
|
package apiv1
|
||||||
|
|
||||||
import "github.com/cloudwego/hertz/pkg/route"
|
import (
|
||||||
|
"github.com/cloudwego/hertz/pkg/route"
|
||||||
|
v1 "github.com/snowykami/neo-blog/internal/controller/v1"
|
||||||
|
"github.com/snowykami/neo-blog/internal/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
func registerCommentRoutes(group *route.RouterGroup) {
|
func registerCommentRoutes(group *route.RouterGroup) {
|
||||||
// TODO: Implement comment routes
|
commentController := v1.NewCommentController()
|
||||||
|
commentGroup := group.Group("/comments").Use(middleware.UseAuth(true))
|
||||||
|
commentGroupWithoutAuth := group.Group("/comments").Use(middleware.UseAuth(false))
|
||||||
|
{
|
||||||
|
commentGroup.POST("/c", commentController.CreateComment)
|
||||||
|
commentGroup.PUT("/c/:id", commentController.UpdateComment)
|
||||||
|
commentGroup.DELETE("/c/:id", commentController.DeleteComment)
|
||||||
|
commentGroup.PUT("/c/:id/react", commentController.ReactComment) // 暂时先不写
|
||||||
|
commentGroupWithoutAuth.GET("/c/:id", commentController.GetComment)
|
||||||
|
commentGroupWithoutAuth.GET("/c/list", commentController.GetCommentList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,15 @@
|
|||||||
package apiv1
|
package apiv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cloudwego/hertz/pkg/route"
|
||||||
|
"github.com/snowykami/neo-blog/internal/controller/v1"
|
||||||
|
"github.com/snowykami/neo-blog/internal/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerLikeRoutes(group *route.RouterGroup) {
|
||||||
|
likeController := v1.NewLikeController()
|
||||||
|
likeGroup := group.Group("/like").Use(middleware.UseAuth(true))
|
||||||
|
{
|
||||||
|
likeGroup.PUT("/toggle", likeController.ToggleLike)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -16,7 +16,7 @@ func registerPostRoutes(group *route.RouterGroup) {
|
|||||||
postGroupWithoutAuth.GET("/p/:id", postController.Get)
|
postGroupWithoutAuth.GET("/p/:id", postController.Get)
|
||||||
postGroupWithoutAuth.GET("/list", postController.List)
|
postGroupWithoutAuth.GET("/list", postController.List)
|
||||||
postGroup.POST("/p", postController.Create)
|
postGroup.POST("/p", postController.Create)
|
||||||
postGroup.PUT("/p", postController.Update)
|
postGroup.PUT("/p/:id", postController.Update)
|
||||||
postGroup.DELETE("/p", postController.Delete)
|
postGroup.DELETE("/p/:id", postController.Delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ func registerUserRoutes(group *route.RouterGroup) {
|
|||||||
userGroupWithoutAuth.GET("/oidc/list", userController.OidcList)
|
userGroupWithoutAuth.GET("/oidc/list", userController.OidcList)
|
||||||
userGroupWithoutAuth.GET("/oidc/login/:name", userController.OidcLogin)
|
userGroupWithoutAuth.GET("/oidc/login/:name", userController.OidcLogin)
|
||||||
userGroupWithoutAuth.GET("/u/:id", userController.GetUser)
|
userGroupWithoutAuth.GET("/u/:id", userController.GetUser)
|
||||||
userGroup.GET("/u", userController.GetUser)
|
userGroup.GET("/me", userController.GetUser)
|
||||||
userGroup.POST("/logout", userController.Logout)
|
userGroup.POST("/logout", userController.Logout)
|
||||||
userGroup.PUT("/u/:id", userController.UpdateUser)
|
userGroup.PUT("/u/:id", userController.UpdateUser)
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ func RegisterRoutes(h *server.Hertz) {
|
|||||||
registerAdminRoutes(apiV1Group)
|
registerAdminRoutes(apiV1Group)
|
||||||
registerFileRoutes(apiV1Group)
|
registerFileRoutes(apiV1Group)
|
||||||
registerLabelRoutes(apiV1Group)
|
registerLabelRoutes(apiV1Group)
|
||||||
|
registerLikeRoutes(apiV1Group)
|
||||||
registerPageRoutes(apiV1Group)
|
registerPageRoutes(apiV1Group)
|
||||||
registerPostRoutes(apiV1Group)
|
registerPostRoutes(apiV1Group)
|
||||||
registerUserRoutes(apiV1Group)
|
registerUserRoutes(apiV1Group)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
|
||||||
"github.com/cloudwego/hertz/pkg/app/server"
|
"github.com/cloudwego/hertz/pkg/app/server"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/snowykami/neo-blog/internal/router/apiv1"
|
"github.com/snowykami/neo-blog/internal/router/apiv1"
|
||||||
@ -25,5 +26,6 @@ func init() {
|
|||||||
server.WithHostPorts(":"+utils.Env.Get("PORT", "8888")),
|
server.WithHostPorts(":"+utils.Env.Get("PORT", "8888")),
|
||||||
server.WithMaxRequestBodySize(utils.Env.GetAsInt("MAX_REQUEST_BODY_SIZE", 1048576000)), // 1000MiB
|
server.WithMaxRequestBodySize(utils.Env.GetAsInt("MAX_REQUEST_BODY_SIZE", 1048576000)), // 1000MiB
|
||||||
)
|
)
|
||||||
|
h.Use(recovery.Recovery())
|
||||||
apiv1.RegisterRoutes(h)
|
apiv1.RegisterRoutes(h)
|
||||||
}
|
}
|
||||||
|
@ -1 +1,36 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/snowykami/neo-blog/internal/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommentService struct{}
|
||||||
|
|
||||||
|
func NewCommentService() *CommentService {
|
||||||
|
return &CommentService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *CommentService) CreateComment(ctx context.Context, req *dto.CreateCommentReq) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateCommentReq) error {
|
||||||
|
// Implementation for updating a comment
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *CommentService) DeleteComment(ctx context.Context, commentID string) error {
|
||||||
|
// Implementation for deleting a comment
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *CommentService) GetComment(ctx context.Context, commentID string) (*dto.CommentDto, error) {
|
||||||
|
// Implementation for getting a single comment
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommentListReq) ([]dto.CommentDto, error) {
|
||||||
|
// // Implementation for getting a list of comments
|
||||||
|
// return nil, nil
|
||||||
|
//}
|
||||||
|
@ -127,12 +127,37 @@ func (p *PostService) UpdatePost(ctx context.Context, id string, req *dto.Create
|
|||||||
func (p *PostService) ListPosts(ctx context.Context, req *dto.ListPostReq) ([]dto.PostDto, error) {
|
func (p *PostService) ListPosts(ctx context.Context, req *dto.ListPostReq) ([]dto.PostDto, error) {
|
||||||
postDtos := make([]dto.PostDto, 0)
|
postDtos := make([]dto.PostDto, 0)
|
||||||
currentUserID, _ := ctxutils.GetCurrentUserID(ctx)
|
currentUserID, _ := ctxutils.GetCurrentUserID(ctx)
|
||||||
posts, err := repo.Post.ListPosts(currentUserID, req.Keywords, req.Page, req.Size, req.OrderedBy, req.Reverse)
|
posts, err := repo.Post.ListPosts(currentUserID, req.Keywords, req.Page, req.Size, req.OrderBy, req.Desc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.New(errs.ErrInternalServer.Code, "failed to list posts", err)
|
return nil, errs.New(errs.ErrInternalServer.Code, "failed to list posts", err)
|
||||||
}
|
}
|
||||||
for _, post := range posts {
|
for _, post := range posts {
|
||||||
postDtos = append(postDtos, post.ToDto())
|
postDtos = append(postDtos, post.ToDtoWithShortContent(100))
|
||||||
}
|
}
|
||||||
return postDtos, nil
|
return postDtos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PostService) ToggleLikePost(ctx context.Context, id string) error {
|
||||||
|
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
||||||
|
if !ok {
|
||||||
|
return errs.ErrUnauthorized
|
||||||
|
}
|
||||||
|
if id == "" {
|
||||||
|
return errs.ErrBadRequest
|
||||||
|
}
|
||||||
|
post, err := repo.Post.GetPostByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return errs.New(errs.ErrNotFound.Code, "post not found", err)
|
||||||
|
}
|
||||||
|
if post.UserID == currentUser.ID {
|
||||||
|
return errs.ErrForbidden
|
||||||
|
}
|
||||||
|
idInt, err := strconv.ParseUint(id, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return errs.New(errs.ErrBadRequest.Code, "invalid post ID", err)
|
||||||
|
}
|
||||||
|
if err := repo.Post.ToggleLikePost(uint(idInt), currentUser.ID); err != nil {
|
||||||
|
return errs.ErrInternalServer
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package tasks
|
package tasks
|
||||||
|
|
||||||
// ClearSessionDaemon 定时任务:清理过期会话
|
// ClearSessionDaemon 定时任务:清理过期会话 TODO: 定期清理过期会话key
|
||||||
func ClearSessionDaemon() {}
|
func ClearSessionDaemon() {}
|
||||||
|
@ -27,18 +27,18 @@ const (
|
|||||||
DefaultBaseUrl = "http://localhost:3000" // 默认BaseUrl
|
DefaultBaseUrl = "http://localhost:3000" // 默认BaseUrl
|
||||||
TargetTypePost = "post"
|
TargetTypePost = "post"
|
||||||
TargetTypeComment = "comment"
|
TargetTypeComment = "comment"
|
||||||
OrderedByCreatedAt = "created_at" // 按创建时间排序
|
OrderByCreatedAt = "created_at" // 按创建时间排序
|
||||||
OrderedByUpdatedAt = "updated_at" // 按更新时间排序
|
OrderByUpdatedAt = "updated_at" // 按更新时间排序
|
||||||
OrderedByLikeCount = "like_count" // 按点赞数排序
|
OrderByLikeCount = "like_count" // 按点赞数排序
|
||||||
OrderedByCommentCount = "comment_count" // 按评论数排序
|
OrderByCommentCount = "comment_count" // 按评论数排序
|
||||||
OrderedByViewCount = "view_count" // 按浏览量排序
|
OrderByViewCount = "view_count" // 按浏览量排序
|
||||||
OrderedByHeat = "heat"
|
OrderByHeat = "heat"
|
||||||
HeatFactorViewWeight = 1 // 热度因子:浏览量权重
|
HeatFactorViewWeight = 1 // 热度因子:浏览量权重
|
||||||
HeatFactorLikeWeight = 5 // 热度因子:点赞权重
|
HeatFactorLikeWeight = 5 // 热度因子:点赞权重
|
||||||
HeatFactorCommentWeight = 10 // 热度因子:评论权重
|
HeatFactorCommentWeight = 10 // 热度因子:评论权重
|
||||||
|
PageLimitDefault = 20 // 默认分页大小
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
OrderedByEnumPost = []string{OrderedByCreatedAt, OrderedByUpdatedAt, OrderedByLikeCount, OrderedByCommentCount, OrderedByViewCount, OrderedByHeat} // 帖子可用的排序方式
|
OrderByEnumPost = []string{OrderByCreatedAt, OrderByUpdatedAt, OrderByLikeCount, OrderByCommentCount, OrderByViewCount, OrderByHeat} // 帖子可用的排序方式
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import { BACKEND_URL } from "@/api/client";
|
import { BACKEND_URL } from "@/api/client";
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
@ -31,4 +32,5 @@ const nextConfig: NextConfig = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export default nextConfig;
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
export default withNextIntl(nextConfig);
|
||||||
|
@ -16,14 +16,14 @@
|
|||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
"field-conv": "^1.0.9",
|
"field-conv": "^1.0.9",
|
||||||
"framer-motion": "^12.23.9",
|
"framer-motion": "^12.23.9",
|
||||||
"i18next": "^25.3.2",
|
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "15.4.1",
|
"next": "15.4.1",
|
||||||
|
"next-intl": "^4.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-i18next": "^15.6.1",
|
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
|
183
web/pnpm-lock.yaml
generated
183
web/pnpm-lock.yaml
generated
@ -29,30 +29,30 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
deepmerge:
|
||||||
|
specifier: ^4.3.1
|
||||||
|
version: 4.3.1
|
||||||
field-conv:
|
field-conv:
|
||||||
specifier: ^1.0.9
|
specifier: ^1.0.9
|
||||||
version: 1.0.9
|
version: 1.0.9
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.23.9
|
specifier: ^12.23.9
|
||||||
version: 12.23.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 12.23.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
i18next:
|
|
||||||
specifier: ^25.3.2
|
|
||||||
version: 25.3.2(typescript@5.8.3)
|
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.525.0
|
specifier: ^0.525.0
|
||||||
version: 0.525.0(react@19.1.0)
|
version: 0.525.0(react@19.1.0)
|
||||||
next:
|
next:
|
||||||
specifier: 15.4.1
|
specifier: 15.4.1
|
||||||
version: 15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
next-intl:
|
||||||
|
specifier: ^4.3.4
|
||||||
|
version: 4.3.4(next@15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||||
react:
|
react:
|
||||||
specifier: 19.1.0
|
specifier: 19.1.0
|
||||||
version: 19.1.0
|
version: 19.1.0
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: 19.1.0
|
specifier: 19.1.0
|
||||||
version: 19.1.0(react@19.1.0)
|
version: 19.1.0(react@19.1.0)
|
||||||
react-i18next:
|
|
||||||
specifier: ^15.6.1
|
|
||||||
version: 15.6.1(i18next@25.3.2(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
|
||||||
react-icons:
|
react-icons:
|
||||||
specifier: ^5.5.0
|
specifier: ^5.5.0
|
||||||
version: 5.5.0(react@19.1.0)
|
version: 5.5.0(react@19.1.0)
|
||||||
@ -101,10 +101,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
'@babel/runtime@7.27.6':
|
|
||||||
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@emnapi/core@1.4.4':
|
'@emnapi/core@1.4.4':
|
||||||
resolution: {integrity: sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==}
|
resolution: {integrity: sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==}
|
||||||
|
|
||||||
@ -152,6 +148,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==}
|
resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@formatjs/ecma402-abstract@2.3.4':
|
||||||
|
resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==}
|
||||||
|
|
||||||
|
'@formatjs/fast-memoize@2.2.7':
|
||||||
|
resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==}
|
||||||
|
|
||||||
|
'@formatjs/icu-messageformat-parser@2.11.2':
|
||||||
|
resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==}
|
||||||
|
|
||||||
|
'@formatjs/icu-skeleton-parser@1.8.14':
|
||||||
|
resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==}
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.5.10':
|
||||||
|
resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.6.1':
|
||||||
|
resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==}
|
||||||
|
|
||||||
'@humanfs/core@0.19.1':
|
'@humanfs/core@0.19.1':
|
||||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@ -631,6 +645,9 @@ packages:
|
|||||||
'@rushstack/eslint-patch@1.12.0':
|
'@rushstack/eslint-patch@1.12.0':
|
||||||
resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==}
|
resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==}
|
||||||
|
|
||||||
|
'@schummar/icu-type-parser@1.21.5':
|
||||||
|
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||||
|
|
||||||
@ -1097,9 +1114,16 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decimal.js@10.6.0:
|
||||||
|
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||||
|
|
||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
|
deepmerge@4.3.1:
|
||||||
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
define-data-property@1.1.4:
|
define-data-property@1.1.4:
|
||||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1449,17 +1473,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
html-parse-stringify@3.0.1:
|
|
||||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
|
||||||
|
|
||||||
i18next@25.3.2:
|
|
||||||
resolution: {integrity: sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA==}
|
|
||||||
peerDependencies:
|
|
||||||
typescript: ^5
|
|
||||||
peerDependenciesMeta:
|
|
||||||
typescript:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
ignore@5.3.2:
|
ignore@5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@ -1480,6 +1493,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
intl-messageformat@10.7.16:
|
||||||
|
resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==}
|
||||||
|
|
||||||
is-array-buffer@3.0.5:
|
is-array-buffer@3.0.5:
|
||||||
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1786,6 +1802,20 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
negotiator@1.0.0:
|
||||||
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
next-intl@4.3.4:
|
||||||
|
resolution: {integrity: sha512-VWLIDlGbnL/o4LnveJTJD1NOYN8lh3ZAGTWw2krhfgg53as3VsS4jzUVnArJdqvwtlpU/2BIDbWTZ7V4o1jFEw==}
|
||||||
|
peerDependencies:
|
||||||
|
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||||
|
typescript: ^5.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
next@15.4.1:
|
next@15.4.1:
|
||||||
resolution: {integrity: sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw==}
|
resolution: {integrity: sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw==}
|
||||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||||
@ -1915,22 +1945,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.1.0
|
react: ^19.1.0
|
||||||
|
|
||||||
react-i18next@15.6.1:
|
|
||||||
resolution: {integrity: sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==}
|
|
||||||
peerDependencies:
|
|
||||||
i18next: '>= 23.2.3'
|
|
||||||
react: '>= 16.8.0'
|
|
||||||
react-dom: '*'
|
|
||||||
react-native: '*'
|
|
||||||
typescript: ^5
|
|
||||||
peerDependenciesMeta:
|
|
||||||
react-dom:
|
|
||||||
optional: true
|
|
||||||
react-native:
|
|
||||||
optional: true
|
|
||||||
typescript:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
react-icons@5.5.0:
|
react-icons@5.5.0:
|
||||||
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
|
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2222,6 +2236,11 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
use-intl@4.3.4:
|
||||||
|
resolution: {integrity: sha512-sHfiU0QeJ1rirNWRxvCyvlSh9+NczcOzRnPyMeo2rtHXhVnBsvMRjE+UG4eh3lRhCxrvcqei/I0lBxsc59on1w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||||
|
|
||||||
use-sidecar@1.1.3:
|
use-sidecar@1.1.3:
|
||||||
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -2232,10 +2251,6 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
void-elements@3.1.0:
|
|
||||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -2278,8 +2293,6 @@ snapshots:
|
|||||||
'@jridgewell/gen-mapping': 0.3.12
|
'@jridgewell/gen-mapping': 0.3.12
|
||||||
'@jridgewell/trace-mapping': 0.3.29
|
'@jridgewell/trace-mapping': 0.3.29
|
||||||
|
|
||||||
'@babel/runtime@7.27.6': {}
|
|
||||||
|
|
||||||
'@emnapi/core@1.4.4':
|
'@emnapi/core@1.4.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.0.3
|
'@emnapi/wasi-threads': 1.0.3
|
||||||
@ -2340,6 +2353,36 @@ snapshots:
|
|||||||
'@eslint/core': 0.15.1
|
'@eslint/core': 0.15.1
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@formatjs/ecma402-abstract@2.3.4':
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/fast-memoize': 2.2.7
|
||||||
|
'@formatjs/intl-localematcher': 0.6.1
|
||||||
|
decimal.js: 10.6.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/fast-memoize@2.2.7':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/icu-messageformat-parser@2.11.2':
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/ecma402-abstract': 2.3.4
|
||||||
|
'@formatjs/icu-skeleton-parser': 1.8.14
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/icu-skeleton-parser@1.8.14':
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/ecma402-abstract': 2.3.4
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.5.10':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.6.1':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
'@humanfs/node@0.16.6':
|
'@humanfs/node@0.16.6':
|
||||||
@ -2719,6 +2762,8 @@ snapshots:
|
|||||||
|
|
||||||
'@rushstack/eslint-patch@1.12.0': {}
|
'@rushstack/eslint-patch@1.12.0': {}
|
||||||
|
|
||||||
|
'@schummar/icu-type-parser@1.21.5': {}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@ -3199,8 +3244,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decimal.js@10.6.0: {}
|
||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
define-data-property@1.1.4:
|
define-data-property@1.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-define-property: 1.0.1
|
es-define-property: 1.0.1
|
||||||
@ -3699,16 +3748,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
html-parse-stringify@3.0.1:
|
|
||||||
dependencies:
|
|
||||||
void-elements: 3.1.0
|
|
||||||
|
|
||||||
i18next@25.3.2(typescript@5.8.3):
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.27.6
|
|
||||||
optionalDependencies:
|
|
||||||
typescript: 5.8.3
|
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
@ -3726,6 +3765,13 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
|
||||||
|
intl-messageformat@10.7.16:
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/ecma402-abstract': 2.3.4
|
||||||
|
'@formatjs/fast-memoize': 2.2.7
|
||||||
|
'@formatjs/icu-messageformat-parser': 2.11.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
is-array-buffer@3.0.5:
|
is-array-buffer@3.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
@ -4003,6 +4049,18 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
|
next-intl@4.3.4(next@15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3):
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/intl-localematcher': 0.5.10
|
||||||
|
negotiator: 1.0.0
|
||||||
|
next: 15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
use-intl: 4.3.4(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.8.3
|
||||||
|
|
||||||
next@15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
next@15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 15.4.1
|
'@next/env': 15.4.1
|
||||||
@ -4140,16 +4198,6 @@ snapshots:
|
|||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
scheduler: 0.26.0
|
scheduler: 0.26.0
|
||||||
|
|
||||||
react-i18next@15.6.1(i18next@25.3.2(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3):
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.27.6
|
|
||||||
html-parse-stringify: 3.0.1
|
|
||||||
i18next: 25.3.2(typescript@5.8.3)
|
|
||||||
react: 19.1.0
|
|
||||||
optionalDependencies:
|
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
|
||||||
typescript: 5.8.3
|
|
||||||
|
|
||||||
react-icons@5.5.0(react@19.1.0):
|
react-icons@5.5.0(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@ -4541,6 +4589,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
|
|
||||||
|
use-intl@4.3.4(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/fast-memoize': 2.2.7
|
||||||
|
'@schummar/icu-type-parser': 1.21.5
|
||||||
|
intl-messageformat: 10.7.16
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
use-sidecar@1.1.3(@types/react@19.1.8)(react@19.1.0):
|
use-sidecar@1.1.3(@types/react@19.1.8)(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-node-es: 1.1.0
|
detect-node-es: 1.1.0
|
||||||
@ -4549,8 +4604,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
|
|
||||||
void-elements@3.1.0: {}
|
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-bigint: 1.1.0
|
is-bigint: 1.1.0
|
||||||
|
@ -5,8 +5,8 @@ import axiosClient from "./client";
|
|||||||
interface ListPostsParams {
|
interface ListPostsParams {
|
||||||
page?: number;
|
page?: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
orderedBy?: string;
|
orderBy?: string;
|
||||||
reverse?: boolean;
|
desc?: boolean;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,16 +24,16 @@ export async function getPostById(id: string): Promise<Post | null> {
|
|||||||
export async function listPosts({
|
export async function listPosts({
|
||||||
page = 1,
|
page = 1,
|
||||||
size = 10,
|
size = 10,
|
||||||
orderedBy = 'updated_at',
|
orderBy = 'updated_at',
|
||||||
reverse = false,
|
desc = false,
|
||||||
keywords = ''
|
keywords = ''
|
||||||
}: ListPostsParams = {}): Promise<BaseResponse<Post[]>> {
|
}: ListPostsParams = {}): Promise<BaseResponse<Post[]>> {
|
||||||
const res = await axiosClient.get<BaseResponse<Post[]>>("/post/list", {
|
const res = await axiosClient.get<BaseResponse<Post[]>>("/post/list", {
|
||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
size,
|
size,
|
||||||
orderedBy,
|
orderBy,
|
||||||
reverse,
|
desc,
|
||||||
keywords
|
keywords
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -43,4 +43,18 @@ export async function ListOidcConfigs(): Promise<BaseResponse<OidcConfig[]>> {
|
|||||||
"/user/oidc/list"
|
"/user/oidc/list"
|
||||||
);
|
);
|
||||||
return res.data;
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLoginUser(token: string = ""): Promise<BaseResponse<User>> {
|
||||||
|
const res = await axiosClient.get<BaseResponse<User>>("/user/me", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserById(id: number): Promise<BaseResponse<User>> {
|
||||||
|
const res = await axiosClient.get<BaseResponse<User>>(`/user/u/${id}`);
|
||||||
|
return res.data;
|
||||||
}
|
}
|
@ -2,6 +2,8 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { DeviceProvider } from "@/contexts/device-context";
|
import { DeviceProvider } from "@/contexts/device-context";
|
||||||
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
|
import { getLocale } from 'next-intl/server';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@ -29,7 +31,7 @@ export default function RootLayout({
|
|||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<DeviceProvider>
|
<DeviceProvider>
|
||||||
{children}
|
<NextIntlClientProvider>{children}</NextIntlClientProvider>
|
||||||
</DeviceProvider>
|
</DeviceProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -3,7 +3,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Calendar, Eye, Heart, MessageCircle, Lock } from "lucide-react";
|
import { Calendar, Eye, Heart, MessageCircle, Lock } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import config from "@/config";
|
import config from "@/config";
|
||||||
|
|
||||||
@ -23,27 +23,7 @@ export function BlogCard({ post, className }: BlogCardProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 计算阅读时间(估算)
|
// TODO: 阅读时间估计
|
||||||
// const getReadingTime = (content: string) => {
|
|
||||||
// const wordsPerMinute = 200;
|
|
||||||
// const wordCount = content.length;
|
|
||||||
// const minutes = Math.ceil(wordCount / wordsPerMinute);
|
|
||||||
// return `${minutes} 分钟阅读`;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // 根据内容类型获取图标
|
|
||||||
// const getContentTypeIcon = (type: Post['type']) => {
|
|
||||||
// switch (type) {
|
|
||||||
// case 'markdown':
|
|
||||||
// return '📝';
|
|
||||||
// case 'html':
|
|
||||||
// return '🌐';
|
|
||||||
// case 'text':
|
|
||||||
// return '📄';
|
|
||||||
// default:
|
|
||||||
// return '📝';
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cn(
|
<Card className={cn(
|
||||||
@ -131,7 +111,7 @@ export function BlogCard({ post, className }: BlogCardProps) {
|
|||||||
<CardTitle className="line-clamp-2 group-hover:text-primary transition-colors text-lg leading-tight">
|
<CardTitle className="line-clamp-2 group-hover:text-primary transition-colors text-lg leading-tight">
|
||||||
{post.title}
|
{post.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{/* Card Content - 主要内容 */}
|
{/* Card Content - 主要内容 */}
|
||||||
<CardContent className="flex-1">
|
<CardContent className="flex-1">
|
||||||
|
@ -5,8 +5,8 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import type { Label } from "@/models/label";
|
import type { Label } from "@/models/label";
|
||||||
import type { Post } from "@/models/post";
|
import type { Post } from "@/models/post";
|
||||||
import type configType from '@/config';
|
import type configType from '@/config';
|
||||||
import { t } from "i18next";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
// 侧边栏父组件,接收卡片组件列表
|
// 侧边栏父组件,接收卡片组件列表
|
||||||
export default function Sidebar({ cards }: { cards: React.ReactNode[] }) {
|
export default function Sidebar({ cards }: { cards: React.ReactNode[] }) {
|
||||||
@ -115,10 +115,11 @@ export function SidebarIframe(props?: { src?: string; scriptSrc?: string; title?
|
|||||||
title = "External Content",
|
title = "External Content",
|
||||||
height = "400px",
|
height = "400px",
|
||||||
} = props || {};
|
} = props || {};
|
||||||
|
const t = useTranslations('HomePage');
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t(title)}</CardTitle>
|
<CardTitle>{t("title")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<iframe
|
<iframe
|
||||||
|
@ -30,28 +30,28 @@ export default function BlogHome() {
|
|||||||
const fetchPosts = async () => {
|
const fetchPosts = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let orderedBy: string;
|
let orderBy: string;
|
||||||
let reverse: boolean;
|
let desc: boolean;
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case 'latest':
|
case 'latest':
|
||||||
orderedBy = 'updated_at';
|
orderBy = 'updated_at';
|
||||||
reverse = false;
|
desc = true;
|
||||||
break;
|
break;
|
||||||
case 'popular':
|
case 'popular':
|
||||||
orderedBy = 'heat';
|
orderBy = 'heat';
|
||||||
reverse = false;
|
desc = true;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
orderedBy = 'updated_at';
|
orderBy = 'updated_at';
|
||||||
reverse = false;
|
desc = true;
|
||||||
}
|
}
|
||||||
// 处理关键词,空格分割转逗号
|
// 处理关键词,空格分割转逗号
|
||||||
const keywords = debouncedSearch.trim() ? debouncedSearch.trim().split(/\s+/).join(",") : undefined;
|
const keywords = debouncedSearch.trim() ? debouncedSearch.trim().split(/\s+/).join(",") : undefined;
|
||||||
const data = await listPosts({
|
const data = await listPosts({
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
orderedBy,
|
orderBy: orderBy,
|
||||||
reverse,
|
desc: desc,
|
||||||
keywords
|
keywords
|
||||||
});
|
});
|
||||||
setPosts(data.data);
|
setPosts(data.data);
|
||||||
|
0
web/src/components/sidebar.tsx
Normal file
0
web/src/components/sidebar.tsx
Normal file
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
|
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
import i18n, { getDefaultLang } from "@/utils/i18n";
|
|
||||||
|
|
||||||
type Mode = "light" | "dark";
|
type Mode = "light" | "dark";
|
||||||
type Lang = string;
|
type Lang = string;
|
||||||
|
|
||||||
@ -12,8 +10,6 @@ interface DeviceContextProps {
|
|||||||
mode: Mode;
|
mode: Mode;
|
||||||
setMode: (mode: Mode) => void;
|
setMode: (mode: Mode) => void;
|
||||||
toggleMode: () => void;
|
toggleMode: () => void;
|
||||||
lang: Lang;
|
|
||||||
setLang: (lang: Lang) => void;
|
|
||||||
viewport: {
|
viewport: {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
@ -25,8 +21,6 @@ const DeviceContext = createContext<DeviceContextProps>({
|
|||||||
mode: "light",
|
mode: "light",
|
||||||
setMode: () => {},
|
setMode: () => {},
|
||||||
toggleMode: () => {},
|
toggleMode: () => {},
|
||||||
lang: "zh-cn",
|
|
||||||
setLang: () => {},
|
|
||||||
viewport: {
|
viewport: {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
@ -36,7 +30,6 @@ const DeviceContext = createContext<DeviceContextProps>({
|
|||||||
export const DeviceProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const DeviceProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [mode, setModeState] = useState<Mode>("light");
|
const [mode, setModeState] = useState<Mode>("light");
|
||||||
const [lang, setLangState] = useState<Lang>(getDefaultLang());
|
|
||||||
const [viewport, setViewport] = useState({
|
const [viewport, setViewport] = useState({
|
||||||
width: typeof window !== "undefined" ? window.innerWidth : 0,
|
width: typeof window !== "undefined" ? window.innerWidth : 0,
|
||||||
height: typeof window !== "undefined" ? window.innerHeight : 0,
|
height: typeof window !== "undefined" ? window.innerHeight : 0,
|
||||||
@ -92,15 +85,6 @@ export const DeviceProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 初始化语言
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const savedLang = localStorage.getItem("language") || getDefaultLang();
|
|
||||||
setLangState(savedLang);
|
|
||||||
i18n.changeLanguage(savedLang);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setMode = useCallback((newMode: Mode) => {
|
const setMode = useCallback((newMode: Mode) => {
|
||||||
setModeState(newMode);
|
setModeState(newMode);
|
||||||
document.documentElement.classList.toggle("dark", newMode === "dark");
|
document.documentElement.classList.toggle("dark", newMode === "dark");
|
||||||
@ -124,15 +108,9 @@ export const DeviceProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setLang = useCallback((newLang: Lang) => {
|
|
||||||
setLangState(newLang);
|
|
||||||
i18n.changeLanguage(newLang);
|
|
||||||
localStorage.setItem("language", newLang);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DeviceContext.Provider
|
<DeviceContext.Provider
|
||||||
value={{ isMobile, mode, setMode, toggleMode, lang, setLang, viewport }}
|
value={{ isMobile, mode, setMode, toggleMode, viewport }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</DeviceContext.Provider>
|
</DeviceContext.Provider>
|
||||||
|
47
web/src/i18n/request.ts
Normal file
47
web/src/i18n/request.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { getRequestConfig } from 'next-intl/server';
|
||||||
|
import { cookies, headers } from 'next/headers';
|
||||||
|
import deepmerge from 'deepmerge';
|
||||||
|
import { getLoginUser } from '@/api/user';
|
||||||
|
|
||||||
|
export default getRequestConfig(async () => {
|
||||||
|
const locales = await getUserLocales();
|
||||||
|
const messages = await Promise.all(
|
||||||
|
locales.map(async (locale) => {
|
||||||
|
try {
|
||||||
|
return (await import(`@/locales/${locale}.json`)).default;
|
||||||
|
} catch (error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).then((msgs) => msgs.reduce((acc, msg) => deepmerge(acc, msg), {}));
|
||||||
|
return {
|
||||||
|
locale: locales[0],
|
||||||
|
messages
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getUserLocales(): Promise<string[]> {
|
||||||
|
let locales: string[] = ["zh-CN", "zh", "en-US", "en"];
|
||||||
|
const headersList = await headers();
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
try {
|
||||||
|
const token = cookieStore.get('token')?.value || '';
|
||||||
|
const user = (await getLoginUser(token)).data;
|
||||||
|
locales.push(user.language);
|
||||||
|
locales.push(user.language.split('-')[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.info("获取用户信息失败,使用默认语言", error);
|
||||||
|
}
|
||||||
|
const languageInCookie = cookieStore.get('language')?.value;
|
||||||
|
if (languageInCookie) {
|
||||||
|
locales.push(languageInCookie);
|
||||||
|
locales.push(languageInCookie.split('-')[0]);
|
||||||
|
}
|
||||||
|
const acceptLanguage = headersList.get('accept-language');
|
||||||
|
if (acceptLanguage) {
|
||||||
|
const languages = acceptLanguage.split(',').map(lang => lang.split(';')[0]);
|
||||||
|
const languagesWithoutRegion = languages.map(lang => lang.split('-')[0]);
|
||||||
|
locales = [...new Set([...locales, ...languages, ...languagesWithoutRegion])];
|
||||||
|
}
|
||||||
|
return locales.reverse();
|
||||||
|
}
|
5
web/src/locales/en.json
Normal file
5
web/src/locales/en.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"HomePage": {
|
||||||
|
"title": "Hello world!"
|
||||||
|
}
|
||||||
|
}
|
5
web/src/locales/ja.json
Normal file
5
web/src/locales/ja.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"HomePage": {
|
||||||
|
"title": "Hello world!"
|
||||||
|
}
|
||||||
|
}
|
5
web/src/locales/zh-CN.json
Normal file
5
web/src/locales/zh-CN.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"HomePage": {
|
||||||
|
"title": "Hello world!"
|
||||||
|
}
|
||||||
|
}
|
@ -6,4 +6,5 @@ export interface User {
|
|||||||
email: string;
|
email: string;
|
||||||
gender: string;
|
gender: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
language: string;
|
||||||
}
|
}
|
@ -1,27 +0,0 @@
|
|||||||
import { initReactI18next } from "react-i18next";
|
|
||||||
|
|
||||||
import i18n from "i18next";
|
|
||||||
|
|
||||||
import resources from "./locales";
|
|
||||||
|
|
||||||
export const getDefaultLang = () => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
return (
|
|
||||||
localStorage.getItem("language") ||
|
|
||||||
navigator.language.replace("_", "-") || // 保证格式
|
|
||||||
"zh-CN"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return "zh-CN";
|
|
||||||
};
|
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
|
||||||
resources: resources,
|
|
||||||
lng: getDefaultLang(),
|
|
||||||
fallbackLng: "zh-CN",
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default i18n;
|
|
@ -1,30 +0,0 @@
|
|||||||
const resources = {
|
|
||||||
translation: {
|
|
||||||
name: "English",
|
|
||||||
hello: "Hello",
|
|
||||||
login: {
|
|
||||||
login: "Login",
|
|
||||||
failed: "Login failed",
|
|
||||||
forgotPassword: "Forgot password?",
|
|
||||||
username: "Username",
|
|
||||||
usernameOrEmail: "Username or Email",
|
|
||||||
password: "Password",
|
|
||||||
remember: "Remember this device",
|
|
||||||
captcha: {
|
|
||||||
no: "No captcha required",
|
|
||||||
failed: "Captcha verification failed, please try again",
|
|
||||||
fetchFailed: "Failed to fetch captcha, please try again later",
|
|
||||||
processing: "Waiting for verification...",
|
|
||||||
reCaptchaProcessing: "Processing reCAPTCHA verification, please wait...",
|
|
||||||
reCaptchaFailed: "reCAPTCHA verification failed, please try again",
|
|
||||||
reCaptchaSuccess: "reCAPTCHA verification successful",
|
|
||||||
},
|
|
||||||
oidc: {
|
|
||||||
fetchFailed: "Failed to fetch OIDC providers, please try again later",
|
|
||||||
use: "Login with {{provider}}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default resources;
|
|
@ -1,9 +0,0 @@
|
|||||||
import enUS from "./en-us";
|
|
||||||
import zhCN from "./zh-cn";
|
|
||||||
|
|
||||||
const resources = {
|
|
||||||
"zh-CN": zhCN,
|
|
||||||
"en-US": enUS,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default resources;
|
|
@ -1,30 +0,0 @@
|
|||||||
const resources = {
|
|
||||||
translation: {
|
|
||||||
name: "中文",
|
|
||||||
hello: "你好",
|
|
||||||
login: {
|
|
||||||
login: "登录",
|
|
||||||
failed: "登录失败",
|
|
||||||
forgotPassword: "忘了密码?",
|
|
||||||
username: "用户名",
|
|
||||||
usernameOrEmail: "用户名或邮箱",
|
|
||||||
password: "密码",
|
|
||||||
remember: "记住这个设备",
|
|
||||||
captcha: {
|
|
||||||
no: "无需进行机器人挑战",
|
|
||||||
failed: "机器人挑战失败,请重试",
|
|
||||||
fetchFailed: "获取验证码失败,请稍后再试",
|
|
||||||
processing: "等待验证...",
|
|
||||||
reCaptchaProcessing: "正在处理 reCAPTCHA 验证,请稍候...",
|
|
||||||
reCaptchaFailed: "reCAPTCHA 验证失败,请重试",
|
|
||||||
reCaptchaSuccess: "reCAPTCHA 验证成功",
|
|
||||||
},
|
|
||||||
oidc: {
|
|
||||||
fetchFailed: "获取 OIDC 提供商失败,请稍后再试",
|
|
||||||
use: "使用 {{provider}} 登录",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default resources;
|
|
Reference in New Issue
Block a user