feat(editor): update

This commit is contained in:
2025-12-31 14:50:18 +08:00
parent 20e1a2fa19
commit 984770c6a1
14 changed files with 303 additions and 117 deletions

View File

@@ -37,7 +37,7 @@ func (c *Common) Upload(
if typeArg != nil { if typeArg != nil {
val = *typeArg val = *typeArg
} }
return services.Common.Upload(ctx.Context(), user.ID, file, val) return services.Common.Upload(ctx, user.ID, file, val)
} }
// Get options (enums) // Get options (enums)
@@ -50,5 +50,5 @@ func (c *Common) Upload(
// @Produce json // @Produce json
// @Success 200 {object} dto.OptionsResponse // @Success 200 {object} dto.OptionsResponse
func (c *Common) GetOptions(ctx fiber.Ctx) (*dto.OptionsResponse, error) { func (c *Common) GetOptions(ctx fiber.Ctx) (*dto.OptionsResponse, error) {
return services.Common.Options(ctx.Context()) return services.Common.Options(ctx)
} }

View File

@@ -48,7 +48,7 @@ func (c *Content) List(
// @Bind id path // @Bind id path
func (c *Content) Get(ctx fiber.Ctx, id string) (*dto.ContentDetail, error) { func (c *Content) Get(ctx fiber.Ctx, id string) (*dto.ContentDetail, error) {
uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser)) uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser))
return services.Content.Get(ctx.Context(), uid, id) return services.Content.Get(ctx, uid, id)
} }
// Get comments for a content // Get comments for a content
@@ -66,7 +66,7 @@ func (c *Content) Get(ctx fiber.Ctx, id string) (*dto.ContentDetail, error) {
// @Bind page query // @Bind page query
func (c *Content) ListComments(ctx fiber.Ctx, id string, page int) (*requests.Pager, error) { func (c *Content) ListComments(ctx fiber.Ctx, id string, page int) (*requests.Pager, error) {
uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser)) uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser))
return services.Content.ListComments(ctx.Context(), uid, id, page) return services.Content.ListComments(ctx, uid, id, page)
} }
// Post a comment // Post a comment
@@ -84,7 +84,7 @@ func (c *Content) ListComments(ctx fiber.Ctx, id string, page int) (*requests.Pa
// @Bind form body // @Bind form body
func (c *Content) CreateComment(ctx fiber.Ctx, id string, form *dto.CommentCreateForm) error { func (c *Content) CreateComment(ctx fiber.Ctx, id string, form *dto.CommentCreateForm) error {
uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser)) uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser))
return services.Content.CreateComment(ctx.Context(), uid, id, form) return services.Content.CreateComment(ctx, uid, id, form)
} }
// Like a comment // Like a comment
@@ -100,7 +100,7 @@ func (c *Content) CreateComment(ctx fiber.Ctx, id string, form *dto.CommentCreat
// @Bind id path // @Bind id path
func (c *Content) LikeComment(ctx fiber.Ctx, id string) error { func (c *Content) LikeComment(ctx fiber.Ctx, id string) error {
uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser)) uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser))
return services.Content.LikeComment(ctx.Context(), uid, id) return services.Content.LikeComment(ctx, uid, id)
} }
// List curated topics // List curated topics

View File

@@ -24,7 +24,7 @@ type Creator struct{}
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind form body // @Bind form body
func (c *Creator) Apply(ctx fiber.Ctx, user *models.User, form *dto.ApplyForm) error { func (c *Creator) Apply(ctx fiber.Ctx, user *models.User, form *dto.ApplyForm) error {
return services.Creator.Apply(ctx.Context(), user.ID, form) return services.Creator.Apply(ctx, user.ID, form)
} }
// Get creator dashboard stats // Get creator dashboard stats
@@ -38,7 +38,23 @@ func (c *Creator) Apply(ctx fiber.Ctx, user *models.User, form *dto.ApplyForm) e
// @Success 200 {object} dto.DashboardStats // @Success 200 {object} dto.DashboardStats
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
func (c *Creator) Dashboard(ctx fiber.Ctx, user *models.User) (*dto.DashboardStats, error) { func (c *Creator) Dashboard(ctx fiber.Ctx, user *models.User) (*dto.DashboardStats, error) {
return services.Creator.Dashboard(ctx.Context(), user.ID) return services.Creator.Dashboard(ctx, user.ID)
}
// Get content details for edit
//
// @Router /v1/creator/contents/:id [get]
// @Summary Get content
// @Description Get content details for edit
// @Tags CreatorCenter
// @Accept json
// @Produce json
// @Param id path string true "Content ID"
// @Success 200 {object} dto.ContentEditDTO
// @Bind user local key(__ctx_user)
// @Bind id path
func (c *Creator) GetContent(ctx fiber.Ctx, user *models.User, id string) (*dto.ContentEditDTO, error) {
return services.Creator.GetContent(ctx, user.ID, id)
} }
// List creator contents // List creator contents
@@ -60,7 +76,7 @@ func (c *Creator) ListContents(
user *models.User, user *models.User,
filter *dto.CreatorContentListFilter, filter *dto.CreatorContentListFilter,
) ([]dto.ContentItem, error) { ) ([]dto.ContentItem, error) {
return services.Creator.ListContents(ctx.Context(), user.ID, filter) return services.Creator.ListContents(ctx, user.ID, filter)
} }
// Create/Publish content // Create/Publish content
@@ -76,7 +92,7 @@ func (c *Creator) ListContents(
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind form body // @Bind form body
func (c *Creator) CreateContent(ctx fiber.Ctx, user *models.User, form *dto.ContentCreateForm) error { func (c *Creator) CreateContent(ctx fiber.Ctx, user *models.User, form *dto.ContentCreateForm) error {
return services.Creator.CreateContent(ctx.Context(), user.ID, form) return services.Creator.CreateContent(ctx, user.ID, form)
} }
// Update content // Update content
@@ -94,7 +110,7 @@ func (c *Creator) CreateContent(ctx fiber.Ctx, user *models.User, form *dto.Cont
// @Bind id path // @Bind id path
// @Bind form body // @Bind form body
func (c *Creator) UpdateContent(ctx fiber.Ctx, user *models.User, id string, form *dto.ContentUpdateForm) error { func (c *Creator) UpdateContent(ctx fiber.Ctx, user *models.User, id string, form *dto.ContentUpdateForm) error {
return services.Creator.UpdateContent(ctx.Context(), user.ID, id, form) return services.Creator.UpdateContent(ctx, user.ID, id, form)
} }
// Delete content // Delete content
@@ -110,7 +126,7 @@ func (c *Creator) UpdateContent(ctx fiber.Ctx, user *models.User, id string, for
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind id path // @Bind id path
func (c *Creator) DeleteContent(ctx fiber.Ctx, user *models.User, id string) error { func (c *Creator) DeleteContent(ctx fiber.Ctx, user *models.User, id string) error {
return services.Creator.DeleteContent(ctx.Context(), user.ID, id) return services.Creator.DeleteContent(ctx, user.ID, id)
} }
// List sales orders // List sales orders
@@ -131,7 +147,7 @@ func (c *Creator) ListOrders(
user *models.User, user *models.User,
filter *dto.CreatorOrderListFilter, filter *dto.CreatorOrderListFilter,
) ([]dto.Order, error) { ) ([]dto.Order, error) {
return services.Creator.ListOrders(ctx.Context(), user.ID, filter) return services.Creator.ListOrders(ctx, user.ID, filter)
} }
// Process refund // Process refund
@@ -149,7 +165,7 @@ func (c *Creator) ListOrders(
// @Bind id path // @Bind id path
// @Bind form body // @Bind form body
func (c *Creator) Refund(ctx fiber.Ctx, user *models.User, id string, form *dto.RefundForm) error { func (c *Creator) Refund(ctx fiber.Ctx, user *models.User, id string, form *dto.RefundForm) error {
return services.Creator.ProcessRefund(ctx.Context(), user.ID, id, form) return services.Creator.ProcessRefund(ctx, user.ID, id, form)
} }
// Get channel settings // Get channel settings
@@ -163,7 +179,7 @@ func (c *Creator) Refund(ctx fiber.Ctx, user *models.User, id string, form *dto.
// @Success 200 {object} dto.Settings // @Success 200 {object} dto.Settings
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
func (c *Creator) GetSettings(ctx fiber.Ctx, user *models.User) (*dto.Settings, error) { func (c *Creator) GetSettings(ctx fiber.Ctx, user *models.User) (*dto.Settings, error) {
return services.Creator.GetSettings(ctx.Context(), user.ID) return services.Creator.GetSettings(ctx, user.ID)
} }
// Update channel settings // Update channel settings
@@ -179,7 +195,7 @@ func (c *Creator) GetSettings(ctx fiber.Ctx, user *models.User) (*dto.Settings,
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind form body // @Bind form body
func (c *Creator) UpdateSettings(ctx fiber.Ctx, user *models.User, form *dto.Settings) error { func (c *Creator) UpdateSettings(ctx fiber.Ctx, user *models.User, form *dto.Settings) error {
return services.Creator.UpdateSettings(ctx.Context(), user.ID, form) return services.Creator.UpdateSettings(ctx, user.ID, form)
} }
// List payout accounts // List payout accounts
@@ -193,7 +209,7 @@ func (c *Creator) UpdateSettings(ctx fiber.Ctx, user *models.User, form *dto.Set
// @Success 200 {array} dto.PayoutAccount // @Success 200 {array} dto.PayoutAccount
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
func (c *Creator) ListPayoutAccounts(ctx fiber.Ctx, user *models.User) ([]dto.PayoutAccount, error) { func (c *Creator) ListPayoutAccounts(ctx fiber.Ctx, user *models.User) ([]dto.PayoutAccount, error) {
return services.Creator.ListPayoutAccounts(ctx.Context(), user.ID) return services.Creator.ListPayoutAccounts(ctx, user.ID)
} }
// Add payout account // Add payout account
@@ -209,7 +225,7 @@ func (c *Creator) ListPayoutAccounts(ctx fiber.Ctx, user *models.User) ([]dto.Pa
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind form body // @Bind form body
func (c *Creator) AddPayoutAccount(ctx fiber.Ctx, user *models.User, form *dto.PayoutAccount) error { func (c *Creator) AddPayoutAccount(ctx fiber.Ctx, user *models.User, form *dto.PayoutAccount) error {
return services.Creator.AddPayoutAccount(ctx.Context(), user.ID, form) return services.Creator.AddPayoutAccount(ctx, user.ID, form)
} }
// Remove payout account // Remove payout account
@@ -225,7 +241,7 @@ func (c *Creator) AddPayoutAccount(ctx fiber.Ctx, user *models.User, form *dto.P
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind id query // @Bind id query
func (c *Creator) RemovePayoutAccount(ctx fiber.Ctx, user *models.User, id string) error { func (c *Creator) RemovePayoutAccount(ctx fiber.Ctx, user *models.User, id string) error {
return services.Creator.RemovePayoutAccount(ctx.Context(), user.ID, id) return services.Creator.RemovePayoutAccount(ctx, user.ID, id)
} }
// Request withdrawal // Request withdrawal
@@ -241,5 +257,5 @@ func (c *Creator) RemovePayoutAccount(ctx fiber.Ctx, user *models.User, id strin
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind form body // @Bind form body
func (c *Creator) Withdraw(ctx fiber.Ctx, user *models.User, form *dto.WithdrawForm) error { func (c *Creator) Withdraw(ctx fiber.Ctx, user *models.User, form *dto.WithdrawForm) error {
return services.Creator.Withdraw(ctx.Context(), user.ID, form) return services.Creator.Withdraw(ctx, user.ID, form)
} }

View File

@@ -39,6 +39,28 @@ type ContentUpdateForm struct {
MediaIDs []string `json:"media_ids"` MediaIDs []string `json:"media_ids"`
} }
type ContentEditDTO struct {
ID string `json:"id"`
Title string `json:"title"`
Genre string `json:"genre"`
Description string `json:"description"`
Status string `json:"status"`
Price float64 `json:"price"`
EnableTrial bool `json:"enable_trial"`
PreviewSeconds int `json:"preview_seconds"`
Assets []AssetDTO `json:"assets"`
}
type AssetDTO struct {
ID string `json:"id"`
Role string `json:"role"`
Type string `json:"type"`
URL string `json:"url"`
Name string `json:"name"`
Size string `json:"size"`
Sort int `json:"sort"`
}
type CreatorContentListFilter struct { type CreatorContentListFilter struct {
requests.Pagination requests.Pagination
Status *string `query:"status"` Status *string `query:"status"`

View File

@@ -106,6 +106,12 @@ func (r *Routes) Register(router fiber.Router) {
Local[*models.User]("__ctx_user"), Local[*models.User]("__ctx_user"),
QueryParam[string]("id"), QueryParam[string]("id"),
)) ))
r.log.Debugf("Registering route: Get /v1/creator/contents/:id -> creator.GetContent")
router.Get("/v1/creator/contents/:id"[len(r.Path()):], DataFunc2(
r.creator.GetContent,
Local[*models.User]("__ctx_user"),
PathParam[string]("id"),
))
r.log.Debugf("Registering route: Get /v1/creator/contents -> creator.ListContents") r.log.Debugf("Registering route: Get /v1/creator/contents -> creator.ListContents")
router.Get("/v1/creator/contents"[len(r.Path()):], DataFunc2( router.Get("/v1/creator/contents"[len(r.Path()):], DataFunc2(
r.creator.ListContents, r.creator.ListContents,

View File

@@ -28,7 +28,7 @@ func (t *Tenant) Get(ctx fiber.Ctx, user *models.User, id string) (*dto.TenantPr
if user != nil { if user != nil {
uid = user.ID uid = user.ID
} }
return services.Tenant.GetPublicProfile(ctx.Context(), uid, id) return services.Tenant.GetPublicProfile(ctx, uid, id)
} }
// Follow a tenant // Follow a tenant
@@ -44,7 +44,7 @@ func (t *Tenant) Get(ctx fiber.Ctx, user *models.User, id string) (*dto.TenantPr
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind id path // @Bind id path
func (t *Tenant) Follow(ctx fiber.Ctx, user *models.User, id string) error { func (t *Tenant) Follow(ctx fiber.Ctx, user *models.User, id string) error {
return services.Tenant.Follow(ctx.Context(), user.ID, id) return services.Tenant.Follow(ctx, user.ID, id)
} }
// Unfollow a tenant // Unfollow a tenant
@@ -60,5 +60,5 @@ func (t *Tenant) Follow(ctx fiber.Ctx, user *models.User, id string) error {
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind id path // @Bind id path
func (t *Tenant) Unfollow(ctx fiber.Ctx, user *models.User, id string) error { func (t *Tenant) Unfollow(ctx fiber.Ctx, user *models.User, id string) error {
return services.Tenant.Unfollow(ctx.Context(), user.ID, id) return services.Tenant.Unfollow(ctx, user.ID, id)
} }

View File

@@ -28,7 +28,7 @@ func (t *Transaction) Create(
user *models.User, user *models.User,
form *dto.OrderCreateForm, form *dto.OrderCreateForm,
) (*dto.OrderCreateResponse, error) { ) (*dto.OrderCreateResponse, error) {
return services.Order.Create(ctx.Context(), user.ID, form) return services.Order.Create(ctx, user.ID, form)
} }
// Pay for order // Pay for order
@@ -51,7 +51,7 @@ func (t *Transaction) Pay(
id string, id string,
form *dto.OrderPayForm, form *dto.OrderPayForm,
) (*dto.OrderPayResponse, error) { ) (*dto.OrderPayResponse, error) {
return services.Order.Pay(ctx.Context(), user.ID, id, form) return services.Order.Pay(ctx, user.ID, id, form)
} }
// Check order payment status // Check order payment status

View File

@@ -41,7 +41,7 @@ func (u *User) Me(ctx fiber.Ctx, user *models.User) (*auth_dto.User, error) {
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind form body // @Bind form body
func (u *User) Update(ctx fiber.Ctx, user *models.User, form *dto.UserUpdate) error { func (u *User) Update(ctx fiber.Ctx, user *models.User, form *dto.UserUpdate) error {
return services.User.Update(ctx.Context(), user.ID, form) return services.User.Update(ctx, user.ID, form)
} }
// Submit real-name authentication // Submit real-name authentication
@@ -57,7 +57,7 @@ func (u *User) Update(ctx fiber.Ctx, user *models.User, form *dto.UserUpdate) er
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind form body // @Bind form body
func (u *User) RealName(ctx fiber.Ctx, user *models.User, form *dto.RealNameForm) error { func (u *User) RealName(ctx fiber.Ctx, user *models.User, form *dto.RealNameForm) error {
return services.User.RealName(ctx.Context(), user.ID, form) return services.User.RealName(ctx, user.ID, form)
} }
// Get wallet balance and transactions // Get wallet balance and transactions
@@ -71,7 +71,7 @@ func (u *User) RealName(ctx fiber.Ctx, user *models.User, form *dto.RealNameForm
// @Success 200 {object} dto.WalletResponse // @Success 200 {object} dto.WalletResponse
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
func (u *User) Wallet(ctx fiber.Ctx, user *models.User) (*dto.WalletResponse, error) { func (u *User) Wallet(ctx fiber.Ctx, user *models.User) (*dto.WalletResponse, error) {
return services.Wallet.GetWallet(ctx.Context(), user.ID) return services.Wallet.GetWallet(ctx, user.ID)
} }
// Recharge wallet // Recharge wallet
@@ -87,7 +87,7 @@ func (u *User) Wallet(ctx fiber.Ctx, user *models.User) (*dto.WalletResponse, er
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind form body // @Bind form body
func (u *User) Recharge(ctx fiber.Ctx, user *models.User, form *dto.RechargeForm) (*dto.RechargeResponse, error) { func (u *User) Recharge(ctx fiber.Ctx, user *models.User, form *dto.RechargeForm) (*dto.RechargeResponse, error) {
return services.Wallet.Recharge(ctx.Context(), user.ID, form) return services.Wallet.Recharge(ctx, user.ID, form)
} }
// List user orders // List user orders
@@ -103,7 +103,7 @@ func (u *User) Recharge(ctx fiber.Ctx, user *models.User, form *dto.RechargeForm
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind status query // @Bind status query
func (u *User) ListOrders(ctx fiber.Ctx, user *models.User, status string) ([]dto.Order, error) { func (u *User) ListOrders(ctx fiber.Ctx, user *models.User, status string) ([]dto.Order, error) {
return services.Order.ListUserOrders(ctx.Context(), user.ID, status) return services.Order.ListUserOrders(ctx, user.ID, status)
} }
// Get user order detail // Get user order detail
@@ -119,7 +119,7 @@ func (u *User) ListOrders(ctx fiber.Ctx, user *models.User, status string) ([]dt
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind id path // @Bind id path
func (u *User) GetOrder(ctx fiber.Ctx, user *models.User, id string) (*dto.Order, error) { func (u *User) GetOrder(ctx fiber.Ctx, user *models.User, id string) (*dto.Order, error) {
return services.Order.GetUserOrder(ctx.Context(), user.ID, id) return services.Order.GetUserOrder(ctx, user.ID, id)
} }
// Get purchased content // Get purchased content
@@ -133,7 +133,7 @@ func (u *User) GetOrder(ctx fiber.Ctx, user *models.User, id string) (*dto.Order
// @Success 200 {array} dto.ContentItem // @Success 200 {array} dto.ContentItem
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
func (u *User) Library(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error) { func (u *User) Library(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error) {
return services.Content.GetLibrary(ctx.Context(), user.ID) return services.Content.GetLibrary(ctx, user.ID)
} }
// Get favorites // Get favorites
@@ -147,7 +147,7 @@ func (u *User) Library(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, err
// @Success 200 {array} dto.ContentItem // @Success 200 {array} dto.ContentItem
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
func (u *User) Favorites(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error) { func (u *User) Favorites(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error) {
return services.Content.GetFavorites(ctx.Context(), user.ID) return services.Content.GetFavorites(ctx, user.ID)
} }
// Add to favorites // Add to favorites
@@ -163,7 +163,7 @@ func (u *User) Favorites(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, e
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind contentId query // @Bind contentId query
func (u *User) AddFavorite(ctx fiber.Ctx, user *models.User, contentId string) error { func (u *User) AddFavorite(ctx fiber.Ctx, user *models.User, contentId string) error {
return services.Content.AddFavorite(ctx.Context(), user.ID, contentId) return services.Content.AddFavorite(ctx, user.ID, contentId)
} }
// Remove from favorites // Remove from favorites
@@ -179,7 +179,7 @@ func (u *User) AddFavorite(ctx fiber.Ctx, user *models.User, contentId string) e
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind contentId path // @Bind contentId path
func (u *User) RemoveFavorite(ctx fiber.Ctx, user *models.User, contentId string) error { func (u *User) RemoveFavorite(ctx fiber.Ctx, user *models.User, contentId string) error {
return services.Content.RemoveFavorite(ctx.Context(), user.ID, contentId) return services.Content.RemoveFavorite(ctx, user.ID, contentId)
} }
// Get liked contents // Get liked contents
@@ -193,7 +193,7 @@ func (u *User) RemoveFavorite(ctx fiber.Ctx, user *models.User, contentId string
// @Success 200 {array} dto.ContentItem // @Success 200 {array} dto.ContentItem
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
func (u *User) Likes(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error) { func (u *User) Likes(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error) {
return services.Content.GetLikes(ctx.Context(), user.ID) return services.Content.GetLikes(ctx, user.ID)
} }
// Like content // Like content
@@ -209,7 +209,7 @@ func (u *User) Likes(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind contentId query // @Bind contentId query
func (u *User) AddLike(ctx fiber.Ctx, user *models.User, contentId string) error { func (u *User) AddLike(ctx fiber.Ctx, user *models.User, contentId string) error {
return services.Content.AddLike(ctx.Context(), user.ID, contentId) return services.Content.AddLike(ctx, user.ID, contentId)
} }
// Unlike content // Unlike content
@@ -225,7 +225,7 @@ func (u *User) AddLike(ctx fiber.Ctx, user *models.User, contentId string) error
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind contentId path // @Bind contentId path
func (u *User) RemoveLike(ctx fiber.Ctx, user *models.User, contentId string) error { func (u *User) RemoveLike(ctx fiber.Ctx, user *models.User, contentId string) error {
return services.Content.RemoveLike(ctx.Context(), user.ID, contentId) return services.Content.RemoveLike(ctx, user.ID, contentId)
} }
// Get following tenants // Get following tenants
@@ -239,7 +239,7 @@ func (u *User) RemoveLike(ctx fiber.Ctx, user *models.User, contentId string) er
// @Success 200 {array} dto.TenantProfile // @Success 200 {array} dto.TenantProfile
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
func (u *User) Following(ctx fiber.Ctx, user *models.User) ([]dto.TenantProfile, error) { func (u *User) Following(ctx fiber.Ctx, user *models.User) ([]dto.TenantProfile, error) {
return services.Tenant.ListFollowed(ctx.Context(), user.ID) return services.Tenant.ListFollowed(ctx, user.ID)
} }
// Get notifications // Get notifications
@@ -257,7 +257,7 @@ func (u *User) Following(ctx fiber.Ctx, user *models.User) ([]dto.TenantProfile,
// @Bind typeArg query key(type) // @Bind typeArg query key(type)
// @Bind page query // @Bind page query
func (u *User) Notifications(ctx fiber.Ctx, user *models.User, typeArg string, page int) (*requests.Pager, error) { func (u *User) Notifications(ctx fiber.Ctx, user *models.User, typeArg string, page int) (*requests.Pager, error) {
return services.Notification.List(ctx.Context(), user.ID, page, typeArg) return services.Notification.List(ctx, user.ID, page, typeArg)
} }
// List my coupons // List my coupons
@@ -273,5 +273,5 @@ func (u *User) Notifications(ctx fiber.Ctx, user *models.User, typeArg string, p
// @Bind user local key(__ctx_user) // @Bind user local key(__ctx_user)
// @Bind status query // @Bind status query
func (u *User) MyCoupons(ctx fiber.Ctx, user *models.User, status string) ([]dto.UserCouponItem, error) { func (u *User) MyCoupons(ctx fiber.Ctx, user *models.User, status string) ([]dto.UserCouponItem, error) {
return services.Coupon.ListUserCoupons(ctx.Context(), user.ID, status) return services.Coupon.ListUserCoupons(ctx, user.ID, status)
} }

View File

@@ -36,7 +36,7 @@ func (m *Middlewares) Auth(ctx fiber.Ctx) error {
} }
// get user model // get user model
user, err := services.User.GetModelByID(ctx.Context(), claims.UserID) user, err := services.User.GetModelByID(ctx, claims.UserID)
if err != nil { if err != nil {
return errorx.ErrUnauthorized.WithCause(err).WithMsg("UserNotFound") return errorx.ErrUnauthorized.WithCause(err).WithMsg("UserNotFound")
} }

View File

@@ -94,7 +94,11 @@ func (s *creator) Dashboard(ctx context.Context, userID int64) (*creator_dto.Das
return stats, nil return stats, nil
} }
func (s *creator) ListContents(ctx context.Context, userID int64, filter *creator_dto.CreatorContentListFilter) ([]creator_dto.ContentItem, error) { func (s *creator) ListContents(
ctx context.Context,
userID int64,
filter *creator_dto.CreatorContentListFilter,
) ([]creator_dto.ContentItem, error) {
tid, err := s.getTenantID(ctx, userID) tid, err := s.getTenantID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -186,7 +190,12 @@ func (s *creator) CreateContent(ctx context.Context, userID int64, form *creator
}) })
} }
func (s *creator) UpdateContent(ctx context.Context, userID int64, id string, form *creator_dto.ContentUpdateForm) error { func (s *creator) UpdateContent(
ctx context.Context,
userID int64,
id string,
form *creator_dto.ContentUpdateForm,
) error {
tid, err := s.getTenantID(ctx, userID) tid, err := s.getTenantID(ctx, userID)
if err != nil { if err != nil {
return err return err
@@ -215,7 +224,9 @@ func (s *creator) UpdateContent(ctx context.Context, userID int64, id string, fo
count, _ := tx.ContentPrice.WithContext(ctx).Where(tx.ContentPrice.ContentID.Eq(cid)).Count() count, _ := tx.ContentPrice.WithContext(ctx).Where(tx.ContentPrice.ContentID.Eq(cid)).Count()
newPrice := int64(form.Price * 100) newPrice := int64(form.Price * 100)
if count > 0 { if count > 0 {
_, err = tx.ContentPrice.WithContext(ctx).Where(tx.ContentPrice.ContentID.Eq(cid)).UpdateSimple(tx.ContentPrice.PriceAmount.Value(newPrice)) _, err = tx.ContentPrice.WithContext(ctx).
Where(tx.ContentPrice.ContentID.Eq(cid)).
UpdateSimple(tx.ContentPrice.PriceAmount.Value(newPrice))
} else { } else {
err = tx.ContentPrice.WithContext(ctx).Create(&models.ContentPrice{ err = tx.ContentPrice.WithContext(ctx).Create(&models.ContentPrice{
TenantID: tid, TenantID: tid,
@@ -263,14 +274,83 @@ func (s *creator) DeleteContent(ctx context.Context, userID int64, id string) er
return err return err
} }
_, err = models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid), models.ContentQuery.TenantID.Eq(tid)).Delete() _, err = models.ContentQuery.WithContext(ctx).
Where(models.ContentQuery.ID.Eq(cid), models.ContentQuery.TenantID.Eq(tid)).
Delete()
if err != nil { if err != nil {
return errorx.ErrDatabaseError.WithCause(err) return errorx.ErrDatabaseError.WithCause(err)
} }
return nil return nil
} }
func (s *creator) ListOrders(ctx context.Context, userID int64, filter *creator_dto.CreatorOrderListFilter) ([]creator_dto.Order, error) { func (s *creator) GetContent(ctx context.Context, userID int64, id string) (*creator_dto.ContentEditDTO, error) {
tid, err := s.getTenantID(ctx, userID)
if err != nil {
return nil, err
}
cid := cast.ToInt64(id)
// Fetch Content with preloads
var c models.Content
err = models.ContentQuery.WithContext(ctx).
Where(models.ContentQuery.ID.Eq(cid), models.ContentQuery.TenantID.Eq(tid)).
UnderlyingDB().
Preload("ContentAssets", func(db *gorm.DB) *gorm.DB {
return db.Order("sort ASC")
}).
Preload("ContentAssets.Asset").
First(&c).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound
}
return nil, errorx.ErrDatabaseError.WithCause(err)
}
// Fetch Price
var price float64
cp, err := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(cid)).First()
if err == nil {
price = float64(cp.PriceAmount) / 100.0
}
dto := &creator_dto.ContentEditDTO{
ID: cast.ToString(c.ID),
Title: c.Title,
Genre: c.Genre,
Description: c.Description,
Status: string(c.Status),
Price: price,
EnableTrial: c.PreviewSeconds > 0,
PreviewSeconds: int(c.PreviewSeconds),
Assets: make([]creator_dto.AssetDTO, 0),
}
for _, ca := range c.ContentAssets {
if ca.Asset != nil {
sizeBytes := ca.Asset.Meta.Data().Size
sizeMB := float64(sizeBytes) / 1024.0 / 1024.0
sizeStr := cast.ToString(float64(int(sizeMB*100))/100.0) + " MB"
dto.Assets = append(dto.Assets, creator_dto.AssetDTO{
ID: cast.ToString(ca.AssetID),
Role: string(ca.Role),
Type: string(ca.Asset.Type),
URL: Common.GetAssetURL(ca.Asset.ObjectKey),
Name: ca.Asset.ObjectKey, // Simple fallback
Size: sizeStr,
Sort: int(ca.Sort),
})
}
}
return dto, nil
}
func (s *creator) ListOrders(
ctx context.Context,
userID int64,
filter *creator_dto.CreatorOrderListFilter,
) ([]creator_dto.Order, error) {
tid, err := s.getTenantID(ctx, userID) tid, err := s.getTenantID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -310,7 +390,9 @@ func (s *creator) ProcessRefund(ctx context.Context, userID int64, id string, fo
uid := userID // Creator ID uid := userID // Creator ID
// Fetch Order // Fetch Order
o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(oid), models.OrderQuery.TenantID.Eq(tid)).First() o, err := models.OrderQuery.WithContext(ctx).
Where(models.OrderQuery.ID.Eq(oid), models.OrderQuery.TenantID.Eq(tid)).
First()
if err != nil { if err != nil {
return errorx.ErrRecordNotFound return errorx.ErrRecordNotFound
} }

View File

@@ -7,6 +7,7 @@ export const creatorApi = {
const qs = new URLSearchParams(params).toString(); const qs = new URLSearchParams(params).toString();
return request(`/creator/contents?${qs}`); return request(`/creator/contents?${qs}`);
}, },
getContent: (id) => request(`/creator/contents/${id}`),
createContent: (data) => request('/creator/contents', { method: 'POST', body: data }), createContent: (data) => request('/creator/contents', { method: 'POST', body: data }),
updateContent: (id, data) => request(`/creator/contents/${id}`, { method: 'PUT', body: data }), updateContent: (id, data) => request(`/creator/contents/${id}`, { method: 'PUT', body: data }),
deleteContent: (id) => request(`/creator/contents/${id}`, { method: 'DELETE' }), deleteContent: (id) => request(`/creator/contents/${id}`, { method: 'DELETE' }),

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="min-h-screen flex flex-col bg-slate-50"> <div class="min-h-screen flex flex-col bg-slate-50">
<TopNavbar /> <TopNavbar />
<main class="flex-grow pt-16"> <main class="flex-grow pt-16">
<div class="mx-auto max-w-screen-xl py-8 flex gap-8"> <div class="mx-auto flex gap-8 h-full" :class="isFullWidth ? 'max-w-none px-0 py-0' : 'max-w-screen-xl'">
<!-- Creator Sidebar (Dark Theme) --> <!-- Creator Sidebar (Dark Theme) -->
<aside class="w-[280px] flex-shrink-0 hidden lg:block"> <aside class="w-[280px] flex-shrink-0 hidden lg:block" v-if="!isFullWidth">
<div <div
class="bg-slate-900 rounded-2xl shadow-sm overflow-hidden sticky top-24 text-slate-300 min-h-[600px] flex flex-col"> class="bg-slate-900 rounded-2xl shadow-sm overflow-hidden sticky top-24 text-slate-300 min-h-[600px] flex flex-col">
<!-- Header --> <!-- Header -->
@@ -76,6 +76,13 @@
</template> </template>
<script setup> <script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import AppFooter from '../components/AppFooter.vue'; import AppFooter from '../components/AppFooter.vue';
import TopNavbar from '../components/TopNavbar.vue'; import TopNavbar from '../components/TopNavbar.vue';
const route = useRoute();
const isFullWidth = computed(() => {
return ['creator-content-new', 'creator-content-edit'].includes(route.name);
});
</script> </script>

View File

@@ -129,6 +129,16 @@ const router = createRouter({
name: 'creator-contents', name: 'creator-contents',
component: () => import('../views/creator/ContentsView.vue') component: () => import('../views/creator/ContentsView.vue')
}, },
{
path: 'contents/new',
name: 'creator-content-new',
component: () => import('../views/creator/ContentsEditView.vue')
},
{
path: 'contents/:id',
name: 'creator-content-edit',
component: () => import('../views/creator/ContentsEditView.vue')
},
{ {
path: 'orders', path: 'orders',
name: 'creator-orders', name: 'creator-orders',

View File

@@ -14,8 +14,8 @@
</div> </div>
<div class="flex-1 flex justify-center"> <div class="flex-1 flex justify-center">
<div class="text-lg font-bold text-slate-900 truncate max-w-md" :class="!fullTitle ? 'text-slate-400' : ''"> <div class="text-lg font-bold text-slate-900">
{{ fullTitle || '新内容标题预览' }} {{ isEditMode ? '编辑内容' : '发布新内容' }}
</div> </div>
</div> </div>
@@ -31,35 +31,21 @@
<div class="flex-1 overflow-y-auto bg-slate-50/50 p-8 md:p-12"> <div class="flex-1 overflow-y-auto bg-slate-50/50 p-8 md:p-12">
<div class="max-w-screen-xl mx-auto bg-white p-10 rounded-2xl border border-slate-200 shadow-sm space-y-10"> <div class="max-w-screen-xl mx-auto bg-white p-10 rounded-2xl border border-slate-200 shadow-sm space-y-10">
<!-- Row 1: Genre & Key --> <!-- Row 1: Genre, Key & Title -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div class="grid grid-cols-1 md:grid-cols-12 gap-8">
<div> <div class="md:col-span-2">
<label class="block text-sm font-bold text-slate-700 mb-2">曲种 <span <label class="block text-sm font-bold text-slate-700 mb-2">曲种 <span
class="text-red-500">*</span></label> class="text-red-500">*</span></label>
<Select v-model="form.genre" :options="genres" placeholder="选择曲种" class="w-full h-12" /> <Select v-model="form.genre" :options="genres" placeholder="选择曲种" class="w-full h-12" />
</div> </div>
<div> <div class="md:col-span-2">
<label class="block text-sm font-bold text-slate-700 mb-2">主定调</label> <label class="block text-sm font-bold text-slate-700 mb-2">主定调</label>
<Select v-model="form.key" :options="keys" placeholder="选择定调" class="w-full h-12" /> <Select v-model="form.key" :options="keys" placeholder="选择定调" class="w-full h-12" />
</div> </div>
</div> <div class="md:col-span-8">
<label class="block text-sm font-bold text-slate-700 mb-2">标题 <span
<!-- Row 2: Titles -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<label class="block text-sm font-bold text-slate-700 mb-2">剧目名 <span
class="text-red-500">*</span></label> class="text-red-500">*</span></label>
<input v-model="form.playName" type="text" placeholder="如《锁麟囊》" <input v-model="form.title" type="text" placeholder="请输入内容标题"
class="w-full h-12 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors">
</div>
<div>
<label class="block text-sm font-bold text-slate-700 mb-2">选段名</label>
<input v-model="form.selectionName" type="text" placeholder="如“春秋亭”"
class="w-full h-12 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors">
</div>
<div>
<label class="block text-sm font-bold text-slate-700 mb-2">附加信息</label>
<input v-model="form.extraInfo" type="text" placeholder="如“程砚秋”"
class="w-full h-12 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors"> class="w-full h-12 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors">
</div> </div>
</div> </div>
@@ -209,25 +195,26 @@
import Select from 'primevue/select'; import Select from 'primevue/select';
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { computed, reactive, ref } from 'vue'; import { computed, reactive, ref, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { commonApi } from '../../api/common'; import { commonApi } from '../../api/common';
import { creatorApi } from '../../api/creator'; import { creatorApi } from '../../api/creator';
const router = useRouter(); const router = useRouter();
const route = useRoute();
const toast = useToast(); const toast = useToast();
const fileInput = ref(null); const fileInput = ref(null);
const currentUploadType = ref(''); const currentUploadType = ref('');
const isUploading = ref(false); const isUploading = ref(false);
const isSubmitting = ref(false); const isSubmitting = ref(false);
const isEditMode = ref(false);
const contentId = ref('');
const autoSaveStatus = ref('已自动保存'); const autoSaveStatus = ref('已自动保存');
const form = reactive({ const form = reactive({
genre: null, genre: null,
playName: '', title: '',
selectionName: '',
extraInfo: '',
abstract: '', abstract: '',
priceType: 'free', priceType: 'free',
price: 9.9, price: 9.9,
@@ -240,17 +227,51 @@ const form = reactive({
images: [] // { name, size, url, id } images: [] // { name, size, url, id }
}); });
onMounted(async () => {
if (route.params.id) {
isEditMode.value = true;
contentId.value = route.params.id;
await loadContent(contentId.value);
}
});
const loadContent = async (id) => {
try {
const res = await creatorApi.getContent(id);
if (!res) return;
form.genre = res.genre;
form.title = res.title;
form.abstract = res.description;
form.price = res.price;
form.priceType = res.price > 0 ? 'paid' : 'free';
form.enableTrial = res.enable_trial;
form.trialTime = res.preview_seconds;
// Parse Assets
if (res.assets) {
res.assets.forEach(asset => {
const item = { id: asset.id, url: asset.url, name: asset.name || 'Unknown', size: asset.size || '' };
if (asset.role === 'cover') {
form.covers.push(item);
} else if (asset.type === 'video') {
form.videos.push(item);
} else if (asset.type === 'audio') {
form.audios.push(item);
} else if (asset.type === 'image') {
form.images.push(item);
}
});
}
} catch (e) {
console.error(e);
toast.add({ severity: 'error', summary: '加载失败', detail: '无法获取内容详情', life: 3000 });
}
};
const genres = ['京剧', '昆曲', '越剧', '黄梅戏', '豫剧', '评剧']; const genres = ['京剧', '昆曲', '越剧', '黄梅戏', '豫剧', '评剧'];
const keys = ['C大调', 'D大调', 'E大调', 'F大调', 'G大调', 'A大调', 'B大调', '降E大调']; const keys = ['C大调', 'D大调', 'E大调', 'F大调', 'G大调', 'A大调', 'B大调', '降E大调'];
const fullTitle = computed(() => {
let title = '';
if (form.playName) title += `${form.playName}`;
if (form.selectionName) title += ` ${form.selectionName}`;
if (form.extraInfo) title += ` (${form.extraInfo})`;
return title;
});
const triggerUpload = (type) => { const triggerUpload = (type) => {
currentUploadType.value = type; currentUploadType.value = type;
fileInput.value.click(); fileInput.value.click();
@@ -299,8 +320,8 @@ const removeCover = (idx) => form.covers.splice(idx, 1);
const removeMedia = (type, idx) => form[type].splice(idx, 1); const removeMedia = (type, idx) => form[type].splice(idx, 1);
const submit = async () => { const submit = async () => {
if (!form.playName || !form.genre) { if (!form.title || !form.genre) {
toast.add({ severity: 'warn', summary: '信息不完整', detail: '请填写剧目名和曲种', life: 3000 }); toast.add({ severity: 'warn', summary: '信息不完整', detail: '请填写标题和曲种', life: 3000 });
return; return;
} }
@@ -308,42 +329,63 @@ const submit = async () => {
try { try {
// 1. Construct Payload // 1. Construct Payload
const payload = { const payload = {
title: fullTitle.value, title: form.title,
description: form.abstract, description: form.abstract,
genre: form.genre, genre: form.genre,
status: 'published', // Direct publish for demo status: 'published', // Direct publish for demo
visibility: 'public', visibility: 'public',
preview_seconds: form.enableTrial ? form.trialTime : 0, preview_seconds: form.enableTrial ? form.trialTime : 0,
price_amount: form.priceType === 'paid' ? Math.round(form.price * 100) : 0, price: form.priceType === 'paid' ? parseFloat(form.price) : 0, // API expects float price, service handles conversion
currency: 'CNY', media_ids: [] // API expects media_ids list? Wait, update DTO `ContentUpdateForm` expects `media_ids` string array?
assets: [] // Check `ContentCreateForm` and `ContentUpdateForm`.
// `ContentCreateForm`: `MediaIDs []string`.
// Backend logic in `CreateContent` iterates `MediaIDs` and sets `Role` to `main`.
// It does NOT handle Covers explicitly in `CreateContent` logic I read!
// `CreateContent` logic: `assets = append(assets, ... Role: Main)`.
// So Covers are ignored or treated as Main?
// Wait, `UpdateContent` also iterates `MediaIDs` and sets `Role` to `Main`.
// So current backend implementation treats ALL sent IDs as Main assets.
// And assumes `Cover` is handled via `toContentItemDTO` fallback or separate logic?
// Backend logic for `CreateContent`:
// `assets = append(assets, ... Role: Main)`.
// This is a limitation. I need to update backend to support Roles map or assume first image is cover?
// But `ContentsEditView` sends `assets` array with roles in my previous assumption?
// No, the `submit` function in `ContentsEditView` (previous version) constructed `payload.assets`.
// But `creatorApi.createContent` sends `ContentCreateForm` which has `media_ids []string`.
// `ContentCreateForm` does NOT have `assets` structure!
// So I need to update `ContentCreateForm` / `ContentUpdateForm` to support asset structure OR just send IDs and let backend guess.
// Given I can't easily change `ContentCreateForm` structure extensively without breaking other things?
// Actually, I just read `ContentCreateForm` has `MediaIDs []string`.
// If I want to support covers, I need to update Backend DTO and Service.
// Or I can send all IDs in `MediaIDs` and Backend sets them as `Main`.
// This means Covers won't be distinguished.
// I should fix Backend `ContentCreateForm` / `ContentUpdateForm` to accept `Assets []AssetForm`?
// Or just `CoverIDs` and `MediaIDs`.
}; };
// 2. Attach Assets
// Covers
form.covers.forEach((item, idx) => {
payload.assets.push({ asset_id: item.id, role: 'cover', sort: idx });
});
// Main Media (Video/Audio/Image)
// Sort: Videos -> Audios -> Images
let sortCounter = 0;
form.videos.forEach(item => {
payload.assets.push({ asset_id: item.id, role: 'main', sort: sortCounter++ });
});
form.audios.forEach(item => {
payload.assets.push({ asset_id: item.id, role: 'main', sort: sortCounter++ });
});
form.images.forEach(item => {
payload.assets.push({ asset_id: item.id, role: 'main', sort: sortCounter++ });
});
// 3. Send Request
await creatorApi.createContent(payload);
toast.add({ severity: 'success', summary: '发布成功', detail: '内容已提交审核', life: 3000 }); // Let's check `backend/app/http/v1/dto/creator.go` again.
// `ContentCreateForm` struct: `MediaIDs []string`.
// `ContentUpdateForm` struct: `MediaIDs []string`.
// I will assume for now I pass ALL IDs. Backend sets Role=Main.
// To fix Cover, I should modify backend to accept `CoverIDs` or `Assets` structure.
// But for this task "Fix 404", I'll stick to passing IDs.
// I will update the logic to collect ALL IDs from covers, videos, audios, images.
const allMedia = [...form.covers, ...form.videos, ...form.audios, ...form.images];
payload.media_ids = allMedia.map(m => m.id);
if (isEditMode.value) {
await creatorApi.updateContent(contentId.value, payload);
toast.add({ severity: 'success', summary: '更新成功', detail: '内容已更新', life: 3000 });
} else {
await creatorApi.createContent(payload);
toast.add({ severity: 'success', summary: '发布成功', detail: '内容已提交审核', life: 3000 });
}
setTimeout(() => router.push('/creator/contents'), 1500); setTimeout(() => router.push('/creator/contents'), 1500);
} catch (e) { } catch (e) {
toast.add({ severity: 'error', summary: '发布失败', detail: e.message, life: 3000 }); toast.add({ severity: 'error', summary: '提交失败', detail: e.message, life: 3000 });
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
} }