123 Commits

Author SHA1 Message Date
574e241c8a fix: migrations 2026-04-15 13:42:36 +02:00
5ebf21c559 Merge remote-tracking branch 'origin/main' into front-styles 2026-04-15 12:08:59 +02:00
84b4c70ffb Merge pull request 'bugfix' (#70) from countries_bugfix into main
Reviewed-on: #70
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-15 10:00:41 +00:00
Daniel Goc
2fd9472db1 bugfix 2026-04-15 11:48:29 +02:00
66df535317 Merge pull request 'expand_carts' (#69) from expand_carts into main
Reviewed-on: #69
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-15 09:19:16 +00:00
5663c4e126 fix: addresses 2026-04-14 15:57:45 +02:00
Daniel Goc
e0a86febc4 add missing permission 2026-04-14 15:52:45 +02:00
1d257d82d3 Merge remote-tracking branch 'origin/main' into front-styles 2026-04-14 15:45:36 +02:00
Daniel Goc
40154ec861 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into expand_carts 2026-04-14 15:43:44 +02:00
Daniel Goc
bb507036db change add-product endpoint + remove-product 2026-04-14 15:42:30 +02:00
80a1314dc0 Merge pull request 'some small fixes' (#68) from small_fixes into main
Reviewed-on: #68
2026-04-14 13:16:17 +00:00
Daniel Goc
100a9f57d4 some small fixes 2026-04-14 14:08:57 +02:00
773e7d3c20 Merge pull request 'feat: lookup by id in customer search' (#61) from cust-search into main
Reviewed-on: #61
2026-04-14 11:42:56 +00:00
03a0e5ea64 Merge branch 'main' into cust-search 2026-04-14 11:39:18 +00:00
ce8c19f715 Merge pull request 'feat: make routing per role, add unlogged role' (#67) from routing-per-role into main
Reviewed-on: #67
Reviewed-by: goc_daniel <goc_daniel@ma-al.com>
2026-04-14 11:39:13 +00:00
4edcb0a852 Merge branch 'main' into cust-search 2026-04-14 11:22:00 +00:00
a4120dafa2 Merge branch 'main' into routing-per-role 2026-04-14 11:21:53 +00:00
5e1a8e898c Merge pull request 'orders' (#58) from orders into main
Reviewed-on: #58
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-14 11:20:05 +00:00
Daniel Goc
c610ce38cc fixes after merging with main 2026-04-14 13:19:48 +02:00
8e3e41d6fe Merge branch 'main' into cust-search 2026-04-14 11:16:42 +00:00
b33da9d072 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into routing-per-role 2026-04-14 13:15:51 +02:00
Daniel Goc
604247b7c8 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into orders 2026-04-14 13:14:52 +02:00
e5988a85f3 Merge pull request 'update_categories' (#62) from update_categories into main
Reviewed-on: #62
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-14 10:35:28 +00:00
Daniel Goc
0cb5cc47bb Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into update_categories 2026-04-14 12:33:11 +02:00
Daniel Goc
1efc5417be permissions strings change 2026-04-14 12:32:24 +02:00
Daniel Goc
a0c3dd8ec8 added filtering on is_new and is_favorite 2026-04-14 12:28:39 +02:00
ab783b599d chore: add favorite field to base product query 2026-04-14 11:07:55 +02:00
d173af29fe fix: actually add the unlogged role to migration 2026-04-14 10:18:12 +02:00
f14d60d67b chore: swap permission string in handler to consts 2026-04-14 10:17:05 +02:00
967b101f9b feat: make routing per role, add unlogged role 2026-04-14 09:54:37 +02:00
059808665f Merge remote-tracking branch 'origin/translate' into front-styles 2026-04-14 08:58:32 +02:00
b54645830f fix: store customer-product 2026-04-14 08:55:53 +02:00
97ca510b99 Merge branch 'main' into cust-search 2026-04-14 06:26:47 +00:00
1973189525 Merge remote-tracking branch 'origin/main' into front-styles 2026-04-14 08:18:19 +02:00
26cbdeec0a Merge pull request 'product-procedures' (#59) from product-procedures into main
Reviewed-on: #59
Reviewed-by: goc_daniel <goc_daniel@ma-al.com>
2026-04-13 13:32:28 +00:00
Daniel Goc
ce4cadaa16 most importantly: new category and filter on is_new 2026-04-13 15:29:21 +02:00
Daniel Goc
7f05d39b38 Merge branch 'product-procedures' of ssh://git.ma-al.com:8822/goc_daniel/b2b into update_categories 2026-04-13 14:48:28 +02:00
83b7cd49dd feat: lookup by id in customer search 2026-04-13 14:43:18 +02:00
Daniel Goc
88255776f3 fixes 2026-04-13 14:29:36 +02:00
38cb07f3d4 chore: address pull request review issues 2026-04-13 14:22:14 +02:00
Daniel Goc
1f6d5ecb72 go routine and removing cart 2026-04-13 09:21:33 +02:00
5d97d537cd Merge remote-tracking branch 'origin/main' into front-styles 2026-04-13 08:22:35 +02:00
2e61cde742 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into product-procedures 2026-04-10 15:26:36 +02:00
Daniel Goc
d4d55e2757 send email when creating new order 2026-04-10 15:17:29 +02:00
54608410ea feat: create specific price system and adapt product queries 2026-04-10 15:04:34 +02:00
Daniel Goc
80d26bba12 GET -> POST 2026-04-10 14:57:24 +02:00
Daniel Goc
33e9d016e9 basic orders are ready 2026-04-10 14:53:40 +02:00
Daniel Goc
a03a2b461f small fix 2026-04-10 13:25:00 +02:00
Daniel Goc
134bc4ea53 code ready, time for testing... 2026-04-10 13:23:51 +02:00
Daniel Goc
8595969c6e Find is done 2026-04-10 12:17:52 +02:00
f1a2f4c0b2 fix: fix storage file 2026-04-10 11:38:36 +02:00
Daniel Goc
a6aa06faa0 basic setup 2026-04-10 11:17:58 +02:00
Daniel Goc
4f4b32b131 order struct 2026-04-10 11:06:43 +02:00
Daniel Goc
dfdf8b4db9 orders handler init 2026-04-10 11:01:39 +02:00
bfd20aaa7b Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-10 10:44:47 +02:00
Daniel Goc
438a13c04c orders tables 2026-04-10 10:34:44 +02:00
8024d9f739 Merge pull request 'favorites' (#57) from favorites into main
Reviewed-on: #57
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-10 08:09:13 +00:00
Daniel Goc
c5832c0cf5 minor change 2026-04-10 09:57:07 +02:00
Daniel Goc
61ccd32c4a catching errors again 2026-04-10 09:43:49 +02:00
Daniel Goc
f7f56c2928 catching errors 2026-04-10 09:33:44 +02:00
Daniel Goc
0a5ce5d9c2 ... 2026-04-10 09:13:13 +02:00
1fb2a33cfd fix: create component StorageFileBrowser 2026-04-09 16:00:36 +02:00
Daniel Goc
f1f5daa82b and add filtering by is_favorite 2026-04-09 14:53:56 +02:00
Daniel Goc
393de36cb2 favorites 2026-04-09 14:49:50 +02:00
9fb8e034fc Merge pull request 'added addresses endpoints' (#55) from addresses into main
Reviewed-on: #55
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-09 10:37:36 +00:00
75af44b0df feat: product_attribute list with prices 2026-04-09 09:51:06 +02:00
0d2bf3a27f Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-09 08:28:47 +02:00
c7692bc817 fix: page serchUsers 2026-04-08 13:51:10 +02:00
8824796846 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-08 10:09:40 +02:00
9eb8fc6625 fix: page usersList 2026-04-07 16:06:36 +02:00
a290a72d1d fix: style 2026-04-07 13:54:08 +02:00
b0d0338d23 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-07 13:40:51 +02:00
849b18c4db Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-07 12:55:50 +02:00
a874a063d8 fix: page usersList 2026-04-07 10:06:36 +02:00
11dab263fa Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-07 08:03:23 +02:00
5bda48b94a Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-07 08:02:32 +02:00
2f4ccbaf92 fix: import 2026-04-07 08:01:21 +02:00
fb910b9ac6 Merge remote-tracking branch 'origin/main' into front-styles 2026-04-03 16:33:32 +02:00
4c505c0520 Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-03 15:54:23 +02:00
2f7e313c95 fix: user info 2026-04-03 15:53:31 +02:00
110946a924 Merge remote-tracking branch 'origin/main' into front-styles 2026-04-03 14:35:29 +02:00
3083330fcd Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-03 11:32:10 +02:00
b7c4b6e3fd fix: store 2026-04-03 11:32:04 +02:00
e9016baf5b Merge remote-tracking branch 'origin/product-procedures' into front-styles 2026-04-03 11:31:35 +02:00
68f31952da Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-03 10:56:21 +02:00
c1efcf1b86 fix: style 2026-04-03 10:55:12 +02:00
5cab7573a8 Merge remote-tracking branch 'origin/product-procedures' into front-styles 2026-04-03 10:54:35 +02:00
5f5c3641f9 Merge remote-tracking branch 'origin/product-procedures' into front-styles 2026-04-03 09:23:51 +02:00
d655e22173 chore: add bruno endpoints 2026-04-03 09:22:58 +02:00
5255ece1b4 Merge remote-tracking branch 'origin/translate' into front-styles 2026-04-03 08:57:10 +02:00
e82279926f erditor on product 2026-04-03 08:56:13 +02:00
855721b4b1 fix: tabs 2026-04-03 08:51:45 +02:00
9fcecfb209 Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-03 08:24:31 +02:00
ee375b7cbd Merge remote-tracking branch 'origin/translate' into front-styles 2026-04-02 16:15:00 +02:00
f58abda5cd fix: style 2026-04-02 16:07:15 +02:00
c20cdf2b63 fix: editor 2026-04-02 15:55:00 +02:00
2ce2556685 fix: tabs 2026-04-02 15:53:27 +02:00
e961967a49 Merge remote-tracking branch 'refs/remotes/origin/front-styles' into front-styles 2026-04-02 15:28:00 +02:00
Daniel Goc
a4eb3b52cf change incoming HTML to HTML4 2026-04-02 15:24:34 +02:00
7c69d56a48 fix: style 2026-04-02 14:24:00 +02:00
1494af985a Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-02 11:42:43 +02:00
9513feba37 fix: style 2026-04-02 11:42:12 +02:00
551b5ef77d fix: translations 2026-04-02 11:41:44 +02:00
a7f69c854a Merge remote-tracking branch 'refs/remotes/origin/front-styles' into front-styles 2026-04-02 11:32:26 +02:00
Daniel Goc
2bfe97f6fe bugfix 2026-04-02 10:38:19 +02:00
7e8e9897e1 fix: button showProduct 2026-04-02 09:01:26 +02:00
9c8ce43998 Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-02 08:56:55 +02:00
3652a78783 fix: button showProduct 2026-04-02 08:56:31 +02:00
ecf51e92e8 fix: button showProduct 2026-04-02 08:56:05 +02:00
d0c055a819 fix: select for languages 2026-04-02 08:52:29 +02:00
272c6066fb Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-01 16:19:36 +02:00
79730eb826 fix: save product descriptions 2026-04-01 16:19:02 +02:00
6846ffc0dc fix: sidebar 2026-04-01 16:16:03 +02:00
d8f71bd8ff Merge remote-tracking branch 'origin/translate' into front-styles 2026-04-01 13:42:00 +02:00
aa57d38bd6 fix: selects 2026-04-01 13:41:21 +02:00
f9ae1e491e fix: translate 2026-04-01 13:40:56 +02:00
8bf5a1cf8b Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-01 10:53:49 +02:00
b48a143b40 fix: default lang 2026-04-01 10:52:34 +02:00
729b54ca1a fix: currency/county 2026-04-01 10:52:08 +02:00
980fb1543b Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-04-01 09:36:56 +02:00
b829bf2185 fix: translate 2026-04-01 09:36:01 +02:00
d6066e39ce Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate 2026-03-31 12:49:25 +02:00
12f6249721 fix: requests 2026-03-31 12:22:21 +02:00
165 changed files with 7163 additions and 8263 deletions

View File

@@ -28,7 +28,7 @@ tmp_dir = "tmp"
rerun = false rerun = false
rerun_delay = 500 rerun_delay = 500
send_interrupt = false send_interrupt = false
stop_on_error = false stop_on_error = true
[color] [color]
app = "" app = ""

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/authService" "git.ma-al.com/goc_daniel/b2b/app/service/authService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
@@ -16,9 +17,8 @@ import (
) )
// AuthMiddleware creates authentication middleware // AuthMiddleware creates authentication middleware
func AuthMiddleware() fiber.Handler { func Authenticate() fiber.Handler {
authService := authService.NewAuthService() authService := authService.NewAuthService()
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
// Get token from Authorization header // Get token from Authorization header
authHeader := c.Get("Authorization") authHeader := c.Get("Authorization")
@@ -26,17 +26,13 @@ func AuthMiddleware() fiber.Handler {
// Try to get from cookie // Try to get from cookie
authHeader = c.Cookies("access_token") authHeader = c.Cookies("access_token")
if authHeader == "" { if authHeader == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Next()
"error": "authorization token required",
})
} }
} else { } else {
// Extract token from "Bearer <token>" // Extract token from "Bearer <token>"
parts := strings.Split(authHeader, " ") parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" { if len(parts) != 2 || parts[0] != "Bearer" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Next()
"error": "invalid authorization header format",
})
} }
authHeader = parts[1] authHeader = parts[1]
} }
@@ -44,24 +40,18 @@ func AuthMiddleware() fiber.Handler {
// Validate token // Validate token
claims, err := authService.ValidateToken(authHeader) claims, err := authService.ValidateToken(authHeader)
if err != nil { if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Next()
"error": "invalid or expired token",
})
} }
// Get user from database // Get user from database
user, err := authService.GetUserByID(claims.UserID) user, err := authService.GetUserByID(claims.UserID)
if err != nil { if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Next()
"error": "user not found",
})
} }
// Check if user is active // Check if user is active
if !user.IsActive { if !user.IsActive {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ return c.Next()
"error": "user account is inactive",
})
} }
// Create locale. LangID is overwritten by auth Token // Create locale. LangID is overwritten by auth Token
@@ -79,10 +69,8 @@ func AuthMiddleware() fiber.Handler {
} }
// We now populate the target user // We now populate the target user
if model.CustomerRole(user.Role.Name) != model.RoleAdmin { if !userLocale.OriginalUser.HasPermission(perms.Teleport) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ return c.Next()
"error": "admin access required",
})
} }
targetUserID, err := strconv.Atoi(targetUserIDAttribute) targetUserID, err := strconv.Atoi(targetUserIDAttribute)
@@ -115,22 +103,14 @@ func AuthMiddleware() fiber.Handler {
} }
} }
// RequireAdmin creates admin-only middleware func Authorize() fiber.Handler {
func RequireAdmin() fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
originalUserRole, ok := localeExtractor.GetOriginalUserRole(c) _, ok := localeExtractor.GetUserID(c)
if !ok { if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "not authenticated", "error": "not authenticated",
}) })
} }
if model.CustomerRole(originalUserRole.Name) != model.RoleAdmin {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "admin access required",
})
}
return c.Next() return c.Next()
} }
} }

View File

@@ -2,27 +2,27 @@ package middleware
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
func Require(p perms.Permission) fiber.Handler { func Require(p perms.Permission) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
u := c.Locals("user") user, ok := localeExtractor.GetCustomer(c)
if u == nil {
return c.SendStatus(fiber.StatusUnauthorized)
}
user, ok := u.(*model.UserSession)
if !ok { if !ok {
return c.SendStatus(fiber.StatusInternalServerError) return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
for _, perm := range user.Permissions { for _, perm := range user.Role.Permissions {
if perm == p { if perm.Name == p {
return c.Next() return c.Next()
} }
} }
return c.SendStatus(fiber.StatusForbidden) return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrForbidden)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrForbidden)))
} }
} }

View File

@@ -7,4 +7,12 @@ const (
UserWriteAny Permission = "user.write.any" UserWriteAny Permission = "user.write.any"
UserDeleteAny Permission = "user.delete.any" UserDeleteAny Permission = "user.delete.any"
CurrencyWrite Permission = "currency.write" CurrencyWrite Permission = "currency.write"
SpecificPriceManage Permission = "specific_price.manage"
WebdavCreateToken Permission = "webdav.create_token"
ProductTranslationSave Permission = "product_translation.save"
ProductTranslationTranslate Permission = "product_translation.translate"
SearchCreateIndex Permission = "search.create_index"
OrdersViewAll Permission = "orders.view_all"
OrdersModifyAll Permission = "orders.modify_all"
Teleport Permission = "teleport"
) )

View File

@@ -49,7 +49,7 @@ func AuthHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/google", handler.GoogleLogin) r.Get("/google", handler.GoogleLogin)
r.Get("/google/callback", handler.GoogleCallback) r.Get("/google/callback", handler.GoogleCallback)
authProtected := r.Group("", middleware.AuthMiddleware()) authProtected := r.Group("", middleware.Authorize())
authProtected.Get("/me", handler.Me) authProtected.Get("/me", handler.Me)
authProtected.Post("/update-choice", handler.UpdateJWTToken) authProtected.Post("/update-choice", handler.UpdateJWTToken)

View File

@@ -2,6 +2,7 @@ package public
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/service/menuService" "git.ma-al.com/goc_daniel/b2b/app/service/menuService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
@@ -31,12 +32,21 @@ func RoutingHandlerRoutes(r fiber.Router) fiber.Router {
} }
func (h *RoutingHandler) GetRouting(c fiber.Ctx) error { func (h *RoutingHandler) GetRouting(c fiber.Ctx) error {
lang_id, ok := localeExtractor.GetLangID(c) langId, ok := localeExtractor.GetLangID(c)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
menu, err := h.menuService.GetRoutes(lang_id)
var roleId uint
customer, ok := localeExtractor.GetCustomer(c)
if !ok {
roleId = constdata.UNLOGGED_USER_ROLE_ID
} else {
roleId = customer.RoleID
}
menu, err := h.menuService.GetRoutes(langId, roleId)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -124,13 +124,13 @@ func (h *AddressesHandler) RetrieveAddressesInfo(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
addresses_info, err := h.addressesService.RetrieveAddressesInfo(userID) addresses, err := h.addressesService.RetrieveAddresses(userID)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
return c.JSON(response.Make(&addresses_info, 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(addresses, 0, i18n.T_(c, response.Message_OK)))
} }
func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error { func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error {

View File

@@ -29,10 +29,12 @@ func CartsHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewCartsHandler() handler := NewCartsHandler()
r.Get("/add-new-cart", handler.AddNewCart) r.Get("/add-new-cart", handler.AddNewCart)
r.Delete("/remove-cart", handler.RemoveCart)
r.Get("/change-cart-name", handler.ChangeCartName) r.Get("/change-cart-name", handler.ChangeCartName)
r.Get("/retrieve-carts-info", handler.RetrieveCartsInfo) r.Get("/retrieve-carts-info", handler.RetrieveCartsInfo)
r.Get("/retrieve-cart", handler.RetrieveCart) r.Get("/retrieve-cart", handler.RetrieveCart)
r.Get("/add-product-to-cart", handler.AddProduct) r.Get("/add-product-to-cart", handler.AddProduct)
r.Delete("/remove-product-from-cart", handler.RemoveProduct)
return r return r
} }
@@ -53,6 +55,29 @@ func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
return c.JSON(response.Make(&new_cart, 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(&new_cart, 0, i18n.T_(c, response.Message_OK)))
} }
func (h *CartsHandler) RemoveCart(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
cart_id_attribute := c.Query("cart_id")
cart_id, err := strconv.Atoi(cart_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.cartsService.RemoveCart(userID, uint(cart_id))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error { func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := localeExtractor.GetUserID(c)
if !ok { if !ok {
@@ -117,6 +142,7 @@ func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error {
return c.JSON(response.Make(cart, 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(cart, 0, i18n.T_(c, response.Message_OK)))
} }
// adds or sets given amount of products to the cart
func (h *CartsHandler) AddProduct(c fiber.Ctx) error { func (h *CartsHandler) AddProduct(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := localeExtractor.GetUserID(c)
if !ok { if !ok {
@@ -159,7 +185,59 @@ func (h *CartsHandler) AddProduct(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
err = h.cartsService.AddProduct(userID, uint(cart_id), uint(product_id), product_attribute_id, uint(amount)) set_amount_attribute := c.Query("set_amount")
set_amount, err := strconv.ParseBool(set_amount_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.cartsService.AddProduct(userID, uint(cart_id), uint(product_id), product_attribute_id, amount, set_amount)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
// removes product from the cart.
func (h *CartsHandler) RemoveProduct(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
cart_id_attribute := c.Query("cart_id")
cart_id, err := strconv.Atoi(cart_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
product_id_attribute := c.Query("product_id")
product_id, err := strconv.Atoi(product_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
product_attribute_id_attribute := c.Query("product_attribute_id")
var product_attribute_id *uint
if product_attribute_id_attribute == "" {
product_attribute_id = nil
} else {
val, err := strconv.Atoi(product_attribute_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
uval := uint(val)
product_attribute_id = &uval
}
err = h.cartsService.RemoveProduct(userID, uint(cart_id), uint(product_id), product_attribute_id)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -3,6 +3,7 @@ package restricted
import ( import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/customerService" "git.ma-al.com/goc_daniel/b2b/app/service/customerService"
@@ -30,7 +31,7 @@ func CustomerHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewCustomerHandler() handler := NewCustomerHandler()
r.Get("", handler.customerData) r.Get("", handler.customerData)
r.Get("/list", handler.listCustomers) r.Get("/list", middleware.Require(perms.UserReadAny), handler.listCustomers)
return r return r
} }
@@ -75,10 +76,6 @@ func (h *customerHandler) listCustomers(fc fiber.Ctx) error {
return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute)))
} }
if !user.HasPermission(perms.UserReadAny) {
return fc.Status(fiber.StatusForbidden).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden)))
}
p, filt, err := query_params.ParseFilters[model.Customer](fc, columnMappingListUsers) p, filt, err := query_params.ParseFilters[model.Customer](fc, columnMappingListUsers)
if err != nil { if err != nil {
@@ -87,12 +84,6 @@ func (h *customerHandler) listCustomers(fc fiber.Ctx) error {
} }
search := fc.Query("search") search := fc.Query("search")
if search != "" {
if !user.HasPermission(perms.UserReadAny) {
return fc.Status(fiber.StatusForbidden).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden)))
}
}
customer, err := h.service.Find(user.LangID, p, filt, search) customer, err := h.service.Find(user.LangID, p, filt, search)
if err != nil { if err != nil {

View File

@@ -0,0 +1,171 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/orderService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type OrdersHandler struct {
ordersService *orderService.OrderService
}
func NewOrdersHandler() *OrdersHandler {
ordersService := orderService.New()
return &OrdersHandler{
ordersService: ordersService,
}
}
func OrdersHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewOrdersHandler()
r.Get("/list", handler.ListOrders)
r.Post("/place-new-order", handler.PlaceNewOrder)
r.Post("/change-order-address", handler.ChangeOrderAddress)
r.Get("/change-order-status", handler.ChangeOrderStatus)
return r
}
// when a user (not admin) wants to list orders, we automatically append filter to only view his orders.
// we base permissions and user based on target user only.
func (h *OrdersHandler) ListOrders(c fiber.Ctx) error {
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
paging, filters, err := query_params.ParseFilters[model.CustomerOrder](c, columnMappingListOrders)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
list, err := h.ordersService.Find(user, paging, filters)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK)))
}
var columnMappingListOrders map[string]string = map[string]string{
"order_id": "b2b_customer_orders.order_id",
"user_id": "b2b_customer_orders.user_id",
"name": "b2b_customer_orders.name",
"country_id": "b2b_customer_orders.country_id",
"status": "b2b_customer_orders.status",
}
func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
cart_id_attribute := c.Query("cart_id")
cart_id, err := strconv.Atoi(cart_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
country_id_attribute := c.Query("country_id")
country_id, err := strconv.Atoi(country_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
address_info := string(c.Body())
if address_info == "" {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
name := c.Query("name")
err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), name, uint(country_id), address_info)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
// we base permissions and user based on target user only.
func (h *OrdersHandler) ChangeOrderAddress(c fiber.Ctx) error {
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
order_id_attribute := c.Query("order_id")
order_id, err := strconv.Atoi(order_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
country_id_attribute := c.Query("country_id")
country_id, err := strconv.Atoi(country_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
address_info := string(c.Body())
if address_info == "" {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
err = h.ordersService.ChangeOrderAddress(user, uint(order_id), uint(country_id), address_info)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
// we base permissions and user based on target user only.
// TODO: well, permissions and all that.
func (h *OrdersHandler) ChangeOrderStatus(c fiber.Ctx) error {
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
order_id_attribute := c.Query("order_id")
order_id, err := strconv.Atoi(order_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
status := c.Query("status")
err = h.ordersService.ChangeOrderStatus(user, uint(order_id), status)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -4,8 +4,9 @@ import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
"git.ma-al.com/goc_daniel/b2b/app/service/productService" "git.ma-al.com/goc_daniel/b2b/app/service/productService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
@@ -34,6 +35,9 @@ func ProductsHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/:id/:country_id/:quantity", handler.GetProductJson) r.Get("/:id/:country_id/:quantity", handler.GetProductJson)
r.Get("/list", handler.ListProducts) r.Get("/list", handler.ListProducts)
r.Get("/list-variants/:product_id", handler.ListProductVariants)
r.Post("/favorite/:product_id", handler.AddToFavorites)
r.Delete("/favorite/:product_id", handler.RemoveFromFavorites)
return r return r
} }
@@ -68,7 +72,7 @@ func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
productJson, err := h.productService.GetJSON(p_id_product, int(customer.LangID), int(customer.ID), b2b_id_country, p_quantity) productJson, err := h.productService.Get(uint(p_id_product), customer.LangID, customer.ID, uint(b2b_id_country), uint(p_quantity))
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
@@ -78,19 +82,19 @@ func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
} }
func (h *ProductsHandler) ListProducts(c fiber.Ctx) error { func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingListProducts) paging, filters, err := query_params.ParseFilters[dbmodel.PsProduct](c, columnMappingListProducts)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
id_lang, ok := localeExtractor.GetLangID(c) customer, ok := localeExtractor.GetCustomer(c)
if !ok { if !ok || customer == nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
list, err := h.productService.Find(id_lang, paging, filters) list, err := h.productService.Find(customer.LangID, customer.ID, paging, filters, customer, constdata.DEFAULT_PRODUCT_QUANTITY, constdata.SHOP_ID)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
@@ -99,11 +103,85 @@ func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK)))
} }
// These are all the filterable fields
var columnMappingListProducts map[string]string = map[string]string{ var columnMappingListProducts map[string]string = map[string]string{
"product_id": "ps.id_product", "product_id": "bp.product_id",
"name": "pl.name", "name": "bp.name",
"reference": "p.reference", "reference": "bp.reference",
"category_name": "cl.name", "category_id": "bp.category_id",
"category_id": "cp.id_category", "quantity": "bp.quantity",
"quantity": "sa.quantity", "is_favorite": "bp.is_favorite",
"is_new": "bp.is_new",
}
func (h *ProductsHandler) AddToFavorites(c fiber.Ctx) error {
productIDStr := c.Params("product_id")
productID, err := strconv.Atoi(productIDStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
err = h.productService.AddToFavorites(userID, uint(productID))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *ProductsHandler) RemoveFromFavorites(c fiber.Ctx) error {
productIDStr := c.Params("product_id")
productID, err := strconv.Atoi(productIDStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
err = h.productService.RemoveFromFavorites(userID, uint(productID))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *ProductsHandler) ListProductVariants(c fiber.Ctx) error {
productIDStr := c.Params("product_id")
productID, err := strconv.Atoi(productIDStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
customer, ok := localeExtractor.GetCustomer(c)
if !ok || customer == nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
list, err := h.productService.GetProductAttributes(customer.LangID, uint(productID), constdata.SHOP_ID, customer.ID, customer.CountryID, constdata.DEFAULT_PRODUCT_QUANTITY)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&list, len(list), i18n.T_(c, response.Message_OK)))
} }

View File

@@ -4,7 +4,8 @@ import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/service/productTranslationService" "git.ma-al.com/goc_daniel/b2b/app/service/productTranslationService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
@@ -35,8 +36,8 @@ func ProductTranslationHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewProductTranslationHandler() handler := NewProductTranslationHandler()
r.Get("/get-product-description", handler.GetProductDescription) r.Get("/get-product-description", handler.GetProductDescription)
r.Post("/save-product-description", handler.SaveProductDescription) r.Post("/save-product-description", middleware.Require(perms.ProductTranslationSave), handler.SaveProductDescription)
r.Get("/translate-product-description", handler.TranslateProductDescription) r.Get("/translate-product-description", middleware.Require(perms.ProductTranslationTranslate), handler.TranslateProductDescription)
return r return r
} }
@@ -80,12 +81,6 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
productID_attribute := c.Query("productID") productID_attribute := c.Query("productID")
productID, err := strconv.Atoi(productID_attribute) productID, err := strconv.Atoi(productID_attribute)
if err != nil { if err != nil {
@@ -123,12 +118,6 @@ func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) err
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
productID_attribute := c.Query("productID") productID_attribute := c.Query("productID")
productID, err := strconv.Atoi(productID_attribute) productID, err := strconv.Atoi(productID_attribute)
if err != nil { if err != nil {

View File

@@ -4,7 +4,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService" "git.ma-al.com/goc_daniel/b2b/app/service/meiliService"
searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService" searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
@@ -30,7 +31,7 @@ func NewMeiliSearchHandler() *MeiliSearchHandler {
func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router { func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMeiliSearchHandler() handler := NewMeiliSearchHandler()
r.Get("/create-index", handler.CreateIndex) r.Get("/create-index", middleware.Require(perms.SearchCreateIndex), handler.CreateIndex)
r.Post("/search", handler.Search) r.Post("/search", handler.Search)
r.Post("/settings", handler.GetSettings) r.Post("/settings", handler.GetSettings)
@@ -44,12 +45,6 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
err := h.meiliService.CreateIndex(id_lang) err := h.meiliService.CreateIndex(id_lang)
if err != nil { if err != nil {
fmt.Printf("CreateIndex error: %v\n", err) fmt.Printf("CreateIndex error: %v\n", err)

View File

@@ -0,0 +1,160 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/specificPriceService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type SpecificPriceHandler struct {
SpecificPriceService *specificPriceService.SpecificPriceService
config *config.Config
}
func NewSpecificPriceHandler() *SpecificPriceHandler {
SpecificPriceService := specificPriceService.New()
return &SpecificPriceHandler{
SpecificPriceService: SpecificPriceService,
config: config.Get(),
}
}
func SpecificPriceHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewSpecificPriceHandler()
r.Post("/", middleware.Require(perms.SpecificPriceManage), handler.Create)
r.Put("/:id", middleware.Require(perms.SpecificPriceManage), handler.Update)
r.Delete("/:id", middleware.Require(perms.SpecificPriceManage), handler.Delete)
r.Get("/", middleware.Require(perms.SpecificPriceManage), handler.List)
r.Get("/:id", middleware.Require(perms.SpecificPriceManage), handler.GetByID)
r.Patch("/:id/activate", middleware.Require(perms.SpecificPriceManage), handler.Activate)
r.Patch("/:id/deactivate", middleware.Require(perms.SpecificPriceManage), handler.Deactivate)
return r
}
func (h *SpecificPriceHandler) Create(c fiber.Ctx) error {
var pr model.SpecificPrice
if err := c.Bind().Body(&pr); err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
result, err := h.SpecificPriceService.Create(c.Context(), &pr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Update(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
var pr model.SpecificPrice
if err := c.Bind().Body(&pr); err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
result, err := h.SpecificPriceService.Update(c.Context(), id, &pr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) List(c fiber.Ctx) error {
result, err := h.SpecificPriceService.List(c.Context())
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) GetByID(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
result, err := h.SpecificPriceService.GetByID(c.Context(), id)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Activate(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.SpecificPriceService.SetActive(c.Context(), id, true)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Deactivate(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.SpecificPriceService.SetActive(c.Context(), id, false)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Delete(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.SpecificPriceService.Delete(c.Context(), id)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -4,7 +4,8 @@ import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/service/storageService" "git.ma-al.com/goc_daniel/b2b/app/service/storageService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
@@ -34,7 +35,7 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/download-file/*", handler.DownloadFile) r.Get("/download-file/*", handler.DownloadFile)
// for admins only // for admins only
r.Get("/create-new-webdav-token", handler.CreateNewWebdavToken) r.Get("/create-new-webdav-token", middleware.Require(perms.WebdavCreateToken), handler.CreateNewWebdavToken)
return r return r
} }
@@ -84,12 +85,6 @@ func (h *StorageHandler) CreateNewWebdavToken(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
new_token, err := h.storageService.NewWebdavToken(userID) new_token, err := h.storageService.NewWebdavToken(userID)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).

View File

@@ -86,9 +86,10 @@ func (s *Server) Setup() error {
// API routes // API routes
s.api = s.app.Group("/api/v1") s.api = s.app.Group("/api/v1")
s.api.Use(middleware.Authenticate())
s.public = s.api.Group("/public") s.public = s.api.Group("/public")
s.restricted = s.api.Group("/restricted") s.restricted = s.api.Group("/restricted")
s.restricted.Use(middleware.AuthMiddleware()) s.restricted.Use(middleware.Authorize())
s.webdav = s.api.Group("/webdav") s.webdav = s.api.Group("/webdav")
s.webdav.Use(middleware.Webdav()) s.webdav.Use(middleware.Webdav())
@@ -132,6 +133,13 @@ func (s *Server) Setup() error {
carts := s.restricted.Group("/carts") carts := s.restricted.Group("/carts")
restricted.CartsHandlerRoutes(carts) restricted.CartsHandlerRoutes(carts)
// orders (restricted)
orders := s.restricted.Group("/orders")
restricted.OrdersHandlerRoutes(orders)
specificPrice := s.restricted.Group("/specific-price")
restricted.SpecificPriceHandlerRoutes(specificPrice)
// addresses (restricted) // addresses (restricted)
addresses := s.restricted.Group("/addresses") addresses := s.restricted.Group("/addresses")
restricted.AddressesHandlerRoutes(addresses) restricted.AddressesHandlerRoutes(addresses)
@@ -159,16 +167,6 @@ func (s *Server) Setup() error {
// }) // })
// }) // })
// // Admin routes example
// admin := s.api.Group("/admin")
// admin.Use(middleware.AuthMiddleware())
// admin.Use(middleware.RequireAdmin())
// admin.Get("/users", func(c fiber.Ctx) error {
// return c.JSON(fiber.Map{
// "message": "Admin area - user management",
// })
// })
// keep this at the end because its wilderange // keep this at the end because its wilderange
general.InitBo(s.App()) general.InitBo(s.App())

View File

@@ -3,7 +3,8 @@ package model
type Address struct { type Address struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"` CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"`
AddressInfo string `gorm:"column:address_info;not null" json:"address_info"` AddressString string `gorm:"column:address_string;not null" json:"address_string"`
AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"`
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"` CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"`
} }
@@ -11,15 +12,7 @@ func (Address) TableName() string {
return "b2b_addresses" return "b2b_addresses"
} }
type AddressUnparsed struct { type AddressUnparsed interface{}
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"`
AddressInfo AddressField `gorm:"column:address_info;not null" json:"address_info"`
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"`
}
type AddressField interface {
}
// Address template in Poland // Address template in Poland
type AddressPL struct { type AddressPL struct {

View File

@@ -11,6 +11,7 @@ type ScannedCategory struct {
IsoCode string `gorm:"column:iso_code"` IsoCode string `gorm:"column:iso_code"`
Visited bool // this is for internal backend use only Visited bool // this is for internal backend use only
Filter string // filter applicable to this category
} }
type Category struct { type Category struct {
@@ -25,6 +26,7 @@ type CategoryParams struct {
CategoryID uint `json:"category_id" form:"category_id"` CategoryID uint `json:"category_id" form:"category_id"`
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"` LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
Locale string `json:"locale" form:"locale"` Locale string `json:"locale" form:"locale"`
Filter string `json:"filter" form:"filter"`
} }
type CategoryInBreadcrumb struct { type CategoryInBreadcrumb struct {

View File

@@ -1,15 +1,13 @@
package model package model
import "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
// Represents a country together with its associated currency // Represents a country together with its associated currency
type Country struct { type Country struct {
ID uint `gorm:"primaryKey;column:id" json:"id"` ID uint `gorm:"primaryKey;column:id" json:"id"`
Name string `gorm:"column:name" json:"name"` Name string `gorm:"column:name" json:"name"`
Flag string `gorm:"size:16;not null;column:flag" json:"flag"` Flag string `gorm:"size:16;not null;column:flag" json:"flag"`
PSCurrencyID uint `gorm:"column:currency_id" json:"currency_id"` CurrencyID uint `gorm:"column:b2b_id_currency" json:"currency_id"`
PSCurrency *dbmodel.PsCurrency `gorm:"foreignKey:PSCurrencyID;references:IDCurrency" json:"ps_currency"` Currency *Currency `gorm:"foreignKey:CurrencyID" json:"currency,omitempty"`
} }
func (Country) TableName() string { func (Country) TableName() string {

View File

@@ -31,6 +31,7 @@ type Customer struct {
LastLoginAt *time.Time `json:"last_login_at,omitempty"` LastLoginAt *time.Time `json:"last_login_at,omitempty"`
LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language
CountryID uint `gorm:"default:2" json:"country_id"` // User's selected country CountryID uint `gorm:"default:2" json:"country_id"` // User's selected country
Country *Country `gorm:"foreignKey:CountryID" json:"country,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`

27
app/model/order.go Normal file
View File

@@ -0,0 +1,27 @@
package model
type CustomerOrder struct {
OrderID uint `gorm:"column:order_id;primaryKey;autoIncrement" json:"order_id"`
UserID uint `gorm:"column:user_id;not null;index" json:"user_id"`
Name string `gorm:"column:name;not null" json:"name"`
CountryID uint `gorm:"column:country_id;not null" json:"country_id"`
AddressString string `gorm:"column:address_string;not null" json:"address_string"`
AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"`
Status string `gorm:"column:status;size:50;not null" json:"status"`
Products []OrderProduct `gorm:"foreignKey:OrderID;references:OrderID" json:"products"`
}
func (CustomerOrder) TableName() string {
return "b2b_customer_orders"
}
type OrderProduct struct {
OrderID uint `gorm:"column:order_id;not null;index" json:"-"`
ProductID uint `gorm:"column:product_id;not null" json:"product_id"`
ProductAttributeID *uint `gorm:"column:product_attribute_id" json:"product_attribute_id,omitempty"`
Amount uint `gorm:"column:amount;not null" json:"amount"`
}
func (OrderProduct) TableName() string {
return "b2b_orders_products"
}

View File

@@ -1,66 +1,5 @@
package model package model
// Product contains each and every column from the table ps_product.
type Product struct {
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
SupplierID uint `gorm:"column:id_supplier" json:"supplier_id" form:"supplier_id"`
ManufacturerID uint `gorm:"column:id_manufacturer" json:"manufacturer_id" form:"manufacturer_id"`
CategoryDefaultID uint `gorm:"column:id_category_default" json:"category_default_id" form:"category_default_id"`
ShopDefaultID uint `gorm:"column:id_shop_default" json:"shop_default_id" form:"shop_default_id"`
TaxRulesGroupID uint `gorm:"column:id_tax_rules_group" json:"tax_rules_group_id" form:"tax_rules_group_id"`
OnSale uint `gorm:"column:on_sale" json:"on_sale" form:"on_sale"`
OnlineOnly uint `gorm:"column:online_only" json:"online_only" form:"online_only"`
EAN13 string `gorm:"column:ean13;type:varchar(13)" json:"ean13" form:"ean13"`
ISBN string `gorm:"column:isbn;type:varchar(32)" json:"isbn" form:"isbn"`
UPC string `gorm:"column:upc;type:varchar(12)" json:"upc" form:"upc"`
EkoTax float32 `gorm:"column:eko_tax;type:decimal(20,6)" json:"eko_tax" form:"eko_tax"`
Quantity uint `gorm:"column:quantity" json:"quantity" form:"quantity"`
MinimalQuantity uint `gorm:"column:minimal_quantity" json:"minimal_quantity" form:"minimal_quantity"`
LowStockThreshold uint `gorm:"column:low_stock_threshold" json:"low_stock_threshold" form:"low_stock_threshold"`
LowStockAlert uint `gorm:"column:low_stock_alert" json:"low_stock_alert" form:"low_stock_alert"`
Price float32 `gorm:"column:price;type:decimal(20,6)" json:"price" form:"price"`
WholesalePrice float32 `gorm:"column:wholesale_price;type:decimal(20,6)" json:"wholesale_price" form:"wholesale_price"`
Unity string `gorm:"column:unity;type:varchar(255)" json:"unity" form:"unity"`
UnitPriceRatio float32 `gorm:"column:unit_price_ratio;type:decimal(20,6)" json:"unit_price_ratio" form:"unit_price_ratio"`
UnitID uint `gorm:"column:id_unit;primaryKey" json:"unit_id" form:"unit_id"`
AdditionalShippingCost float32 `gorm:"column:additional_shipping_cost;type:decimal(20,2)" json:"additional_shipping_cost" form:"additional_shipping_cost"`
Reference string `gorm:"column:reference;type:varchar(64)" json:"reference" form:"reference"`
SupplierReference string `gorm:"column:supplier_reference;type:varchar(64)" json:"supplier_reference" form:"supplier_reference"`
Location string `gorm:"column:location;type:varchar(64)" json:"location" form:"location"`
Width float32 `gorm:"column:width;type:decimal(20,6)" json:"width" form:"width"`
Height float32 `gorm:"column:height;type:decimal(20,6)" json:"height" form:"height"`
Depth float32 `gorm:"column:depth;type:decimal(20,6)" json:"depth" form:"depth"`
Weight float32 `gorm:"column:weight;type:decimal(20,6)" json:"weight" form:"weight"`
OutOfStock uint `gorm:"column:out_of_stock" json:"out_of_stock" form:"out_of_stock"`
AdditionalDeliveryTimes uint `gorm:"column:additional_delivery_times" json:"additional_delivery_times" form:"additional_delivery_times"`
QuantityDiscount uint `gorm:"column:quantity_discount" json:"quantity_discount" form:"quantity_discount"`
Customizable uint `gorm:"column:customizable" json:"customizable" form:"customizable"`
UploadableFiles uint `gorm:"column:uploadable_files" json:"uploadable_files" form:"uploadable_files"`
TextFields uint `gorm:"column:text_fields" json:"text_fields" form:"text_fields"`
Active uint `gorm:"column:active" json:"active" form:"active"`
RedirectType string `gorm:"column:redirect_type;type:enum('','404','301-product','302-product','301-category','302-category')" json:"redirect_type" form:"redirect_type"`
TypeRedirectedID int `gorm:"column:id_type_redirected" json:"type_redirected_id" form:"type_redirected_id"`
AvailableForOrder uint `gorm:"column:available_for_order" json:"available_for_order" form:"available_for_order"`
AvailableDate string `gorm:"column:available_date;type:date" json:"available_date" form:"available_date"`
ShowCondition uint `gorm:"column:show_condition" json:"show_condition" form:"show_condition"`
Condition string `gorm:"column:condition;type:enum('new','used','refurbished')" json:"condition" form:"condition"`
ShowPrice uint `gorm:"column:show_price" json:"show_price" form:"show_price"`
Indexed uint `gorm:"column:indexed" json:"indexed" form:"indexed"`
Visibility string `gorm:"column:visibility;type:enum('both','catalog','search','none')" json:"visibility" form:"visibility"`
CacheIsPack uint `gorm:"column:cache_is_pack" json:"cache_is_pack" form:"cache_is_pack"`
CacheHasAttachments uint `gorm:"column:cache_has_attachments" json:"cache_has_attachments" form:"cache_has_attachments"`
IsVirtual uint `gorm:"column:is_virtual" json:"is_virtual" form:"is_virtual"`
CacheDefaultAttribute uint `gorm:"column:cache_default_attribute" json:"cache_default_attribute" form:"cache_default_attribute"`
DateAdd string `gorm:"column:date_add;type:datetime" json:"date_add" form:"date_add"`
DateUpd string `gorm:"column:date_upd;type:datetime" json:"date_upd" form:"date_upd"`
AdvancedStockManagement uint `gorm:"column:advanced_stock_management" json:"advanced_stock_management" form:"advanced_stock_management"`
PackStockType uint `gorm:"column:pack_stock_type" json:"pack_stock_type" form:"pack_stock_type"`
State uint `gorm:"column:state" json:"state" form:"state"`
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
}
type ProductInList struct { type ProductInList struct {
ProductID uint `gorm:"column:product_id" json:"product_id" form:"product_id"` ProductID uint `gorm:"column:product_id" json:"product_id" form:"product_id"`
Name string `gorm:"column:name" json:"name" form:"name"` Name string `gorm:"column:name" json:"name" form:"name"`
@@ -70,6 +9,10 @@ type ProductInList struct {
Reference string `gorm:"column:reference" json:"reference"` Reference string `gorm:"column:reference" json:"reference"`
VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"` VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"`
Quantity int64 `gorm:"column:quantity" json:"quantity"` Quantity int64 `gorm:"column:quantity" json:"quantity"`
PriceTaxExcl float64 `gorm:"column:price_tax_excl" json:"price_tax_excl"`
PriceTaxIncl float64 `gorm:"column:price_tax_incl" json:"price_tax_incl"`
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
IsNew uint `gorm:"column:is_new" json:"is_new"`
} }
type ProductFilters struct { type ProductFilters struct {
@@ -85,3 +28,12 @@ type ProductFilters struct {
} }
type FeatVal = map[uint][]uint type FeatVal = map[uint][]uint
type B2bFavorite struct {
UserID uint `gorm:"column:user_id;not null;primaryKey" json:"user_id"`
ProductID uint `gorm:"column:product_id;not null;primaryKey" json:"product_id"`
}
func (*B2bFavorite) TableName() string {
return "b2b_favorites"
}

View File

@@ -7,7 +7,6 @@ type Route struct {
Component string `gorm:"type:varchar(255);not null;comment:path to component file" json:"component"` Component string `gorm:"type:varchar(255);not null;comment:path to component file" json:"component"`
Meta *string `gorm:"type:longtext;default:'{}'" json:"meta,omitempty"` Meta *string `gorm:"type:longtext;default:'{}'" json:"meta,omitempty"`
Active *bool `gorm:"type:tinyint;default:1" json:"active,omitempty"` Active *bool `gorm:"type:tinyint;default:1" json:"active,omitempty"`
SortOrder *int `gorm:"type:int;default:0" json:"sort_order,omitempty"`
} }
func (Route) TableName() string { func (Route) TableName() string {

View File

@@ -0,0 +1,29 @@
package model
import "time"
type SpecificPrice struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
ValidFrom *time.Time `gorm:"null" json:"valid_from"`
ValidTill *time.Time `gorm:"null" json:"valid_till"`
HasExpirationDate bool `gorm:"default:false" json:"has_expiration_date"`
ReductionType string `gorm:"type:enum('amount','percentage');not null" json:"reduction_type"`
Price *float64 `gorm:"type:decimal(10,2);null" json:"price"`
CurrencyID *uint64 `gorm:"column:b2b_id_currency;null" json:"currency_id"`
PercentageReduction *float64 `gorm:"type:decimal(5,2);null" json:"percentage_reduction"`
FromQuantity uint32 `gorm:"default:1" json:"from_quantity"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt *time.Time `gorm:"null" json:"created_at"`
UpdatedAt *time.Time `gorm:"null" json:"updated_at"`
ProductIDs []uint64 `gorm:"-" json:"product_ids"`
CategoryIDs []uint64 `gorm:"-" json:"category_ids"`
ProductAttributeIDs []uint64 `gorm:"-" json:"product_attribute_ids"`
CountryIDs []uint64 `gorm:"-" json:"country_ids"`
CustomerIDs []uint64 `gorm:"-" json:"customer_ids"`
}
func (SpecificPrice) TableName() string {
return "b2b_specific_price"
}

View File

@@ -49,7 +49,7 @@ func (repo *AddressesRepo) UserAddressesAmt(user_id uint) (uint, error) {
func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error { func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error {
address := model.Address{ address := model.Address{
CustomerID: user_id, CustomerID: user_id,
AddressInfo: address_info, AddressString: address_info,
CountryID: country_id, CountryID: country_id,
} }
@@ -62,7 +62,7 @@ func (repo *AddressesRepo) UpdateAddress(user_id uint, address_id uint, address_
address := model.Address{ address := model.Address{
ID: address_id, ID: address_id,
CustomerID: user_id, CustomerID: user_id,
AddressInfo: address_info, AddressString: address_info,
CountryID: country_id, CountryID: country_id,
} }

View File

@@ -1,20 +1,26 @@
package cartsRepo package cartsRepo
import ( import (
"errors"
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"gorm.io/gorm"
) )
type UICartsRepo interface { type UICartsRepo interface {
CartsAmount(user_id uint) (uint, error) CartsAmount(user_id uint) (uint, error)
CreateNewCart(user_id uint) (model.CustomerCart, error) CreateNewCart(user_id uint) (model.CustomerCart, error)
UserHasCart(user_id uint, cart_id uint) (uint, error) RemoveCart(user_id uint, cart_id uint) error
UserHasCart(user_id uint, cart_id uint) (bool, error)
UpdateCartName(user_id uint, cart_id uint, new_name string) error UpdateCartName(user_id uint, cart_id uint, new_name string) error
RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error)
RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error)
CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error) CheckProductExists(product_id uint, product_attribute_id *uint) (bool, error)
AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error AddProduct(cart_id uint, product_id uint, product_attribute_id *uint, amount uint, set_amount bool) error
RemoveProduct(cart_id uint, product_id uint, product_attribute_id *uint) error
} }
type CartsRepo struct{} type CartsRepo struct{}
@@ -49,7 +55,15 @@ func (repo *CartsRepo) CreateNewCart(user_id uint) (model.CustomerCart, error) {
return cart, err return cart, err
} }
func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) { func (repo *CartsRepo) RemoveCart(user_id uint, cart_id uint) error {
return db.DB.
Table("b2b_customer_carts").
Where("cart_id = ? AND user_id = ?", cart_id, user_id).
Delete(nil).
Error
}
func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (bool, error) {
var amt uint var amt uint
err := db.DB. err := db.DB.
@@ -59,7 +73,7 @@ func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) {
Scan(&amt). Scan(&amt).
Error Error
return amt, err return amt >= 1, err
} }
func (repo *CartsRepo) UpdateCartName(user_id uint, cart_id uint, new_name string) error { func (repo *CartsRepo) UpdateCartName(user_id uint, cart_id uint, new_name string) error {
@@ -96,7 +110,7 @@ func (repo *CartsRepo) RetrieveCart(user_id uint, cart_id uint) (*model.Customer
return &cart, err return &cart, err
} }
func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error) { func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id *uint) (bool, error) {
var amt uint var amt uint
if product_attribute_id == nil { if product_attribute_id == nil {
@@ -106,7 +120,7 @@ func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id
Where("id_product = ?", product_id). Where("id_product = ?", product_id).
Scan(&amt). Scan(&amt).
Error Error
return amt, err return amt >= 1, err
} else { } else {
err := db.DB. err := db.DB.
@@ -116,18 +130,65 @@ func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id
Where("ps.id_product = ? AND pas.id_product_attribute = ?", product_id, *product_attribute_id). Where("ps.id_product = ? AND pas.id_product_attribute = ?", product_id, *product_attribute_id).
Scan(&amt). Scan(&amt).
Error Error
return amt, err return amt >= 1, err
} }
} }
func (repo *CartsRepo) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error { func (repo *CartsRepo) AddProduct(cart_id uint, product_id uint, product_attribute_id *uint, amount uint, set_amount bool) error {
product := model.CartProduct{ var product model.CartProduct
err := db.DB.
Where(&model.CartProduct{
CartID: cart_id,
ProductID: product_id,
ProductAttributeID: product_attribute_id,
}).
First(&product).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
if amount < 1 {
return responseErrors.ErrAmountMustBePositive
} else if amount > constdata.MAX_AMOUNT_OF_PRODUCT_IN_CART {
return responseErrors.ErrAmountMustBeReasonable
}
product = model.CartProduct{
CartID: cart_id, CartID: cart_id,
ProductID: product_id, ProductID: product_id,
ProductAttributeID: product_attribute_id, ProductAttributeID: product_attribute_id,
Amount: amount, Amount: amount,
} }
err := db.DB.Create(&product).Error
return db.DB.Create(&product).Error
}
// Some other DB error
return err return err
} }
// Product already exists in cart
if set_amount {
product.Amount = amount
} else {
product.Amount = product.Amount + amount
}
if product.Amount < 1 {
return responseErrors.ErrAmountMustBePositive
} else if product.Amount > constdata.MAX_AMOUNT_OF_PRODUCT_IN_CART {
return responseErrors.ErrAmountMustBeReasonable
}
return db.DB.Save(&product).Error
}
func (repo *CartsRepo) RemoveProduct(cart_id uint, product_id uint, product_attribute_id *uint) error {
return db.DB.
Where(&model.CartProduct{
CartID: cart_id,
ProductID: product_id,
ProductAttributeID: product_attribute_id,
}).
Delete(&model.CartProduct{}).Error
}

View File

@@ -1,48 +0,0 @@
package categoriesRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
)
type UICategoriesRepo interface {
GetAllCategories(idLang uint) ([]model.ScannedCategory, error)
}
type CategoriesRepo struct{}
func New() UICategoriesRepo {
return &CategoriesRepo{}
}
func (r *CategoriesRepo) GetAllCategories(idLang uint) ([]model.ScannedCategory, error) {
var allCategories []model.ScannedCategory
categoryTbl := (&dbmodel.PsCategory{}).TableName()
categoryLangTbl := (&dbmodel.PsCategoryLang{}).TableName()
categoryShopTbl := (&dbmodel.PsCategoryShop{}).TableName()
langTbl := (&dbmodel.PsLang{}).TableName()
err := db.Get().
Model(dbmodel.PsCategory{}).
Select(`
ps_category.id_category AS category_id,
ps_category_lang.name AS name,
ps_category.active AS active,
ps_category_shop.position AS position,
ps_category.id_parent AS id_parent,
ps_category.is_root_category AS is_root_category,
ps_category_lang.link_rewrite AS link_rewrite,
ps_lang.iso_code AS iso_code
`).
Joins(`LEFT JOIN `+categoryLangTbl+` ON `+categoryLangTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryLangTbl+`.id_shop = ? AND `+categoryLangTbl+`.id_lang = ?`,
constdata.SHOP_ID, idLang).
Joins(`LEFT JOIN `+categoryShopTbl+` ON `+categoryShopTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryShopTbl+`.id_shop = ?`,
constdata.SHOP_ID).
Joins(`JOIN ` + langTbl + ` ON ` + langTbl + `.id_lang = ` + categoryLangTbl + `.id_lang`).
Scan(&allCategories).Error
return allCategories, err
}

View File

@@ -2,11 +2,14 @@ package categoryrepo
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
) )
type UICategoryRepo interface { type UICategoryRepo interface {
GetCategoryTranslations(ids []uint, idLang uint) (map[uint]string, error) GetCategoryTranslations(ids []uint, idLang uint) (map[uint]string, error)
RetrieveMenuCategories(idLang uint) ([]model.ScannedCategory, error)
} }
type CategoryRepo struct{} type CategoryRepo struct{}
@@ -42,3 +45,33 @@ func (r *CategoryRepo) GetCategoryTranslations(ids []uint, idLang uint) (map[uin
return translations, nil return translations, nil
} }
func (r *CategoryRepo) RetrieveMenuCategories(idLang uint) ([]model.ScannedCategory, error) {
var allCategories []model.ScannedCategory
categoryTbl := (&dbmodel.PsCategory{}).TableName()
categoryLangTbl := (&dbmodel.PsCategoryLang{}).TableName()
categoryShopTbl := (&dbmodel.PsCategoryShop{}).TableName()
langTbl := (&dbmodel.PsLang{}).TableName()
err := db.Get().
Model(dbmodel.PsCategory{}).
Select(`
ps_category.id_category AS category_id,
ps_category_lang.name AS name,
ps_category.active AS active,
ps_category_shop.position AS position,
ps_category.id_parent AS id_parent,
ps_category.is_root_category AS is_root_category,
ps_category_lang.link_rewrite AS link_rewrite,
ps_lang.iso_code AS iso_code
`).
Joins(`LEFT JOIN `+categoryLangTbl+` ON `+categoryLangTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryLangTbl+`.id_shop = ? AND `+categoryLangTbl+`.id_lang = ?`,
constdata.SHOP_ID, idLang).
Joins(`LEFT JOIN `+categoryShopTbl+` ON `+categoryShopTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryShopTbl+`.id_shop = ?`,
constdata.SHOP_ID).
Joins(`JOIN ` + langTbl + ` ON ` + langTbl + `.id_lang = ` + categoryLangTbl + `.id_lang`).
Scan(&allCategories).Error
return allCategories, err
}

View File

@@ -1,6 +1,7 @@
package customerRepo package customerRepo
import ( import (
"fmt"
"strings" "strings"
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
@@ -80,13 +81,16 @@ func (repo *CustomerRepo) Find(langId uint, p find.Paging, filt *filters.Filters
for _, word := range words { for _, word := range words {
conditions = append(conditions, ` conditions = append(conditions, `
(LOWER(first_name) LIKE ? OR (
id = ? OR
LOWER(first_name) LIKE ? OR
LOWER(last_name) LIKE ? OR LOWER(last_name) LIKE ? OR
LOWER(email) LIKE ?) LOWER(email) LIKE ?)
`) `)
args = append(args, strings.ToLower(word))
for range 3 { for range 3 {
args = append(args, "%"+strings.ToLower(word)+"%") args = append(args, fmt.Sprintf("%%%s%%", strings.ToLower(word)))
} }
} }
@@ -110,88 +114,3 @@ func (repo *CustomerRepo) Save(customer *model.Customer) error {
func (repo *CustomerRepo) Create(customer *model.Customer) error { func (repo *CustomerRepo) Create(customer *model.Customer) error {
return db.DB.Create(customer).Error return db.DB.Create(customer).Error
} }
// func (repo *CustomerRepo) Search(
// customerId uint,
// partnerCode string,
// p find.Paging,
// filt *filters.FiltersList,
// search string,
// ) (found find.Found[model.UserInList], err error) {
// words := strings.Fields(search)
// if len(words) > 5 {
// words = words[:5]
// }
// query := ctx.DB().
// Model(&model.Customer{}).
// Select("customer.id AS id, customer.first_name as first_name, customer.last_name as last_name, customer.phone_number AS phone_number, customer.email AS email, count(distinct investment_plan_contract.id) as iiplan_purchases, count(distinct `order`.id) as single_purchases, entity.name as entity_name").
// Where("customer.id <> ?", customerId).
// Where("(customer.id IN (SELECT id FROM customer WHERE partner_code IN (WITH RECURSIVE partners AS (SELECT code AS dst FROM partner WHERE code = ? UNION SELECT code FROM partner JOIN partners ON partners.dst = partner.superior_code) SELECT dst FROM partners)) OR customer.recommender_code = ?)", partnerCode, partnerCode).
// Scopes(view.CustomerListQuery())
// var conditions []string
// var args []interface{}
// for _, word := range words {
// conditions = append(conditions, `
// (LOWER(first_name) LIKE ? OR
// LOWER(last_name) LIKE ? OR
// phone_number LIKE ? OR
// LOWER(email) LIKE ?)
// `)
// for i := 0; i < 4; i++ {
// args = append(args, "%"+strings.ToLower(word)+"%")
// }
// }
// finalQuery := strings.Join(conditions, " AND ")
// query = query.Where(finalQuery, args...).
// Scopes(filt.All()...)
// found, err = find.Paginate[V](ctx, p, query)
// return found, errs.Recorded(span, err)
// }
// func (repo *ListRepo) ListUsers(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.UserInList], error) {
// var list []model.UserInList
// var total int64
// query := db.Get().
// Table("b2b_customers AS users").
// Select(`
// users.id AS id,
// users.email AS email,
// users.first_name AS first_name,
// users.last_name AS last_name,
// users.role AS role
// `)
// // Apply all filters
// if filt != nil {
// filt.ApplyAll(query)
// }
// // run counter first as query is without limit and offset
// err := query.Count(&total).Error
// if err != nil {
// return find.Found[model.UserInList]{}, err
// }
// err = query.
// Order("users.id DESC").
// Limit(p.Limit()).
// Offset(p.Offset()).
// Find(&list).Error
// if err != nil {
// return find.Found[model.UserInList]{}, err
// }
// return find.Found[model.UserInList]{
// Items: list,
// Count: uint(total),
// }, nil
// }

View File

@@ -3,6 +3,7 @@ package localeSelectorRepo
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
) )
type UILocaleSelectorRepo interface { type UILocaleSelectorRepo interface {
@@ -25,7 +26,9 @@ func (r *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) {
func (r *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) { func (r *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
var countries []model.Country var countries []model.Country
err := db.Get(). err := db.Get().
Preload("PSCurrency"). Select("*").
Preload("Currency").
Joins("LEFT JOIN " + dbmodel.TableNamePsCountryLang + " AS cl ON cl." + dbmodel.PsCountryLangCols.IDCountry.Col() + " = b2b_countries.ps_id_country AND cl." + dbmodel.PsCountryLangCols.IDLang.Col() + " = 2").
Find(&countries).Error Find(&countries).Error
return countries, err return countries, err
} }

View File

@@ -0,0 +1,110 @@
package ordersRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
)
type UIOrdersRepo interface {
UserHasOrder(user_id uint, order_id uint) (bool, error)
Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error)
PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string) error
ChangeOrderAddress(order_id uint, country_id uint, address_info string) error
ChangeOrderStatus(order_id uint, status string) error
}
type OrdersRepo struct{}
func New() UIOrdersRepo {
return &OrdersRepo{}
}
func (repo *OrdersRepo) UserHasOrder(user_id uint, order_id uint) (bool, error) {
var amt uint
err := db.DB.
Table("b2b_customer_orders").
Select("COUNT(*) AS amt").
Where("user_id = ? AND order_id = ?", user_id, order_id).
Scan(&amt).
Error
return amt >= 1, err
}
func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) {
var list []model.CustomerOrder
var total int64
query := db.Get().
Model(&model.CustomerOrder{}).
Preload("Products").
Order("b2b_customer_orders.order_id DESC")
// Apply all filters
if filt != nil {
filt.ApplyAll(query)
}
// run counter first as query is without limit and offset
err := query.Count(&total).Error
if err != nil {
return &find.Found[model.CustomerOrder]{}, err
}
err = query.
Limit(p.Limit()).
Offset(p.Offset()).
Find(&list).Error
if err != nil {
return &find.Found[model.CustomerOrder]{}, err
}
return &find.Found[model.CustomerOrder]{
Items: list,
Count: uint(total),
}, nil
}
func (repo *OrdersRepo) PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string) error {
order := model.CustomerOrder{
UserID: cart.UserID,
Name: name,
CountryID: country_id,
AddressString: address_info,
Status: constdata.NEW_ORDER_STATUS,
Products: make([]model.OrderProduct, 0, len(cart.Products)),
}
for _, product := range cart.Products {
order.Products = append(order.Products, model.OrderProduct{
ProductID: product.ProductID,
ProductAttributeID: product.ProductAttributeID,
Amount: product.Amount,
})
}
return db.DB.Create(&order).Error
}
func (repo *OrdersRepo) ChangeOrderAddress(order_id uint, country_id uint, address_info string) error {
return db.DB.
Table("b2b_customer_orders").
Where("order_id = ?", order_id).
Updates(map[string]interface{}{
"country_id": country_id,
"address_string": address_info,
}).
Error
}
func (repo *OrdersRepo) ChangeOrderStatus(order_id uint, status string) error {
return db.DB.
Table("b2b_customer_orders").
Where("order_id = ?", order_id).
Update("status", status).
Error
}

View File

@@ -9,7 +9,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -17,7 +16,6 @@ type UIProductDescriptionRepo interface {
GetProductDescription(productID uint, productid_lang uint) (*model.ProductDescription, error) GetProductDescription(productID uint, productid_lang uint) (*model.ProductDescription, error)
CreateIfDoesNotExist(productID uint, productid_lang uint) error CreateIfDoesNotExist(productID uint, productid_lang uint) error
UpdateFields(productID uint, productid_lang uint, updates map[string]string) error UpdateFields(productID uint, productid_lang uint, updates map[string]string) error
GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error)
} }
type ProductDescriptionRepo struct{} type ProductDescriptionRepo struct{}
@@ -118,108 +116,3 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productid_lang uin
return nil return nil
} }
// GetMeiliProductsBatchedScanned returns a batch of products with LIMIT/OFFSET pagination
// The scanning is done inside the repo to keep the service layer cleaner
func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error) {
var products []model.MeiliSearchProduct
err := db.Get().
Table("ps_product_shop ps").
Select(`
ps.id_product AS id_product,
pl.name AS name,
TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description_short, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description,
p.ean13,
p.reference,
ps.price,
ps.id_category_default AS id_category,
cl.name AS cat_name,
cl.link_rewrite AS l_rew,
COALESCE(vary.attributes, JSON_OBJECT()) AS attr,
COALESCE(feat.features, JSON_OBJECT()) AS feat,
img.id_image,
cat.category_ids,
(SELECT COUNT(*) FROM ps_product_attribute_shop pas2 WHERE pas2.id_product = ps.id_product AND pas2.id_shop = ?) AS variations
`, constdata.SHOP_ID).
Joins("JOIN ps_product p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("LEFT JOIN variations vary ON vary.id_product = ps.id_product").
Joins("LEFT JOIN features feat ON feat.id_product = ps.id_product").
Joins("LEFT JOIN images img ON img.id_product = ps.id_product").
Joins("LEFT JOIN categories cat ON cat.id_product = ps.id_product").
Joins("JOIN products_page pp ON pp.id_product = ps.id_product").
Where("ps.active = ?", 1).
Order("ps.id_product").
Clauses(exclause.With{CTEs: []exclause.CTE{
{
Name: "products_page",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsProductShop{}).
Select("id_product, price").
Where("id_shop = ? AND active = 1", constdata.SHOP_ID).
Order("id_product").
Limit(limit).
Offset(offset),
},
},
{
Name: "variation_attributes",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_product_attribute_shop pas"). // <- explicit alias here
Select(`
pas.id_product,
pag.id_attribute_group AS attribute_name,
JSON_ARRAYAGG(DISTINCT pa.id_attribute) AS attribute_values
`).
Joins("JOIN ps_product_attribute_combination ppac ON ppac.id_product_attribute = pas.id_product_attribute").
Joins("JOIN ps_attribute pa ON pa.id_attribute = ppac.id_attribute").
Joins("JOIN ps_attribute_group pag ON pag.id_attribute_group = pa.id_attribute_group").
Where("pas.id_shop = ?", constdata.SHOP_ID).
Group("pas.id_product, pag.id_attribute_group"),
},
},
{
Name: "variations",
Subquery: exclause.Subquery{
DB: db.Get().
Table("variation_attributes").
Select("id_product, JSON_OBJECTAGG(attribute_name, attribute_values) AS attributes").
Group("id_product"),
},
},
{
Name: "features",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_feature_product pfp"). // <- explicit alias
Select("pfp.id_product, JSON_OBJECTAGG(pfp.id_feature, pfp.id_feature_value) AS features").
Group("pfp.id_product"),
},
},
{
Name: "images",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsImageShop{}).
Select("id_product, id_image").
Where("id_shop = ? AND cover = 1", constdata.SHOP_ID),
},
},
{
Name: "categories",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsCategoryProduct{}).
Select("id_product, JSON_ARRAYAGG(id_category) AS category_ids").
Group("id_product"),
},
},
}}).Find(&products).Error
return products, err
}

View File

@@ -2,7 +2,6 @@ package productsRepo
import ( import (
"encoding/json" "encoding/json"
"fmt"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
@@ -10,13 +9,22 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/app/view"
"git.ma-al.com/goc_marek/gormcol" "git.ma-al.com/goc_marek/gormcol"
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause" "github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
) )
type UIProductsRepo interface { type UIProductsRepo interface {
GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) // GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error)
Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) Find(id_lang uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error)
GetProductVariants(langID uint, productID uint, shopID uint, customerID uint, countryID uint, quantity uint) ([]view.ProductAttribute, error)
GetBase(p_id_product, p_id_shop, p_id_lang, p_id_customer uint) (view.Product, error)
GetPrice(p_id_product uint, productAttributeID *uint, p_id_shop uint, p_id_customer uint, p_id_country uint, p_quantity uint) (view.Price, error)
GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity uint) ([]view.ProductAttribute, error)
AddToFavorites(userID uint, productID uint) error
RemoveFromFavorites(userID uint, productID uint) error
ExistsInFavorites(userID uint, productID uint) (bool, error)
ProductInDatabase(productID uint) (bool, error)
} }
type ProductsRepo struct{} type ProductsRepo struct{}
@@ -25,81 +33,251 @@ func New() UIProductsRepo {
return &ProductsRepo{} return &ProductsRepo{}
} }
func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) { func (repo *ProductsRepo) GetBase(p_id_product, p_id_shop, p_id_lang, p_id_customer uint) (view.Product, error) {
var productStr string // ← Scan as string first var result view.Product
err := db.DB.Raw(`CALL get_full_product(?,?,?,?,?,?)`, err := db.DB.Raw(`CALL get_product_base(?,?,?,?)`,
p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity). p_id_product, p_id_shop, p_id_lang, p_id_customer).
Scan(&productStr). Scan(&result).Error
Error
return result, err
}
func (repo *ProductsRepo) GetPrice(
p_id_product uint, productAttributeID *uint, p_id_shop uint, p_id_customer uint, p_id_country uint, p_quantity uint,
) (view.Price, error) {
type row struct {
Price json.RawMessage `gorm:"column:price"`
}
var r row
err := db.DB.Raw(`
SELECT fn_product_price(?,?,?,?,?,?) AS price`,
p_id_product, p_id_shop, p_id_customer, p_id_country, p_quantity, productAttributeID).
Scan(&r).Error
if err != nil {
return view.Price{}, err
}
var temp struct {
Base json.Number `json:"base"`
FinalTaxExcl json.Number `json:"final_tax_excl"`
FinalTaxIncl json.Number `json:"final_tax_incl"`
TaxRate json.Number `json:"tax_rate"`
Priority json.Number `json:"priority"`
}
if err := json.Unmarshal(r.Price, &temp); err != nil {
return view.Price{}, err
}
price := view.Price{
Base: mustParseFloat(temp.Base),
FinalTaxExcl: mustParseFloat(temp.FinalTaxExcl),
FinalTaxIncl: mustParseFloat(temp.FinalTaxIncl),
TaxRate: mustParseFloat(temp.TaxRate),
Priority: mustParseInt(temp.Priority),
}
return price, nil
}
func mustParseFloat(n json.Number) float64 {
f, _ := n.Float64()
return f
}
func mustParseInt(n json.Number) int {
i, _ := n.Int64()
return int(i)
}
func (repo *ProductsRepo) GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity uint) ([]view.ProductAttribute, error) {
var results []view.ProductAttribute
err := db.DB.Raw(`
CALL get_product_variants(?,?,?,?,?,?)`,
p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity).
Scan(&results).Error
return results, err
}
func (repo *ProductsRepo) Find(langID uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error) {
query := db.DB.
Table("base_products AS bp").
Clauses(exclause.With{
CTEs: []exclause.CTE{
{
Name: "favorites",
Subquery: exclause.Subquery{
DB: db.DB.
Table("b2b_favorites").
Select(`
product_id AS product_id,
COUNT(*) > 0 AS is_favorite
`).
Where("user_id = ?", userID).
Group("product_id"),
},
},
{
Name: "new_product_days",
Subquery: exclause.Subquery{
DB: db.DB.
Table("ps_configuration").
Select("CAST(value AS SIGNED) AS days").
Where("name = ?", "PS_NB_DAYS_NEW_PRODUCT"),
},
},
{
Name: "variants",
Subquery: exclause.Subquery{
DB: db.DB.
Table("ps_product_attribute_shop").
Select("id_product, COUNT(*) AS variants_number").
Group("id_product"),
},
},
{
Name: "base_products",
Subquery: exclause.Subquery{
DB: db.DB.
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
Select(`
ps.id_product AS product_id,
pl.name AS name,
ps.id_category_default AS category_id,
p.reference AS reference,
sa.quantity AS quantity,
COALESCE(f.is_favorite, 0) AS is_favorite,
CASE
WHEN ps.date_add >= DATE_SUB(
NOW(),
INTERVAL COALESCE(npd.days, 20) DAY
) AND ps.active = 1
THEN 1
ELSE 0
END AS is_new
`).
Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", langID).
Joins("LEFT JOIN favorites f ON f.product_id = ps.id_product").
Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0").
Joins("LEFT JOIN new_product_days npd ON 1 = 1").
Where("ps.active = ?", 1).
Group("ps.id_product"),
},
},
},
}).
Select(`
bp.product_id AS product_id,
bp.name AS name,
pl.link_rewrite AS link_rewrite,
CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
cl.name AS category_name,
bp.reference AS reference,
COALESCE(v.variants_number, 0) AS variants_number,
bp.quantity AS quantity,
bp.is_favorite AS is_favorite,
bp.is_new AS is_new
`, config.Get().Image.ImagePrefix).
Joins("JOIN ps_product_lang pl ON pl.id_product = bp.product_id AND pl.id_lang = ?", langID).
Joins("JOIN ps_image_shop ims ON ims.id_product = bp.product_id AND ims.cover = 1").
Joins("JOIN ps_category_lang cl ON cl.id_category = bp.category_id AND cl.id_lang = ?", langID).
Joins("LEFT JOIN variants v ON v.id_product = bp.product_id").
Order("bp.product_id DESC")
query = query.Scopes(filt.All()...)
list, err := find.Paginate[model.ProductInList](langID, p, query)
if err != nil {
return nil, err
}
return &list, nil
}
func (repo *ProductsRepo) GetProductVariants(langID uint, productID uint, shopID uint, customerID uint, countryID uint, quantity uint) ([]view.ProductAttribute, error) {
var result []view.ProductAttribute
err := db.DB.
Raw(`
CALL get_product_attributes_with_price(?, ?, ?, ?, ?, ?)
`,
langID,
productID,
shopID,
customerID,
countryID,
quantity,
).
Scan(&result).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Optional: validate it's valid JSON return result, nil
if !json.Valid([]byte(productStr)) {
return nil, fmt.Errorf("invalid json returned from stored procedure")
} }
raw := json.RawMessage(productStr) func (repo *ProductsRepo) PopulateProductPrice(product *model.ProductInList, targetCustomer *model.Customer, quantity int, shopID uint) error {
return &raw, nil row := db.Get().Raw(
} "CALL get_product_price(?, ?, ?, ?, ?)",
product.ProductID,
shopID,
targetCustomer.ID,
targetCustomer.CountryID,
quantity,
).Row()
func (repo *ProductsRepo) Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { var (
var list []model.ProductInList id uint
var total int64 base float64
excl float64
incl float64
tax float64
)
query := db.Get(). err := row.Scan(&id, &base, &excl, &incl, &tax)
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
Select(`
ps.id_product AS product_id,
pl.name AS name,
pl.link_rewrite AS link_rewrite,
CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
cl.name AS category_name,
p.reference AS reference,
COALESCE(v.variants_number, 0) AS variants_number,
sa.quantity AS quantity
`, config.Get().Image.ImagePrefix).
Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", id_lang).
Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1").
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", id_lang).
Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product").
Joins("LEFT JOIN variants v ON v.id_product = ps.id_product").
Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0").
Where("ps.active = ?", 1).
Group("ps.id_product").
Clauses(exclause.With{CTEs: []exclause.CTE{
{
Name: "variants",
Subquery: exclause.Subquery{DB: db.Get().Model(&dbmodel.PsProductAttributeShop{}).Select("id_product", "COUNT(*) AS variants_number").Group("id_product")},
},
}}).
Order("ps.id_product DESC")
// Apply all filters
if filt != nil {
filt.ApplyAll(query)
}
// run counter first as query is without limit and offset
err := query.Count(&total).Error
if err != nil { if err != nil {
return find.Found[model.ProductInList]{}, err return err
} }
err = query. product.PriceTaxExcl = excl
Limit(p.Limit()). product.PriceTaxIncl = incl
Offset(p.Offset()). return nil
Find(&list).Error
if err != nil {
return find.Found[model.ProductInList]{}, err
} }
return find.Found[model.ProductInList]{ func (repo *ProductsRepo) AddToFavorites(userID uint, productID uint) error {
Items: list, fav := model.B2bFavorite{
Count: uint(total), UserID: userID,
}, nil ProductID: productID,
}
return db.Get().Create(&fav).Error
}
func (repo *ProductsRepo) RemoveFromFavorites(userID uint, productID uint) error {
return db.Get().
Where("user_id = ? AND product_id = ?", userID, productID).
Delete(&model.B2bFavorite{}).Error
}
func (repo *ProductsRepo) ExistsInFavorites(userID uint, productID uint) (bool, error) {
var count int64
err := db.Get().
Table("b2b_favorites").
Where("user_id = ? AND product_id = ?", userID, productID).
Count(&count).Error
return count >= 1, err
}
func (repo *ProductsRepo) ProductInDatabase(productID uint) (bool, error) {
var count int64
err := db.Get().
Table(dbmodel.TableNamePsProduct).
Where(dbmodel.PsProductCols.IDProduct.Col()+" = ?", productID).
Count(&count).Error
return count >= 1, err
} }

View File

@@ -7,7 +7,7 @@ import (
) )
type UIRoutesRepo interface { type UIRoutesRepo interface {
GetRoutes(langId uint) ([]model.Route, error) GetRoutes(langId uint, roleId uint) ([]model.Route, error)
GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error) GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error)
} }
@@ -17,13 +17,18 @@ func New() UIRoutesRepo {
return &RoutesRepo{} return &RoutesRepo{}
} }
func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) { func (p *RoutesRepo) GetRoutes(langId uint, roleId uint) ([]model.Route, error) {
routes := []model.Route{} routes := []model.Route{}
err := db.DB.Find(&routes, model.Route{Active: nullable.GetNil(true)}).Error
if err != nil { err := db.
return nil, err Get().
} Model(model.Route{}).
return routes, nil Joins("JOIN b2b_route_roles rr ON rr.route_id = b2b_routes.id").
Where(model.Route{Active: nullable.GetNil(true)}).
Where("rr.role_id = ?", roleId).
Find(&routes).Error
return routes, err
} }
func (p *RoutesRepo) GetTopMenu(langId uint, roleId uint) ([]model.B2BTopMenu, error) { func (p *RoutesRepo) GetTopMenu(langId uint, roleId uint) ([]model.B2BTopMenu, error) {

View File

@@ -7,7 +7,11 @@ import (
"net/http" "net/http"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
) )
type SearchProxyResponse struct { type SearchProxyResponse struct {
@@ -17,6 +21,7 @@ type SearchProxyResponse struct {
type UISearchRepo interface { type UISearchRepo interface {
Search(index string, body []byte) (*SearchProxyResponse, error) Search(index string, body []byte) (*SearchProxyResponse, error)
GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error)
GetIndexSettings(index string) (*SearchProxyResponse, error) GetIndexSettings(index string) (*SearchProxyResponse, error)
GetRoutes(langId uint) ([]model.Route, error) GetRoutes(langId uint) ([]model.Route, error)
} }
@@ -80,3 +85,108 @@ func (r *SearchRepo) doRequest(method, url string, body []byte) (*SearchProxyRes
func (r *SearchRepo) GetRoutes(langId uint) ([]model.Route, error) { func (r *SearchRepo) GetRoutes(langId uint) ([]model.Route, error) {
return nil, nil return nil, nil
} }
// GetMeiliProductsProducts returns a batch of products with LIMIT/OFFSET pagination
// The scanning is done inside the repo to keep the service layer cleaner
func (r *SearchRepo) GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error) {
var products []model.MeiliSearchProduct
err := db.Get().
Table("ps_product_shop ps").
Select(`
ps.id_product AS id_product,
pl.name AS name,
TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description_short, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description,
p.ean13,
p.reference,
ps.price,
ps.id_category_default AS id_category,
cl.name AS cat_name,
cl.link_rewrite AS l_rew,
COALESCE(vary.attributes, JSON_OBJECT()) AS attr,
COALESCE(feat.features, JSON_OBJECT()) AS feat,
img.id_image,
cat.category_ids,
(SELECT COUNT(*) FROM ps_product_attribute_shop pas2 WHERE pas2.id_product = ps.id_product AND pas2.id_shop = ?) AS variations
`, constdata.SHOP_ID).
Joins("JOIN ps_product p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("LEFT JOIN variations vary ON vary.id_product = ps.id_product").
Joins("LEFT JOIN features feat ON feat.id_product = ps.id_product").
Joins("LEFT JOIN images img ON img.id_product = ps.id_product").
Joins("LEFT JOIN categories cat ON cat.id_product = ps.id_product").
Joins("JOIN products_page pp ON pp.id_product = ps.id_product").
Where("ps.active = ?", 1).
Order("ps.id_product").
Clauses(exclause.With{CTEs: []exclause.CTE{
{
Name: "products_page",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsProductShop{}).
Select("id_product, price").
Where("id_shop = ? AND active = 1", constdata.SHOP_ID).
Order("id_product").
Limit(limit).
Offset(offset),
},
},
{
Name: "variation_attributes",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_product_attribute_shop pas"). // <- explicit alias here
Select(`
pas.id_product,
pag.id_attribute_group AS attribute_name,
JSON_ARRAYAGG(DISTINCT pa.id_attribute) AS attribute_values
`).
Joins("JOIN ps_product_attribute_combination ppac ON ppac.id_product_attribute = pas.id_product_attribute").
Joins("JOIN ps_attribute pa ON pa.id_attribute = ppac.id_attribute").
Joins("JOIN ps_attribute_group pag ON pag.id_attribute_group = pa.id_attribute_group").
Where("pas.id_shop = ?", constdata.SHOP_ID).
Group("pas.id_product, pag.id_attribute_group"),
},
},
{
Name: "variations",
Subquery: exclause.Subquery{
DB: db.Get().
Table("variation_attributes").
Select("id_product, JSON_OBJECTAGG(attribute_name, attribute_values) AS attributes").
Group("id_product"),
},
},
{
Name: "features",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_feature_product pfp"). // <- explicit alias
Select("pfp.id_product, JSON_OBJECTAGG(pfp.id_feature, pfp.id_feature_value) AS features").
Group("pfp.id_product"),
},
},
{
Name: "images",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsImageShop{}).
Select("id_product, id_image").
Where("id_shop = ? AND cover = 1", constdata.SHOP_ID),
},
},
{
Name: "categories",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsCategoryProduct{}).
Select("id_product, JSON_ARRAYAGG(id_category) AS category_ids").
Group("id_product"),
},
},
}}).Find(&products).Error
return products, err
}

View File

@@ -0,0 +1,247 @@
package specificPriceRepo
import (
"context"
"errors"
"time"
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"gorm.io/gorm"
)
type UISpecificPriceRepo interface {
Create(ctx context.Context, pr *model.SpecificPrice) error
Update(ctx context.Context, pr *model.SpecificPrice) error
Delete(ctx context.Context, id uint64) error
GetByID(ctx context.Context, id uint64) (*model.SpecificPrice, error)
List(ctx context.Context) ([]*model.SpecificPrice, error)
SetActive(ctx context.Context, id uint64, active bool) error
}
type SpecificPriceRepo struct{}
func New() UISpecificPriceRepo {
return &SpecificPriceRepo{}
}
func (repo *SpecificPriceRepo) Create(ctx context.Context, pr *model.SpecificPrice) error {
return db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
now := time.Now()
pr.CreatedAt = &now
if err := tx.Create(pr).Error; err != nil {
return err
}
return repo.insertRelations(tx, pr)
})
}
func (repo *SpecificPriceRepo) Update(ctx context.Context, pr *model.SpecificPrice) error {
return db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
now := time.Now()
pr.UpdatedAt = &now
if err := tx.Save(pr).Error; err != nil {
return err
}
if err := repo.clearRelations(tx, pr.ID); err != nil {
return err
}
return repo.insertRelations(tx, pr)
})
}
func (repo *SpecificPriceRepo) GetByID(ctx context.Context, id uint64) (*model.SpecificPrice, error) {
var pr model.SpecificPrice
err := db.DB.WithContext(ctx).Where("id = ?", id).First(&pr).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
if err := repo.loadRelations(ctx, &pr); err != nil {
return nil, err
}
return &pr, nil
}
func (repo *SpecificPriceRepo) List(ctx context.Context) ([]*model.SpecificPrice, error) {
var specificPrices []*model.SpecificPrice
err := db.DB.WithContext(ctx).Find(&specificPrices).Error
if err != nil {
return nil, err
}
for i := range specificPrices {
if err := repo.loadRelations(ctx, specificPrices[i]); err != nil {
return nil, err
}
}
return specificPrices, nil
}
func (repo *SpecificPriceRepo) SetActive(ctx context.Context, id uint64, active bool) error {
return db.DB.WithContext(ctx).Model(&model.SpecificPrice{}).Where("id = ?", id).Update("is_active", active).Error
}
func (repo *SpecificPriceRepo) Delete(ctx context.Context, id uint64) error {
return db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Exec("DELETE FROM b2b_specific_price_product WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_category WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_product_attribute WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_country WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_customer WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Delete(&model.SpecificPrice{}, "id = ?", id).Error; err != nil {
return err
}
return nil
})
}
func (repo *SpecificPriceRepo) insertRelations(tx *gorm.DB, pr *model.SpecificPrice) error {
if len(pr.ProductIDs) > 0 {
for _, productID := range pr.ProductIDs {
if err := tx.Exec(`
INSERT INTO b2b_specific_price_product (b2b_specific_price_id, id_product) VALUES (?, ?)
`, pr.ID, productID).Error; err != nil {
return err
}
}
}
if len(pr.CategoryIDs) > 0 {
for _, categoryID := range pr.CategoryIDs {
if err := tx.Exec(`
INSERT INTO b2b_specific_price_category (b2b_specific_price_id, id_category) VALUES (?, ?)
`, pr.ID, categoryID).Error; err != nil {
return err
}
}
}
if len(pr.ProductAttributeIDs) > 0 {
for _, attrID := range pr.ProductAttributeIDs {
if err := tx.Exec(`
INSERT INTO b2b_specific_price_product_attribute (b2b_specific_price_id, id_product_attribute) VALUES (?, ?)
`, pr.ID, attrID).Error; err != nil {
return err
}
}
}
if len(pr.CountryIDs) > 0 {
for _, countryID := range pr.CountryIDs {
if err := tx.Exec(`
INSERT INTO b2b_specific_price_country (b2b_specific_price_id, b2b_id_country) VALUES (?, ?)
`, pr.ID, countryID).Error; err != nil {
return err
}
}
}
if len(pr.CustomerIDs) > 0 {
for _, customerID := range pr.CustomerIDs {
if err := tx.Exec(`
INSERT INTO b2b_specific_price_customer (b2b_specific_price_id, b2b_id_customer) VALUES (?, ?)
`, pr.ID, customerID).Error; err != nil {
return err
}
}
}
return nil
}
func (repo *SpecificPriceRepo) clearRelations(tx *gorm.DB, id uint64) error {
if err := tx.Exec("DELETE FROM b2b_specific_price_product WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_category WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_product_attribute WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_country WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_customer WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
return nil
}
func (repo *SpecificPriceRepo) loadRelations(ctx context.Context, pr *model.SpecificPrice) error {
var err error
var productIDs []struct {
IDProduct uint64 `gorm:"column:id_product"`
}
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_product").Where("b2b_specific_price_id = ?", pr.ID).Select("id_product").Scan(&productIDs).Error; err != nil {
return err
}
for _, p := range productIDs {
pr.ProductIDs = append(pr.ProductIDs, p.IDProduct)
}
var categoryIDs []struct {
IDCategory uint64 `gorm:"column:id_category"`
}
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_category").Where("b2b_specific_price_id = ?", pr.ID).Select("id_category").Scan(&categoryIDs).Error; err != nil {
return err
}
for _, c := range categoryIDs {
pr.CategoryIDs = append(pr.CategoryIDs, c.IDCategory)
}
var attrIDs []struct {
IDAttr uint64 `gorm:"column:id_product_attribute"`
}
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_product_attribute").Where("b2b_specific_price_id = ?", pr.ID).Select("id_product_attribute").Scan(&attrIDs).Error; err != nil {
return err
}
for _, a := range attrIDs {
pr.ProductAttributeIDs = append(pr.ProductAttributeIDs, a.IDAttr)
}
var countryIDs []struct {
IDCountry uint64 `gorm:"column:b2b_id_country"`
}
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_country").Where("b2b_specific_price_id = ?", pr.ID).Select("b2b_id_country").Scan(&countryIDs).Error; err != nil {
return err
}
for _, c := range countryIDs {
pr.CountryIDs = append(pr.CountryIDs, c.IDCountry)
}
var customerIDs []struct {
IDCustomer uint64 `gorm:"column:b2b_id_customer"`
}
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_customer").Where("b2b_specific_price_id = ?", pr.ID).Select("b2b_id_customer").Scan(&customerIDs).Error; err != nil {
return err
}
for _, c := range customerIDs {
pr.CustomerIDs = append(pr.CustomerIDs, c.IDCustomer)
}
return nil
}

View File

@@ -21,7 +21,7 @@ func New() *AddressesService {
} }
} }
func (s *AddressesService) GetTemplate(country_id uint) (model.AddressField, error) { func (s *AddressesService) GetTemplate(country_id uint) (model.AddressUnparsed, error) {
switch country_id { switch country_id {
case 1: // Poland case 1: // Poland
@@ -49,7 +49,7 @@ func (s *AddressesService) AddNewAddress(user_id uint, address_info string, coun
return responseErrors.ErrMaxAmtOfAddressesReached return responseErrors.ErrMaxAmtOfAddressesReached
} }
_, err = s.validateAddressJson(address_info, country_id) _, err = s.ValidateAddressJson(address_info, country_id)
if err != nil { if err != nil {
return err return err
} }
@@ -66,7 +66,7 @@ func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_
return responseErrors.ErrUserHasNoSuchAddress return responseErrors.ErrUserHasNoSuchAddress
} }
_, err = s.validateAddressJson(address_info, country_id) _, err = s.ValidateAddressJson(address_info, country_id)
if err != nil { if err != nil {
return err return err
} }
@@ -74,30 +74,23 @@ func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_
return s.repo.UpdateAddress(user_id, address_id, address_info, country_id) return s.repo.UpdateAddress(user_id, address_id, address_info, country_id)
} }
func (s *AddressesService) RetrieveAddressesInfo(user_id uint) (*[]model.AddressUnparsed, error) { func (s *AddressesService) RetrieveAddresses(user_id uint) (*[]model.Address, error) {
parsed_addresses, err := s.repo.RetrieveAddresses(user_id) addresses, err := s.repo.RetrieveAddresses(user_id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var unparsed_addresses []model.AddressUnparsed for i := 0; i < len(*addresses); i++ {
address_unparsed, err := s.ValidateAddressJson((*addresses)[i].AddressString, (*addresses)[i].CountryID)
for i := 0; i < len(*parsed_addresses); i++ {
var next_address model.AddressUnparsed
next_address.ID = (*parsed_addresses)[i].ID
next_address.CustomerID = (*parsed_addresses)[i].CustomerID
next_address.CountryID = (*parsed_addresses)[i].CountryID
next_address.AddressInfo, err = s.validateAddressJson((*parsed_addresses)[i].AddressInfo, next_address.CountryID)
// log such errors // log such errors
if err != nil { if err != nil {
fmt.Printf("err: %v\n", err) fmt.Printf("err: %v\n", err)
} }
unparsed_addresses = append(unparsed_addresses, next_address) (*addresses)[i].AddressUnparsed = &address_unparsed
} }
return &unparsed_addresses, nil return addresses, nil
} }
func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error { func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error {
@@ -112,7 +105,7 @@ func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error {
} }
// validateAddressJson makes sure that the info string represents a valid json of address in given country // validateAddressJson makes sure that the info string represents a valid json of address in given country
func (s *AddressesService) validateAddressJson(info string, country_id uint) (model.AddressField, error) { func (s *AddressesService) ValidateAddressJson(info string, country_id uint) (model.AddressUnparsed, error) {
dec := json.NewDecoder(strings.NewReader(info)) dec := json.NewDecoder(strings.NewReader(info))
dec.DisallowUnknownFields() dec.DisallowUnknownFields()

View File

@@ -21,6 +21,7 @@ import (
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
// JWTClaims represents the JWT claims // JWTClaims represents the JWT claims
@@ -436,7 +437,7 @@ func (s *AuthService) RevokeAllRefreshTokens(userID uint) {
// GetUserByID retrieves a user by ID // GetUserByID retrieves a user by ID
func (s *AuthService) GetUserByID(userID uint) (*model.Customer, error) { func (s *AuthService) GetUserByID(userID uint) (*model.Customer, error) {
var user model.Customer var user model.Customer
if err := s.db.Preload("Role.Permissions").First(&user, userID).Error; err != nil { if err := s.db.Preload("Role.Permissions").Preload(clause.Associations).First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, responseErrors.ErrUserNotFound return nil, responseErrors.ErrUserNotFound
} }

View File

@@ -34,12 +34,24 @@ func (s *CartsService) CreateNewCart(user_id uint) (model.CustomerCart, error) {
return cart, nil return cart, nil
} }
func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error { func (s *CartsService) RemoveCart(user_id uint, cart_id uint) error {
amt, err := s.repo.UserHasCart(user_id, cart_id) exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil { if err != nil {
return err return err
} }
if amt != 1 { if !exists {
return responseErrors.ErrUserHasNoSuchCart
}
return s.repo.RemoveCart(user_id, cart_id)
}
func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error {
exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchCart return responseErrors.ErrUserHasNoSuchCart
} }
@@ -51,33 +63,45 @@ func (s *CartsService) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, er
} }
func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) { func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) {
amt, err := s.repo.UserHasCart(user_id, cart_id) exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if amt != 1 { if !exists {
return nil, responseErrors.ErrUserHasNoSuchCart return nil, responseErrors.ErrUserHasNoSuchCart
} }
return s.repo.RetrieveCart(user_id, cart_id) return s.repo.RetrieveCart(user_id, cart_id)
} }
func (s *CartsService) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error { func (s *CartsService) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount int, set_amount bool) error {
amt, err := s.repo.UserHasCart(user_id, cart_id) exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil { if err != nil {
return err return err
} }
if amt != 1 { if !exists {
return responseErrors.ErrUserHasNoSuchCart return responseErrors.ErrUserHasNoSuchCart
} }
amt, err = s.repo.CheckProductExists(product_id, product_attribute_id) exists, err = s.repo.CheckProductExists(product_id, product_attribute_id)
if err != nil { if err != nil {
return err return err
} }
if amt != 1 { if !exists {
return responseErrors.ErrProductOrItsVariationDoesNotExist return responseErrors.ErrProductOrItsVariationDoesNotExist
} }
return s.repo.AddProduct(user_id, cart_id, product_id, product_attribute_id, amount) return s.repo.AddProduct(cart_id, product_id, product_attribute_id, uint(amount), set_amount)
}
func (s *CartsService) RemoveProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint) error {
exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchCart
}
return s.repo.RemoveProduct(cart_id, product_id, product_attribute_id)
} }

View File

@@ -117,6 +117,18 @@ func (s *EmailService) SendNewUserAdminNotification(userEmail, userName, baseURL
return s.SendEmail(s.config.AdminEmail, subject, body) return s.SendEmail(s.config.AdminEmail, subject, body)
} }
// SendNewOrderPlacedNotification sends an email to admin when new order is placed
func (s *EmailService) SendNewOrderPlacedNotification(userID uint) error {
if s.config.AdminEmail == "" {
return nil // No admin email configured
}
subject := "New Order Created"
body := s.newOrderPlacedTemplate(userID)
return s.SendEmail(s.config.AdminEmail, subject, body)
}
// verificationEmailTemplate returns the HTML template for email verification // verificationEmailTemplate returns the HTML template for email verification
func (s *EmailService) verificationEmailTemplate(name, verificationURL string, langID uint) string { func (s *EmailService) verificationEmailTemplate(name, verificationURL string, langID uint) string {
buf := bytes.Buffer{} buf := bytes.Buffer{}
@@ -137,3 +149,10 @@ func (s *EmailService) newUserAdminNotificationTemplate(userEmail, userName, bas
emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf) emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf)
return buf.String() return buf.String()
} }
// newUserAdminNotificationTemplate returns the HTML template for admin notification
func (s *EmailService) newOrderPlacedTemplate(userID uint) string {
buf := bytes.Buffer{}
// emails.EmailNewOrderPlacedWrapper(view.EmailLayout[view.EmailNewOrderPlacedData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailNewOrderPlacedData{UserID: userID}}).Render(context.Background(), &buf)
return buf.String()
}

View File

@@ -6,7 +6,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo" searchrepo "git.ma-al.com/goc_daniel/b2b/app/repos/searchRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/meilisearch/meilisearch-go" "github.com/meilisearch/meilisearch-go"
) )
@@ -20,7 +20,7 @@ type MeiliIndexSettings struct {
} }
type MeiliService struct { type MeiliService struct {
productDescriptionRepo productDescriptionRepo.UIProductDescriptionRepo searchRepo searchrepo.UISearchRepo
meiliClient meilisearch.ServiceManager meiliClient meilisearch.ServiceManager
} }
@@ -33,7 +33,7 @@ func New() *MeiliService {
return &MeiliService{ return &MeiliService{
meiliClient: client, meiliClient: client,
productDescriptionRepo: productDescriptionRepo.New(), searchRepo: searchrepo.New(),
} }
} }
@@ -50,7 +50,7 @@ func (s *MeiliService) CreateIndex(id_lang uint) error {
for { for {
// Get batch of products from repo (includes scanning) // Get batch of products from repo (includes scanning)
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang, offset, batchSize) products, err := s.searchRepo.GetMeiliProducts(id_lang, offset, batchSize)
if err != nil { if err != nil {
return fmt.Errorf("failed to get products batch at offset %d: %w", offset, err) return fmt.Errorf("failed to get products batch at offset %d: %w", offset, err)
} }

View File

@@ -3,31 +3,45 @@ package menuService
import ( import (
"slices" "slices"
"sort" "sort"
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo" categoryrepo "git.ma-al.com/goc_daniel/b2b/app/repos/categoryRepo"
routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo" routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
) )
type MenuService struct { type MenuService struct {
categoriesRepo categoriesRepo.UICategoriesRepo categoryRepo categoryrepo.UICategoryRepo
routesRepo routesRepo.UIRoutesRepo routesRepo routesRepo.UIRoutesRepo
} }
func New() *MenuService { func New() *MenuService {
return &MenuService{ return &MenuService{
categoriesRepo: categoriesRepo.New(), categoryRepo: categoryrepo.New(),
routesRepo: routesRepo.New(), routesRepo: routesRepo.New(),
} }
} }
func (s *MenuService) GetCategoryTree(root_category_id uint, id_lang uint) (*model.Category, error) { func (s *MenuService) GetCategoryTree(root_category_id uint, id_lang uint) (*model.Category, error) {
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang) all_categories, err := s.categoryRepo.RetrieveMenuCategories(id_lang)
if err != nil { if err != nil {
return &model.Category{}, err return &model.Category{}, err
} }
// remove blacklisted categories
// to do so, we detach them from the main tree
for i := 0; i < len(all_categories); i++ {
if slices.Contains(constdata.CATEGORY_BLACKLIST, all_categories[i].CategoryID) {
all_categories[i].ParentID = all_categories[i].CategoryID
}
}
iso_code := all_categories[0].IsoCode
s.appendAdditional(&all_categories, id_lang, iso_code)
// find the root // find the root
root_index := 0 root_index := 0
root_found := false root_found := false
@@ -88,8 +102,8 @@ func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCate
return node, true return node, true
} }
func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) { func (s *MenuService) GetRoutes(id_lang, roleId uint) ([]model.Route, error) {
return s.routesRepo.GetRoutes(id_lang) return s.routesRepo.GetRoutes(id_lang, roleId)
} }
func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category { func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category {
@@ -98,7 +112,7 @@ func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) mod
normal.CategoryID = scanned.CategoryID normal.CategoryID = scanned.CategoryID
normal.Label = scanned.Name normal.Label = scanned.Name
// normal.Active = scanned.Active == 1 // normal.Active = scanned.Active == 1
normal.Params = model.CategoryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode} normal.Params = model.CategoryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode, Filter: scanned.Filter}
normal.Children = []model.Category{} normal.Children = []model.Category{}
return normal return normal
} }
@@ -114,11 +128,14 @@ func (a ByPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position } func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position }
func (s *MenuService) GetBreadcrumb(root_category_id uint, start_category_id uint, id_lang uint) ([]model.CategoryInBreadcrumb, error) { func (s *MenuService) GetBreadcrumb(root_category_id uint, start_category_id uint, id_lang uint) ([]model.CategoryInBreadcrumb, error) {
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang) all_categories, err := s.categoryRepo.RetrieveMenuCategories(id_lang)
if err != nil { if err != nil {
return []model.CategoryInBreadcrumb{}, err return []model.CategoryInBreadcrumb{}, err
} }
iso_code := all_categories[0].IsoCode
s.appendAdditional(&all_categories, id_lang, iso_code)
breadcrumb := []model.CategoryInBreadcrumb{} breadcrumb := []model.CategoryInBreadcrumb{}
start_index := 0 start_index := 0
@@ -211,3 +228,24 @@ func (s *MenuService) GetTopMenu(languageId uint, roleId uint) ([]*model.B2BTopM
return roots, nil return roots, nil
} }
func (s *MenuService) appendAdditional(all_categories *[]model.ScannedCategory, id_lang uint, iso_code string) {
for i := 0; i < len(*all_categories); i++ {
(*all_categories)[i].Filter = "category_id_in=" + strconv.Itoa(int((*all_categories)[i].CategoryID))
}
var additional model.ScannedCategory
additional.CategoryID = 10001
additional.Name = "New Products"
additional.Active = 1
additional.Position = 10
additional.ParentID = 2
additional.IsRoot = 0
additional.LinkRewrite = i18n.T___(id_lang, "category.new_products")
additional.IsoCode = iso_code
additional.Visited = false
additional.Filter = "is_new_in=true"
*all_categories = append(*all_categories, additional)
}

View File

@@ -0,0 +1,145 @@
package orderService
import (
"fmt"
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo"
"git.ma-al.com/goc_daniel/b2b/app/repos/ordersRepo"
"git.ma-al.com/goc_daniel/b2b/app/service/addressesService"
"git.ma-al.com/goc_daniel/b2b/app/service/emailService"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
)
type OrderService struct {
ordersRepo ordersRepo.UIOrdersRepo
cartsRepo cartsRepo.UICartsRepo
addressesService *addressesService.AddressesService
emailService *emailService.EmailService
}
func New() *OrderService {
return &OrderService{
ordersRepo: ordersRepo.New(),
cartsRepo: cartsRepo.New(),
addressesService: addressesService.New(),
emailService: emailService.NewEmailService(),
}
}
func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) {
if !user.HasPermission(perms.OrdersViewAll) {
// append filter to view only this user's orders
idStr := strconv.FormatUint(uint64(user.ID), 10)
filt.Append(filters.Where("b2b_customer_orders.user_id = " + idStr))
}
list, err := s.ordersRepo.Find(user.ID, p, filt)
if err != nil {
return nil, err
}
for i := 0; i < len(list.Items); i++ {
address_unparsed, err := s.addressesService.ValidateAddressJson(list.Items[i].AddressString, list.Items[i].CountryID)
// log such errors
if err != nil {
fmt.Printf("err: %v\n", err)
}
list.Items[i].AddressUnparsed = &address_unparsed
}
return list, nil
}
func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, country_id uint, address_info string) error {
_, err := s.addressesService.ValidateAddressJson(address_info, country_id)
if err != nil {
return err
}
exists, err := s.cartsRepo.UserHasCart(user_id, cart_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchCart
}
cart, err := s.cartsRepo.RetrieveCart(user_id, cart_id)
if err != nil {
return err
}
if len(cart.Products) == 0 {
return responseErrors.ErrEmptyCart
}
if name == "" && cart.Name != nil {
name = *cart.Name
}
// all checks passed
err = s.ordersRepo.PlaceNewOrder(cart, name, country_id, address_info)
if err != nil {
return err
}
// from this point onward we do not cancel this order.
// if no error is returned, remove the cart. This should be smooth
err = s.cartsRepo.RemoveCart(user_id, cart_id)
if err != nil {
// Log error but don't fail placing order
_ = err
}
// send email to admin
go func(user_id uint) {
err := s.emailService.SendNewOrderPlacedNotification(user_id)
if err != nil {
// Log error but don't fail placing order
_ = err
}
}(user_id)
return nil
}
func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, country_id uint, address_info string) error {
_, err := s.addressesService.ValidateAddressJson(address_info, country_id)
if err != nil {
return err
}
if !user.HasPermission(perms.OrdersModifyAll) {
exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchOrder
}
}
return s.ordersRepo.ChangeOrderAddress(order_id, country_id, address_info)
}
// This is obiously just an initial version of this function
func (s *OrderService) ChangeOrderStatus(user *model.Customer, order_id uint, status string) error {
if !user.HasPermission(perms.OrdersModifyAll) {
exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchOrder
}
}
return s.ordersRepo.ChangeOrderStatus(order_id, status)
}

View File

@@ -2,12 +2,15 @@ package productService
import ( import (
"encoding/json" "encoding/json"
"errors"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo" "git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"git.ma-al.com/goc_daniel/b2b/app/view"
) )
type ProductService struct { type ProductService struct {
@@ -20,15 +23,146 @@ func New() *ProductService {
} }
} }
func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) { func (s *ProductService) Get(
products, err := s.productsRepo.GetJSON(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer, b2b_id_country, p_quantity) p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity uint,
) (*json.RawMessage, error) {
product, err := s.productsRepo.GetBase(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer)
if err != nil { if err != nil {
return products, err return nil, err
} }
return products, nil price, err := s.productsRepo.GetPrice(p_id_product, nil, constdata.SHOP_ID, p_id_customer, b2b_id_country, p_quantity)
if err != nil {
return nil, err
} }
func (s *ProductService) Find(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) { variants, err := s.productsRepo.GetVariants(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer, b2b_id_country, p_quantity)
return s.productsRepo.Find(id_lang, p, filters) if err != nil {
return nil, err
}
result := view.ProductFull{
Product: product,
Price: price,
Variants: variants,
}
if len(variants) > 0 {
result.Variants = variants
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return nil, err
}
raw := json.RawMessage(jsonBytes)
return &raw, nil
}
func (s *ProductService) Find(
idLang uint,
userID uint,
p find.Paging,
filters *filters.FiltersList,
customer *model.Customer,
quantity uint,
shopID uint,
) (*find.Found[model.ProductInList], error) {
if customer == nil || customer.Country == nil {
return nil, errors.New("customer is nil or missing fields")
}
found, err := s.productsRepo.Find(idLang, userID, p, filters)
if err != nil {
return nil, err
}
// 1. collect simple products (no variants)
simpleProductIndexes := make([]int, 0, len(found.Items))
for i := range found.Items {
if found.Items[i].VariantsNumber <= 0 {
simpleProductIndexes = append(simpleProductIndexes, i)
}
}
// 2. resolve prices ONLY for simple products
for _, i := range simpleProductIndexes {
price, err := s.productsRepo.GetPrice(
found.Items[i].ProductID,
nil,
shopID,
customer.ID,
customer.CountryID,
quantity,
)
if err != nil {
return nil, err
}
found.Items[i].PriceTaxExcl = price.FinalTaxExcl
found.Items[i].PriceTaxIncl = price.FinalTaxIncl
}
return found, nil
}
func (s *ProductService) GetProductAttributes(
langID uint,
productID uint,
shopID uint,
customerID uint,
countryID uint,
quantity uint,
) ([]view.ProductAttribute, error) {
variants, err := s.productsRepo.GetVariants(productID, constdata.SHOP_ID, langID, customerID, countryID, quantity)
if err != nil {
return nil, err
}
return variants, nil
}
func (s *ProductService) AddToFavorites(userID uint, productID uint) error {
exists, err := s.productsRepo.ProductInDatabase(productID)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrProductNotFound
}
exists, err = s.productsRepo.ExistsInFavorites(userID, productID)
if err != nil {
return err
}
if exists {
return responseErrors.ErrAlreadyInFavorites
}
return s.productsRepo.AddToFavorites(userID, productID)
}
func (s *ProductService) RemoveFromFavorites(userID uint, productID uint) error {
exists, err := s.productsRepo.ProductInDatabase(productID)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrProductNotFound
}
exists, err = s.productsRepo.ExistsInFavorites(userID, productID)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrNotInFavorites
}
return s.productsRepo.RemoveFromFavorites(userID, productID)
} }

View File

@@ -8,6 +8,7 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"regexp"
"slices" "slices"
"strings" "strings"
"time" "time"
@@ -110,8 +111,9 @@ func (s *ProductTranslationService) SaveProductDescription(userID uint, productI
// check that fields description, description_short and usage, if they exist, have a valid html format // check that fields description, description_short and usage, if they exist, have a valid html format
mustBeHTML := []string{"description", "description_short", "usage"} mustBeHTML := []string{"description", "description_short", "usage"}
for i := 0; i < len(mustBeHTML); i++ { for i := 0; i < len(mustBeHTML); i++ {
if text, exists := updates[mustBeHTML[i]]; exists { if _, exists := updates[mustBeHTML[i]]; exists {
if !isValidXHTML(text) { updates[mustBeHTML[i]] = parseAutoCloseTags(updates[mustBeHTML[i]])
if !isValidXHTML(updates[mustBeHTML[i]]) {
return responseErrors.ErrInvalidXHTML return responseErrors.ErrInvalidXHTML
} }
} }
@@ -264,9 +266,17 @@ func cleanForPrompt(s string) string {
} }
} }
if slices.Contains(xml.HTMLAutoClose, v.Name.Local) {
prompt += "/>"
} else {
prompt += ">" prompt += ">"
}
case xml.EndElement: case xml.EndElement:
if !slices.Contains(xml.HTMLAutoClose, v.Name.Local) {
prompt += "</" + attrName(v.Name) + ">" prompt += "</" + attrName(v.Name) + ">"
}
case xml.CharData: case xml.CharData:
prompt += string(v) prompt += string(v)
case xml.Comment: case xml.Comment:
@@ -307,6 +317,43 @@ func getStringInBetween(str string, start string, end string) (success bool, res
return true, str[s : s+e] return true, str[s : s+e]
} }
// this converts input into HTML4 format.
// this really is ad-hoc solution, but it works.
func parseAutoCloseTags(s string) string {
alts := ""
for i, name := range xml.HTMLAutoClose {
if i > 0 {
alts += "|"
}
alts += name
}
// remove closing </img> tags
reClose := regexp.MustCompile(`(?i)<\s*\/\s*(?:` + alts + `)\s*>`)
s = reClose.ReplaceAllString(s, "")
// convert <img ...> → <img ... />
// matches <img ...> that do NOT already end with />
reOpen := regexp.MustCompile(`(?i)<\s*(` + alts + `)\b([^>]*?)>`)
s = reOpen.ReplaceAllStringFunc(s, func(tag string) string {
trimmed := strings.TrimSpace(tag)
// Already self-closed: <img ... />, <br/>
if strings.HasSuffix(trimmed, "/>") {
return tag
}
// Replace final > with />
i := strings.LastIndex(tag, ">")
if i < 0 {
return tag
}
return tag[:i] + " />"
})
return s
}
// isValidXHTML checks if the string obeys the XHTML format // isValidXHTML checks if the string obeys the XHTML format
func isValidXHTML(s string) bool { func isValidXHTML(s string) bool {
r := strings.NewReader(s) r := strings.NewReader(s)
@@ -382,7 +429,12 @@ func rebuildFromResponse(s_original string, s_response string) (bool, string) {
result += fmt.Sprintf(` %s="%s"`, attrName(attr.Name), attr.Value) result += fmt.Sprintf(` %s="%s"`, attrName(attr.Name), attr.Value)
} }
} }
if slices.Contains(xml.HTMLAutoClose, v_original.Name.Local) {
result += "/>"
} else {
result += ">" result += ">"
}
case xml.CharData: case xml.CharData:
result += string(v_response) result += string(v_response)
@@ -400,7 +452,7 @@ func rebuildFromResponse(s_original string, s_response string) (bool, string) {
return false, "" return false, ""
} }
if v_original.Name.Local != "img" { if !slices.Contains(xml.HTMLAutoClose, v_original.Name.Local) {
result += "</" + attrName(v_original.Name) + ">" result += "</" + attrName(v_original.Name) + ">"
} }

View File

@@ -4,6 +4,7 @@ import (
"strings" "strings"
"unicode" "unicode"
"git.ma-al.com/goc_daniel/b2b/app/db"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"golang.org/x/text/runes" "golang.org/x/text/runes"
@@ -22,7 +23,7 @@ func SanitizeSlug(s string) string {
s = strings.TrimSpace(strings.ToLower(s)) s = strings.TrimSpace(strings.ToLower(s))
// First apply explicit transliteration for language-specific letters. // First apply explicit transliteration for language-specific letters.
s = transliterateWithTable(s) s = transliterateSlug(s)
// Then normalize and strip any remaining combining marks. // Then normalize and strip any remaining combining marks.
s = removeDiacritics(s) s = removeDiacritics(s)
@@ -40,19 +41,17 @@ func SanitizeSlug(s string) string {
return s return s
} }
func transliterateWithTable(s string) string { func transliterateSlug(s string) string {
var b strings.Builder var cleared string
b.Grow(len(s))
for _, r := range s { err := db.DB.Raw("SELECT slugify_eu(?)", s).Scan(&cleared).Error
if repl, ok := constdata.TRANSLITERATION_TABLE[r]; ok { if err != nil {
b.WriteString(repl) // log error
} else { _ = err
b.WriteRune(r) return s
}
} }
return b.String() return cleared
} }
func removeDiacritics(s string) string { func removeDiacritics(s string) string {

View File

@@ -0,0 +1,124 @@
package specificPriceService
import (
"context"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/specificPriceRepo"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
)
type SpecificPriceService struct {
specificPriceRepo specificPriceRepo.UISpecificPriceRepo
}
func New() *SpecificPriceService {
return &SpecificPriceService{
specificPriceRepo: specificPriceRepo.New(),
}
}
func (s *SpecificPriceService) Create(ctx context.Context, pr *model.SpecificPrice) (*model.SpecificPrice, error) {
if err := s.validateRequest(pr); err != nil {
return nil, err
}
if err := s.specificPriceRepo.Create(ctx, pr); err != nil {
return nil, err
}
return pr, nil
}
func (s *SpecificPriceService) Update(ctx context.Context, id uint64, pr *model.SpecificPrice) (*model.SpecificPrice, error) {
existing, err := s.specificPriceRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if existing == nil {
return nil, responseErrors.ErrSpecificPriceNotFound
}
if err := s.validateUpdateRequest(pr); err != nil {
return nil, err
}
pr.ID = id
if err := s.specificPriceRepo.Update(ctx, pr); err != nil {
return nil, err
}
return pr, nil
}
func (s *SpecificPriceService) GetByID(ctx context.Context, id uint64) (*model.SpecificPrice, error) {
pr, err := s.specificPriceRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if pr == nil {
return nil, responseErrors.ErrSpecificPriceNotFound
}
return pr, nil
}
func (s *SpecificPriceService) List(ctx context.Context) ([]*model.SpecificPrice, error) {
return s.specificPriceRepo.List(ctx)
}
func (s *SpecificPriceService) SetActive(ctx context.Context, id uint64, active bool) error {
pr, err := s.specificPriceRepo.GetByID(ctx, id)
if err != nil {
return err
}
if pr == nil {
return responseErrors.ErrSpecificPriceNotFound
}
return s.specificPriceRepo.SetActive(ctx, id, active)
}
func (s *SpecificPriceService) Delete(ctx context.Context, id uint64) error {
pr, err := s.specificPriceRepo.GetByID(ctx, id)
if err != nil {
return err
}
if pr == nil {
return responseErrors.ErrSpecificPriceNotFound
}
return s.specificPriceRepo.Delete(ctx, id)
}
func (s *SpecificPriceService) validateRequest(pr *model.SpecificPrice) error {
if pr.ReductionType != "amount" && pr.ReductionType != "percentage" {
return responseErrors.ErrInvalidReductionType
}
if pr.ReductionType == "percentage" && pr.PercentageReduction == nil {
return responseErrors.ErrPercentageRequired
}
if pr.ReductionType == "amount" && pr.Price == nil {
return responseErrors.ErrPriceRequired
}
return nil
}
func (s *SpecificPriceService) validateUpdateRequest(pr *model.SpecificPrice) error {
if pr.ReductionType != "" && pr.ReductionType != "amount" && pr.ReductionType != "percentage" {
return responseErrors.ErrInvalidReductionType
}
if pr.ReductionType == "percentage" && pr.PercentageReduction == nil {
return responseErrors.ErrPercentageRequired
}
if pr.ReductionType == "amount" && pr.Price == nil {
return responseErrors.ErrPriceRequired
}
return nil
}

View File

@@ -0,0 +1,26 @@
package emails
import (
"git.ma-al.com/goc_daniel/b2b/app/templ/layout"
"git.ma-al.com/goc_daniel/b2b/app/view"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
)
templ EmailNewOrderPlacedWrapper(data view.EmailLayout[view.EmailNewOrderPlacedData]) {
@layout.Base( i18n.T___(data.LangID, "email.email_new_order_placed_notification_title")) {
<div class="container">
<div class="email-wrapper">
<div class="email-header">
<h1>New Order Placed</h1>
</div>
<div class="email-body">
<p>Hello Administrator,</p>
<p>User with id { data.Data.UserID } has placed a new order. </p>
</div>
<div class="email-footer">
<p>&copy; 2024 Gitea Manager. All rights reserved.</p>
</div>
</div>
</div>
}
}

View File

@@ -3,19 +3,27 @@ package constdata
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads). // PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$` const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
const SHOP_ID = 1 const SHOP_ID = 1
const DEFAULT_PRODUCT_QUANTITY = 1
const SHOP_DEFAULT_LANGUAGE = 1 const SHOP_DEFAULT_LANGUAGE = 1
const ADMIN_NOTIFICATION_LANGUAGE = 2 const ADMIN_NOTIFICATION_LANGUAGE = 2
// CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1 // CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1
const CATEGORY_TREE_ROOT_ID = 2 const CATEGORY_TREE_ROOT_ID = 2
// since arrays can not be const
var CATEGORY_BLACKLIST = []uint{250}
const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const MAX_AMOUNT_OF_CARTS_PER_USER = 10
const DEFAULT_NEW_CART_NAME = "new cart" const DEFAULT_NEW_CART_NAME = "new cart"
const MAX_AMOUNT_OF_PRODUCT_IN_CART = 1024
const MAX_AMOUNT_OF_ADDRESSES_PER_USER = 10 const MAX_AMOUNT_OF_ADDRESSES_PER_USER = 10
const USER_LOCALE = "user" const USER_LOCALE = "user"
// ORDERS
const NEW_ORDER_STATUS = "PENDING"
// WEBDAV // WEBDAV
const NBYTES_IN_WEBDAV_TOKEN = 32 const NBYTES_IN_WEBDAV_TOKEN = 32
const WEBDAV_HREF_ROOT = "http://localhost:3000/api/v1/webdav/storage" const WEBDAV_HREF_ROOT = "http://localhost:3000/api/v1/webdav/storage"
@@ -26,22 +34,4 @@ const NON_ALNUM_REGEX = `[^a-z0-9]+`
const MULTI_DASH_REGEX = `-+` const MULTI_DASH_REGEX = `-+`
const SLUG_REGEX = `^[a-z0-9]+(?:-[a-z0-9]+)*$` const SLUG_REGEX = `^[a-z0-9]+(?:-[a-z0-9]+)*$`
// Currently supports only German+Polish specific cases const UNLOGGED_USER_ROLE_ID = 4
var TRANSLITERATION_TABLE = map[rune]string{
// German
'ä': "ae",
'ö': "oe",
'ü': "ue",
'ß': "ss",
// Polish
'ą': "a",
'ć': "c",
'ę': "e",
'ł': "l",
'ń': "n",
'ó': "o",
'ś': "s",
'ż': "z",
'ź': "z",
}

View File

@@ -22,14 +22,6 @@ func GetUserID(c fiber.Ctx) (uint, bool) {
return user_locale.User.ID, true return user_locale.User.ID, true
} }
func GetOriginalUserRole(c fiber.Ctx) (model.Role, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.OriginalUser == nil || user_locale.OriginalUser.Role == nil {
return model.Role{}, false
}
return *user_locale.OriginalUser.Role, true
}
func GetCustomer(c fiber.Ctx) (*model.Customer, bool) { func GetCustomer(c fiber.Ctx) (*model.Customer, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.User == nil { if !ok || user_locale.User == nil {

View File

@@ -49,8 +49,11 @@ var (
ErrAIResponseFail = errors.New("AI responded with failure") ErrAIResponseFail = errors.New("AI responded with failure")
ErrAIBadOutput = errors.New("AI response does not obey the format") ErrAIBadOutput = errors.New("AI response does not obey the format")
// Typed errors for product list handler // Typed errors for product handler
ErrBadPaging = errors.New("bad or missing paging attribute value in header") ErrBadPaging = errors.New("bad or missing paging attribute value in header")
ErrProductNotFound = errors.New("product with provided id does not exist")
ErrAlreadyInFavorites = errors.New("the product already is in your favorites")
ErrNotInFavorites = errors.New("the product already is not in your favorites")
// Typed errors for menu handler // Typed errors for menu handler
ErrNoRootFound = errors.New("no root found in categories table") ErrNoRootFound = errors.New("no root found in categories table")
@@ -62,6 +65,18 @@ var (
ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached")
ErrUserHasNoSuchCart = errors.New("user does not have cart with given id") ErrUserHasNoSuchCart = errors.New("user does not have cart with given id")
ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist")
ErrAmountMustBePositive = errors.New("amount must be positive")
ErrAmountMustBeReasonable = errors.New("amount must be reasonable")
// Typed errors for orders handler
ErrEmptyCart = errors.New("the cart is empty")
ErrUserHasNoSuchOrder = errors.New("user does not have order with given id")
// Typed errors for price reduction handler
ErrInvalidReductionType = errors.New("invalid reduction type: must be 'amount' or 'percentage'")
ErrPercentageRequired = errors.New("percentage_reduction required when reduction_type is percentage")
ErrPriceRequired = errors.New("price required when reduction_type is amount")
ErrSpecificPriceNotFound = errors.New("price reduction not found")
// Typed errors for storage // Typed errors for storage
ErrAccessDenied = errors.New("access denied!") ErrAccessDenied = errors.New("access denied!")
@@ -170,6 +185,12 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrBadPaging): case errors.Is(err, ErrBadPaging):
return i18n.T_(c, "error.err_bad_paging") return i18n.T_(c, "error.err_bad_paging")
case errors.Is(err, ErrProductNotFound):
return i18n.T_(c, "error.err_product_not_found")
case errors.Is(err, ErrAlreadyInFavorites):
return i18n.T_(c, "error.err_already_in_favorites")
case errors.Is(err, ErrNotInFavorites):
return i18n.T_(c, "error.err_already_not_in_favorites")
case errors.Is(err, ErrNoRootFound): case errors.Is(err, ErrNoRootFound):
return i18n.T_(c, "error.err_no_root_found") return i18n.T_(c, "error.err_no_root_found")
@@ -186,6 +207,15 @@ func GetErrorCode(c fiber.Ctx, err error) string {
return i18n.T_(c, "error.err_user_has_no_such_cart") return i18n.T_(c, "error.err_user_has_no_such_cart")
case errors.Is(err, ErrProductOrItsVariationDoesNotExist): case errors.Is(err, ErrProductOrItsVariationDoesNotExist):
return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist") return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist")
case errors.Is(err, ErrAmountMustBePositive):
return i18n.T_(c, "error.err_amount_must_be_positive")
case errors.Is(err, ErrAmountMustBeReasonable):
return i18n.T_(c, "error.err_amount_must_be_reasonable")
case errors.Is(err, ErrEmptyCart):
return i18n.T_(c, "error.err_cart_is_empty")
case errors.Is(err, ErrUserHasNoSuchOrder):
return i18n.T_(c, "error.err_user_has_no_such_order")
case errors.Is(err, ErrAccessDenied): case errors.Is(err, ErrAccessDenied):
return i18n.T_(c, "error.err_access_denied") return i18n.T_(c, "error.err_access_denied")
@@ -198,6 +228,15 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrMissingFileFieldDocument): case errors.Is(err, ErrMissingFileFieldDocument):
return i18n.T_(c, "error.err_missing_file_field_document") return i18n.T_(c, "error.err_missing_file_field_document")
case errors.Is(err, ErrInvalidReductionType):
return i18n.T_(c, "error.invalid_reduction_type")
case errors.Is(err, ErrPercentageRequired):
return i18n.T_(c, "error.percentage_required")
case errors.Is(err, ErrPriceRequired):
return i18n.T_(c, "error.price_required")
case errors.Is(err, ErrSpecificPriceNotFound):
return i18n.T_(c, "error.price_reduction_not_found")
case errors.Is(err, ErrJSONBody): case errors.Is(err, ErrJSONBody):
return i18n.T_(c, "error.err_json_body") return i18n.T_(c, "error.err_json_body")
@@ -249,6 +288,9 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrInvalidURLSlug), errors.Is(err, ErrInvalidURLSlug),
errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrInvalidXHTML),
errors.Is(err, ErrBadPaging), errors.Is(err, ErrBadPaging),
errors.Is(err, ErrProductNotFound),
errors.Is(err, ErrAlreadyInFavorites),
errors.Is(err, ErrNotInFavorites),
errors.Is(err, ErrNoRootFound), errors.Is(err, ErrNoRootFound),
errors.Is(err, ErrCircularDependency), errors.Is(err, ErrCircularDependency),
errors.Is(err, ErrStartCategoryNotFound), errors.Is(err, ErrStartCategoryNotFound),
@@ -256,6 +298,13 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrMaxAmtOfCartsReached),
errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrUserHasNoSuchCart),
errors.Is(err, ErrProductOrItsVariationDoesNotExist), errors.Is(err, ErrProductOrItsVariationDoesNotExist),
errors.Is(err, ErrAmountMustBePositive),
errors.Is(err, ErrAmountMustBeReasonable),
errors.Is(err, ErrEmptyCart),
errors.Is(err, ErrUserHasNoSuchOrder),
errors.Is(err, ErrInvalidReductionType),
errors.Is(err, ErrPercentageRequired),
errors.Is(err, ErrPriceRequired),
errors.Is(err, ErrAccessDenied), errors.Is(err, ErrAccessDenied),
errors.Is(err, ErrFolderDoesNotExist), errors.Is(err, ErrFolderDoesNotExist),
errors.Is(err, ErrFileDoesNotExist), errors.Is(err, ErrFileDoesNotExist),
@@ -267,6 +316,8 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrInvalidCountryID), errors.Is(err, ErrInvalidCountryID),
errors.Is(err, ErrInvalidAddressJSON): errors.Is(err, ErrInvalidAddressJSON):
return fiber.StatusBadRequest return fiber.StatusBadRequest
case errors.Is(err, ErrSpecificPriceNotFound):
return fiber.StatusNotFound
case errors.Is(err, ErrEmailExists): case errors.Is(err, ErrEmailExists):
return fiber.StatusConflict return fiber.StatusConflict
case errors.Is(err, ErrAIResponseFail), case errors.Is(err, ErrAIResponseFail),

View File

@@ -18,3 +18,7 @@ type EmailAdminNotificationData struct {
type EmailPasswordResetData struct { type EmailPasswordResetData struct {
ResetURL string ResetURL string
} }
type EmailNewOrderPlacedData struct {
UserID uint
}

98
app/view/product.go Normal file
View File

@@ -0,0 +1,98 @@
package view
import (
"encoding/json"
"time"
)
type ProductAttribute struct {
IDProductAttribute uint `gorm:"column:id_product_attribute" json:"id_product_attribute"`
Reference string `gorm:"column:reference" json:"reference"`
BasePrice float64 `gorm:"column:base_price" json:"base_price"`
PriceTaxExcl float64 `gorm:"column:price_tax_excl" json:"price_tax_excl"`
PriceTaxIncl float64 `gorm:"column:price_tax_incl" json:"price_tax_incl"`
Quantity int64 `gorm:"column:quantity" json:"quantity"`
Attributes json.RawMessage `gorm:"column:attributes" json:"attributes"`
}
type Price struct {
Base float64 `json:"base"`
FinalTaxExcl float64 `json:"final_tax_excl"`
FinalTaxIncl float64 `json:"final_tax_incl"`
TaxRate float64 `json:"tax_rate"`
Priority int `json:"priority"` // or string
}
type Variant struct {
ID uint `json:"id_product_attribute"`
Reference string `json:"reference"`
BasePrice float64 `json:"base_price"`
FinalExcl float64 `json:"final_tax_excl"`
FinalIncl float64 `json:"final_tax_incl"`
Stock int `json:"stock"`
}
type ProductFull struct {
Product Product `json:"product"`
Price Price `json:"price"`
Variants []ProductAttribute `json:"variants,omitempty"`
}
type Product struct {
ID uint `gorm:"column:id" json:"id"`
Reference string `gorm:"column:reference" json:"reference"`
SupplierReference string `gorm:"column:supplier_reference" json:"supplier_reference,omitempty"`
EAN13 string `gorm:"column:ean13" json:"ean13,omitempty"`
UPC string `gorm:"column:upc" json:"upc,omitempty"`
ISBN string `gorm:"column:isbn" json:"isbn,omitempty"`
// Basic Price (from product table)
BasePrice float64 `gorm:"column:base_price" json:"base_price"`
WholesalePrice float64 `gorm:"column:wholesale_price" json:"wholesale_price,omitempty"`
Unity string `gorm:"column:unity" json:"unity,omitempty"`
UnitPriceRatio float64 `gorm:"column:unit_price_ratio" json:"unit_price_ratio,omitempty"`
// Stock & Availability
Quantity int `gorm:"column:quantity" json:"quantity"`
MinimalQuantity int `gorm:"column:minimal_quantity" json:"minimal_quantity"`
AvailableForOrder bool `gorm:"column:available_for_order" json:"available_for_order"`
AvailableDate string `gorm:"column:available_date" json:"available_date,omitempty"`
OutOfStockBehavior int `gorm:"column:out_of_stock_behavior" json:"out_of_stock_behavior"`
// Flags
OnSale bool `gorm:"column:on_sale" json:"on_sale"`
ShowPrice bool `gorm:"column:show_price" json:"show_price"`
Condition string `gorm:"column:condition" json:"condition"`
IsVirtual bool `gorm:"column:is_virtual" json:"is_virtual"`
// Physical
Weight float64 `gorm:"column:weight" json:"weight"`
Width float64 `gorm:"column:width" json:"width"`
Height float64 `gorm:"column:height" json:"height"`
Depth float64 `gorm:"column:depth" json:"depth"`
AdditionalShippingCost float64 `gorm:"column:additional_shipping_cost" json:"additional_shipping_cost,omitempty"`
// Delivery
DeliveryDays int `gorm:"column:delivery_days" json:"delivery_days,omitempty"`
// Status
Active bool `gorm:"column:active" json:"active"`
Visibility string `gorm:"column:visibility" json:"visibility"`
Indexed bool `gorm:"column:indexed" json:"indexed"`
// Timestamps
DateAdd time.Time `gorm:"column:date_add" json:"date_add"`
DateUpd time.Time `gorm:"column:date_upd" json:"date_upd"`
// Language fields
Name string `gorm:"column:name" json:"name"`
Description string `gorm:"column:description" json:"description"`
DescriptionShort string `gorm:"column:description_short" json:"description_short"`
// Relations
Manufacturer string `gorm:"column:manufacturer" json:"manufacturer"`
Category string `gorm:"column:category" json:"category"`
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
}

55
bo/auto-imports.d.ts vendored
View File

@@ -7,10 +7,10 @@
export {} export {}
declare global { declare global {
const avatarGroupInjectionKey: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js').avatarGroupInjectionKey const avatarGroupInjectionKey: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js').avatarGroupInjectionKey
const defineLocale: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js').defineLocale const defineLocale: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/defineLocale').defineLocale
const defineShortcuts: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js').defineShortcuts const defineShortcuts: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts').defineShortcuts
const extendLocale: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js').extendLocale const extendLocale: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/defineLocale').extendLocale
const extractShortcuts: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js').extractShortcuts const extractShortcuts: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts').extractShortcuts
const fieldGroupInjectionKey: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js').fieldGroupInjectionKey const fieldGroupInjectionKey: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js').fieldGroupInjectionKey
const formBusInjectionKey: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js').formBusInjectionKey const formBusInjectionKey: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js').formBusInjectionKey
const formErrorsInjectionKey: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js').formErrorsInjectionKey const formErrorsInjectionKey: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js').formErrorsInjectionKey
@@ -29,46 +29,17 @@ declare global {
const useAvatarGroup: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js').useAvatarGroup const useAvatarGroup: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js').useAvatarGroup
const useComponentIcons: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js').useComponentIcons const useComponentIcons: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js').useComponentIcons
const useComponentUI: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useComponentUI.js').useComponentUI const useComponentUI: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useComponentUI.js').useComponentUI
const useContentSearch: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch.js').useContentSearch const useContentSearch: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch').useContentSearch
const useEditorMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useEditorMenu.js').useEditorMenu const useEditorMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useEditorMenu.js').useEditorMenu
const useFieldGroup: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js').useFieldGroup const useFieldGroup: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js').useFieldGroup
const useFileUpload: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.js').useFileUpload const useFileUpload: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload').useFileUpload
const useFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js').useFormField const useFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useFormField').useFormField
const useKbd: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js').useKbd const useKbd: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useKbd').useKbd
const useLocale: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js').useLocale const useLocale: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js').useLocale
const useOverlay: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.js').useOverlay const useOverlay: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useOverlay').useOverlay
const usePortal: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js').usePortal const usePortal: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js').usePortal
const useResizable: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useResizable.js').useResizable const useResizable: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useResizable').useResizable
const useScrollspy: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy.js').useScrollspy const useScrollShadow: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useScrollShadow').useScrollShadow
const useToast: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useToast.js').useToast const useScrollspy: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy').useScrollspy
} const useToast: typeof import('./node_modules/@nuxt/ui/dist/runtime/composables/useToast').useToast
// for type re-export
declare global {
// @ts-ignore
export type { ShortcutConfig, ShortcutsConfig, ShortcutsOptions } from './node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d'
import('./node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d')
// @ts-ignore
export type { UseComponentIconsProps } from './node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d'
import('./node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d')
// @ts-ignore
export type { ThemeUI, ThemeRootContext } from './node_modules/@nuxt/ui/dist/runtime/composables/useComponentUI.d'
import('./node_modules/@nuxt/ui/dist/runtime/composables/useComponentUI.d')
// @ts-ignore
export type { EditorMenuOptions } from './node_modules/@nuxt/ui/dist/runtime/composables/useEditorMenu.d'
import('./node_modules/@nuxt/ui/dist/runtime/composables/useEditorMenu.d')
// @ts-ignore
export type { UseFileUploadOptions } from './node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d'
import('./node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d')
// @ts-ignore
export type { KbdKey, KbdKeySpecific } from './node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d'
import('./node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d')
// @ts-ignore
export type { OverlayOptions, Overlay } from './node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d'
import('./node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d')
// @ts-ignore
export type { UseResizableProps, UseResizableReturn } from './node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d'
import('./node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d')
// @ts-ignore
export type { Toast } from './node_modules/@nuxt/ui/dist/runtime/composables/useToast.d'
import('./node_modules/@nuxt/ui/dist/runtime/composables/useToast.d')
} }

View File

@@ -1,25 +1,25 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 1,
"workspaces": { "workspaces": {
"": { "": {
"name": "bo", "name": "bo",
"dependencies": { "dependencies": {
"@nuxt/ui": "^4.5.0", "@nuxt/ui": "^4.6.0",
"@tailwindcss/vite": "^4.2.0", "@tailwindcss/vite": "^4.2.2",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"tailwindcss": "^4.2.0", "reka-ui": "^2.9.3",
"tailwindcss": "^4.2.2",
"vue": "beta", "vue": "beta",
"vue-chartjs": "^5.3.3", "vue-chartjs": "^5.3.3",
"vue-i18n": "11", "vue-i18n": "^11.3.0",
"vue-router": "^5.0.2", "vue-router": "^5.0.4",
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node24": "^24.0.4", "@tsconfig/node24": "^24.0.4",
"@types/node": "^24.10.13", "@types/node": "^24.12.0",
"@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue": "^6.0.5",
"@vue/eslint-config-typescript": "^14.6.0", "@vue/eslint-config-typescript": "^14.7.0",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
@@ -27,8 +27,8 @@
"prettier": "3.8.1", "prettier": "3.8.1",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "beta", "vite": "beta",
"vite-plugin-vue-devtools": "^8.0.6", "vite-plugin-vue-devtools": "^8.1.1",
"vue-tsc": "^3.2.4", "vue-tsc": "^3.2.6",
}, },
}, },
}, },
@@ -88,9 +88,9 @@
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
"@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-decorators": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA=="], "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-decorators": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA=="],
@@ -114,63 +114,63 @@
"@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="],
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], "@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
@@ -202,7 +202,7 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@iconify/collections": ["@iconify/collections@1.0.659", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-gD9i7zfQpr3ZAv1BywaEri8o008KQT9iYaMw5EY4Y/y0qeEZB12FJMMyJGS3NvSeG80udu4kx8aZZLRsAdcANw=="], "@iconify/collections": ["@iconify/collections@1.0.668", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-d4ygrDoTqb02p7YNHdNzy64y4VQG0bd3w26EH+mJ0eKAj+DSWxDJ1Qw/WdCAqhKfjb+mRp1n9+YwcELs2YWVlg=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
@@ -234,7 +234,7 @@
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@@ -242,17 +242,17 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@nuxt/devtools-kit": ["@nuxt/devtools-kit@3.2.3", "", { "dependencies": { "@nuxt/kit": "^4.3.1", "execa": "^8.0.1" }, "peerDependencies": { "vite": ">=6.0" } }, "sha512-5zj7Xx5CDI6P84kMalXoxGLd470buF6ncsRhiEPq8UlwdpVeR7bwi8QnparZNFBdG79bZ5KUkfi5YDXpLYPoIA=="], "@nuxt/devtools-kit": ["@nuxt/devtools-kit@3.2.4", "", { "dependencies": { "@nuxt/kit": "^4.4.2", "execa": "^8.0.1" }, "peerDependencies": { "vite": ">=6.0" } }, "sha512-Yxy2Xgmq5hf3dQy983V0xh0OJV2mYwRZz9eVIGc3EaribdFGPDNGMMbYqX9qCty3Pbxn/bCF3J0UyPaNlHVayQ=="],
"@nuxt/fonts": ["@nuxt/fonts@0.14.0", "", { "dependencies": { "@nuxt/devtools-kit": "^3.2.1", "@nuxt/kit": "^4.2.2", "consola": "^3.4.2", "defu": "^6.1.4", "fontless": "^0.2.1", "h3": "^1.15.5", "magic-regexp": "^0.10.0", "ofetch": "^1.5.1", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unifont": "^0.7.4", "unplugin": "^3.0.0", "unstorage": "^1.17.4" } }, "sha512-4uXQl9fa5F4ibdgU8zomoOcyMdnwgdem+Pi8JEqeDYI5yPR32Kam6HnuRr47dTb97CstaepAvXPWQUUHMtjsFQ=="], "@nuxt/fonts": ["@nuxt/fonts@0.14.0", "", { "dependencies": { "@nuxt/devtools-kit": "^3.2.1", "@nuxt/kit": "^4.2.2", "consola": "^3.4.2", "defu": "^6.1.4", "fontless": "^0.2.1", "h3": "^1.15.5", "magic-regexp": "^0.10.0", "ofetch": "^1.5.1", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unifont": "^0.7.4", "unplugin": "^3.0.0", "unstorage": "^1.17.4" } }, "sha512-4uXQl9fa5F4ibdgU8zomoOcyMdnwgdem+Pi8JEqeDYI5yPR32Kam6HnuRr47dTb97CstaepAvXPWQUUHMtjsFQ=="],
"@nuxt/icon": ["@nuxt/icon@2.2.1", "", { "dependencies": { "@iconify/collections": "^1.0.641", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0", "@iconify/vue": "^5.0.0", "@nuxt/devtools-kit": "^3.1.1", "@nuxt/kit": "^4.2.2", "consola": "^3.4.2", "local-pkg": "^1.1.2", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinyglobby": "^0.2.15" } }, "sha512-GI840yYGuvHI0BGDQ63d6rAxGzG96jQcWrnaWIQKlyQo/7sx9PjXkSHckXUXyX1MCr9zY6U25Td6OatfY6Hklw=="], "@nuxt/icon": ["@nuxt/icon@2.2.1", "", { "dependencies": { "@iconify/collections": "^1.0.641", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0", "@iconify/vue": "^5.0.0", "@nuxt/devtools-kit": "^3.1.1", "@nuxt/kit": "^4.2.2", "consola": "^3.4.2", "local-pkg": "^1.1.2", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinyglobby": "^0.2.15" } }, "sha512-GI840yYGuvHI0BGDQ63d6rAxGzG96jQcWrnaWIQKlyQo/7sx9PjXkSHckXUXyX1MCr9zY6U25Td6OatfY6Hklw=="],
"@nuxt/kit": ["@nuxt/kit@4.3.1", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-UjBFt72dnpc+83BV3OIbCT0YHLevJtgJCHpxMX0YRKWLDhhbcDdUse87GtsQBrjvOzK7WUNUYLDS/hQLYev5rA=="], "@nuxt/kit": ["@nuxt/kit@4.4.2", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "mlly": "^1.8.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog=="],
"@nuxt/schema": ["@nuxt/schema@4.3.1", "", { "dependencies": { "@vue/shared": "^3.5.27", "defu": "^6.1.4", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "std-env": "^3.10.0" } }, "sha512-S+wHJdYDuyk9I43Ej27y5BeWMZgi7R/UVql3b3qtT35d0fbpXW7fUenzhLRCCDC6O10sjguc6fcMcR9sMKvV8g=="], "@nuxt/schema": ["@nuxt/schema@4.4.2", "", { "dependencies": { "@vue/shared": "^3.5.30", "defu": "^6.1.4", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "std-env": "^4.0.0" } }, "sha512-/q6C7Qhiricgi+PKR7ovBnJlKTL0memCbA1CzRT+itCW/oeYzUfeMdQ35mGntlBoyRPNrMXbzuSUhfDbSCU57w=="],
"@nuxt/ui": ["@nuxt/ui@4.5.1", "", { "dependencies": { "@floating-ui/dom": "^1.7.5", "@iconify/vue": "^5.0.0", "@internationalized/date": "^3.11.0", "@internationalized/number": "^3.6.5", "@nuxt/fonts": "^0.14.0", "@nuxt/icon": "^2.2.1", "@nuxt/kit": "^4.3.1", "@nuxt/schema": "^4.3.1", "@nuxtjs/color-mode": "^3.5.2", "@standard-schema/spec": "^1.1.0", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/vite": "^4.2.1", "@tanstack/vue-table": "^8.21.3", "@tanstack/vue-virtual": "^3.13.19", "@tiptap/core": "^3.20.0", "@tiptap/extension-bubble-menu": "^3.20.0", "@tiptap/extension-code": "^3.20.0", "@tiptap/extension-collaboration": "^3.20.0", "@tiptap/extension-drag-handle": "^3.20.0", "@tiptap/extension-drag-handle-vue-3": "^3.20.0", "@tiptap/extension-floating-menu": "^3.20.0", "@tiptap/extension-horizontal-rule": "^3.20.0", "@tiptap/extension-image": "^3.20.0", "@tiptap/extension-mention": "^3.20.0", "@tiptap/extension-node-range": "^3.20.0", "@tiptap/extension-placeholder": "^3.20.0", "@tiptap/markdown": "^3.20.0", "@tiptap/pm": "^3.20.0", "@tiptap/starter-kit": "^3.20.0", "@tiptap/suggestion": "^3.20.0", "@tiptap/vue-3": "^3.20.0", "@unhead/vue": "^2.1.10", "@vueuse/core": "^14.2.1", "@vueuse/integrations": "^14.2.1", "@vueuse/shared": "^14.2.1", "colortranslator": "^5.0.0", "consola": "^3.4.2", "defu": "^6.1.4", "embla-carousel-auto-height": "^8.6.0", "embla-carousel-auto-scroll": "^8.6.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-class-names": "^8.6.0", "embla-carousel-fade": "^8.6.0", "embla-carousel-vue": "^8.6.0", "embla-carousel-wheel-gestures": "^8.1.0", "fuse.js": "^7.1.0", "hookable": "^5.5.3", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.0", "motion-v": "^1.10.3", "ohash": "^2.0.11", "pathe": "^2.0.3", "reka-ui": "2.8.2", "scule": "^1.3.0", "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", "tailwindcss": "^4.2.1", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unplugin": "^3.0.0", "unplugin-auto-import": "^21.0.0", "unplugin-vue-components": "^31.0.0", "vaul-vue": "0.4.1", "vue-component-type-helpers": "^3.2.5" }, "peerDependencies": { "@inertiajs/vue3": "^2.0.7", "@nuxt/content": "^3.0.0", "joi": "^18.0.0", "superstruct": "^2.0.0", "typescript": "^5.9.3", "valibot": "^1.0.0", "vue-router": "^4.5.0", "yup": "^1.7.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["@inertiajs/vue3", "@nuxt/content", "joi", "superstruct", "valibot", "vue-router", "yup", "zod"], "bin": { "nuxt-ui": "cli/index.mjs" } }, "sha512-5hWgreVPX6EsNCZNoOd2o7m9fTA3fwUMDw+zeYTSAjhSKtAuvkZrBtmez4MUeTv+LO1gknesgvErdIvlUnElTg=="], "@nuxt/ui": ["@nuxt/ui@4.6.0", "", { "dependencies": { "@floating-ui/dom": "^1.7.6", "@iconify/vue": "^5.0.0", "@internationalized/date": "^3.12.0", "@internationalized/number": "^3.6.5", "@nuxt/fonts": "^0.14.0", "@nuxt/icon": "^2.2.1", "@nuxt/kit": "^4.4.2", "@nuxt/schema": "^4.4.2", "@nuxtjs/color-mode": "^3.5.2", "@standard-schema/spec": "^1.1.0", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/vite": "^4.2.2", "@tanstack/vue-table": "^8.21.3", "@tanstack/vue-virtual": "^3.13.23", "@tiptap/core": "^3.20.4", "@tiptap/extension-bubble-menu": "^3.20.4", "@tiptap/extension-code": "^3.20.4", "@tiptap/extension-collaboration": "^3.20.4", "@tiptap/extension-drag-handle": "^3.20.4", "@tiptap/extension-drag-handle-vue-3": "^3.20.4", "@tiptap/extension-floating-menu": "^3.20.4", "@tiptap/extension-horizontal-rule": "^3.20.4", "@tiptap/extension-image": "^3.20.4", "@tiptap/extension-mention": "^3.20.4", "@tiptap/extension-node-range": "^3.20.4", "@tiptap/extension-placeholder": "^3.20.4", "@tiptap/markdown": "^3.20.4", "@tiptap/pm": "^3.20.4", "@tiptap/starter-kit": "^3.20.4", "@tiptap/suggestion": "^3.20.4", "@tiptap/vue-3": "^3.20.4", "@unhead/vue": "^2.1.12", "@vueuse/core": "^14.2.1", "@vueuse/integrations": "^14.2.1", "@vueuse/shared": "^14.2.1", "colortranslator": "^5.0.0", "consola": "^3.4.2", "defu": "^6.1.4", "embla-carousel-auto-height": "^8.6.0", "embla-carousel-auto-scroll": "^8.6.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-class-names": "^8.6.0", "embla-carousel-fade": "^8.6.0", "embla-carousel-vue": "^8.6.0", "embla-carousel-wheel-gestures": "^8.1.0", "fuse.js": "^7.1.0", "hookable": "^6.1.0", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.2", "motion-v": "^2.2.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "reka-ui": "2.9.2", "scule": "^1.3.0", "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", "tailwindcss": "^4.2.2", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unplugin": "^3.0.0", "unplugin-auto-import": "^21.0.0", "unplugin-vue-components": "^31.1.0", "vaul-vue": "0.4.1", "vue-component-type-helpers": "^3.2.6" }, "peerDependencies": { "@inertiajs/vue3": "^2.0.7", "@nuxt/content": "^3.0.0", "joi": "^18.0.0", "superstruct": "^2.0.0", "typescript": "^5.6.3", "valibot": "^1.0.0", "vue-router": "^4.5.0 || ^5.0.0", "yup": "^1.7.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["@inertiajs/vue3", "@nuxt/content", "joi", "superstruct", "valibot", "vue-router", "yup", "zod"], "bin": { "nuxt-ui": "cli/index.mjs" } }, "sha512-AcoM3bljDvxd4PGfzDbY4zyyy9Zsajfagr3il1iki7G8Ji3SG6Xsgy+weug+Hyp76YM5919oc6ciJmram5bf6A=="],
"@nuxtjs/color-mode": ["@nuxtjs/color-mode@3.5.2", "", { "dependencies": { "@nuxt/kit": "^3.13.2", "pathe": "^1.1.2", "pkg-types": "^1.2.1", "semver": "^7.6.3" } }, "sha512-cC6RfgZh3guHBMLLjrBB2Uti5eUoGM9KyauOaYS9ETmxNWBMTvpgjvSiSJp1OFljIXPIqVTJ3xtJpSNZiO3ZaA=="], "@nuxtjs/color-mode": ["@nuxtjs/color-mode@3.5.2", "", { "dependencies": { "@nuxt/kit": "^3.13.2", "pathe": "^1.1.2", "pkg-types": "^1.2.1", "semver": "^7.6.3" } }, "sha512-cC6RfgZh3guHBMLLjrBB2Uti5eUoGM9KyauOaYS9ETmxNWBMTvpgjvSiSJp1OFljIXPIqVTJ3xtJpSNZiO3ZaA=="],
@@ -336,121 +336,121 @@
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="], "@swc/helpers": ["@swc/helpers@0.5.20", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="], "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="],
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.21", "", {}, "sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw=="], "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.23", "", {}, "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg=="],
"@tanstack/vue-table": ["@tanstack/vue-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "vue": ">=3.2" } }, "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw=="], "@tanstack/vue-table": ["@tanstack/vue-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "vue": ">=3.2" } }, "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw=="],
"@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.21", "", { "dependencies": { "@tanstack/virtual-core": "3.13.21" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-zneUNdQTcUhoDl6+ek+/O4S9gSZRAc2q7VLscZ4WZnFfZcHc3M7OyVCfSDC3hGuwFqzfL8Cx5bZF6zbGCYwXmw=="], "@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.23", "", { "dependencies": { "@tanstack/virtual-core": "3.13.23" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-b5jPluAR6U3eOq6GWAYSpj3ugnAIZgGR0e6aGAgyRse0Yu6MVQQ0ZWm9SArSXWtageogn6bkVD8D//c4IjW3xQ=="],
"@tiptap/core": ["@tiptap/core@3.20.1", "", { "peerDependencies": { "@tiptap/pm": "^3.20.1" } }, "sha512-SwkPEWIfaDEZjC8SEIi4kZjqIYUbRgLUHUuQezo5GbphUNC8kM1pi3C3EtoOPtxXrEbY6e4pWEzW54Pcrd+rVA=="], "@tiptap/core": ["@tiptap/core@3.22.0", "", { "peerDependencies": { "@tiptap/pm": "^3.22.0" } }, "sha512-EA/XFbvvz0yRyccqrgOwB9RQe6+uJ8NszjLKH9+3xPE2/+Sa2imax0IqWl7YOXkWihdQVrlpP+EpQF9APKx3jg=="],
"@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-WzNXk/63PQI2fav4Ta6P0GmYRyu8Gap1pV3VUqaVK829iJ6Zt1T21xayATHEHWMK27VT1GLPJkx9Ycr2jfDyQw=="], "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-WF7K1jtEhkhCZFOoei3QrUHMsM6i9eqXw1IuL6cAX3+CBpqVg89KbP/cJp05dYKU0SO0LJkn87biKVqcnAcN7A=="],
"@tiptap/extension-bold": ["@tiptap/extension-bold@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-fz++Qv6Rk/Hov0IYG/r7TJ1Y4zWkuGONe0UN5g0KY32NIMg3HeOHicbi4xsNWTm9uAOl3eawWDkezEMrleObMw=="], "@tiptap/extension-bold": ["@tiptap/extension-bold@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-mPG1FzOy2DVaJHHuX/eQPIuYie0kqG07M04nElBY8QlV0oYB4/kd0Aubz+m9czqHx/F9u/L98kmMFhCh2DWk2w=="],
"@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.20.1", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-XaPvO6aCoWdFnCBus0s88lnj17NR/OopV79i8Qhgz3WMR0vrsL5zsd45l0lZuu9pSvm5VW47SoxakkJiZC1suw=="], "@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.22.0", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-792CUdP0roO17jQJ+fflSJEWfw2cAric61nV2291a2iL7L/6mNJGP4QFim1FqZzfx3/FwHSXEI3NYT6wdUlh8w=="],
"@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.20.1", "", { "peerDependencies": { "@tiptap/extension-list": "^3.20.1" } }, "sha512-mbrlvOZo5OF3vLhp+3fk9KuL/6J/wsN0QxF6ZFRAHzQ9NkJdtdfARcBeBnkWXGN8inB6YxbTGY1/E4lmBkOpOw=="], "@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.22.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.22.0" } }, "sha512-GUBYUXlNMxfJVpbDQnwNU54rqIBHEIQ7KyPptghSzvmnwMlr6n10OmYSGSVvNpOAIYkHkuhzkwgUDeU+YzWbjg=="],
"@tiptap/extension-code": ["@tiptap/extension-code@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-509DHINIA/Gg+eTG7TEkfsS8RUiPLH5xZNyLRT0A1oaoaJmECKfrV6aAm05IdfTyqDqz6LW5pbnX6DdUC4keug=="], "@tiptap/extension-code": ["@tiptap/extension-code@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-JGxByyyUdR0yRt1mOxnA2dp6PmI9pr6C846UkZtuOCwhBOBLkoBGkZqW4FytLPOfWGVJpm7w5tx7h1n5uNwfag=="],
"@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-vKejwBq+Nlj4Ybd3qOyDxIQKzYymdNH+8eXkKwGShk2nfLJIxq69DCyGvmuHgipIO1qcYPJ149UNpGN+YGcdmA=="], "@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-HtnYHj6yHVy2dKs02j5dyEehWQMGOGRMiZBkefY3TwSSNzGESVcFfDV+Xr87j7zDGYvY16vOtVmyRTOTqPn49A=="],
"@tiptap/extension-collaboration": ["@tiptap/extension-collaboration@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1", "@tiptap/y-tiptap": "^3.0.2", "yjs": "^13" } }, "sha512-JnwLvyzrutBffHp6YPnf0XyTnhAgqZ9D8JSUKFp0edvai+dxsb+UMlawesBrgAuoQXw3B8YZUo2VFDVdKas1xQ=="], "@tiptap/extension-collaboration": ["@tiptap/extension-collaboration@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0", "@tiptap/y-tiptap": "^3.0.2", "yjs": "^13" } }, "sha512-S/nPIth4/Dr5wmxROk4ELWRYhBXxWt7O6cIi8Fca1rYDBdXofjgt4VDO2iCjnOF0LkIekjQudSOFLWadbM2Z+w=="],
"@tiptap/extension-document": ["@tiptap/extension-document@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-9vrqdGmRV7bQCSY3NLgu7UhIwgOCDp4sKqMNsoNRX0aZ021QQMTvBQDPkiRkCf7MNsnWrNNnr52PVnULEn3vFQ=="], "@tiptap/extension-document": ["@tiptap/extension-document@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-ZdtuBt2KnxIYBtp/VrKiWQ5cLPw1qDKb+sieipBaDWuvhgDNi1kfr//ByEP8xPhcjJfH/C3PCdYYVwIUJwzdqQ=="],
"@tiptap/extension-drag-handle": ["@tiptap/extension-drag-handle@3.20.1", "", { "dependencies": { "@floating-ui/dom": "^1.6.13" }, "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/extension-collaboration": "^3.20.1", "@tiptap/extension-node-range": "^3.20.1", "@tiptap/pm": "^3.20.1", "@tiptap/y-tiptap": "^3.0.2" } }, "sha512-sIfu5Gt1aZVAagvzr+3Qx+8GE294eU3G6/pYjqNHvf71sDCpdN2gFpeI7WF05foVsCGsH6ieu8x/kTXf5GbNZA=="], "@tiptap/extension-drag-handle": ["@tiptap/extension-drag-handle@3.22.0", "", { "dependencies": { "@floating-ui/dom": "^1.6.13" }, "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/extension-collaboration": "^3.22.0", "@tiptap/extension-node-range": "^3.22.0", "@tiptap/pm": "^3.22.0", "@tiptap/y-tiptap": "^3.0.2" } }, "sha512-OdOCqdgprFrvVhbLCCGX1D0hXo/2SL8o1o50Gy0l1lgiH9vqi/B2LjUJZwYYneQa1K0ME1J4dL4Vp89V4jej0A=="],
"@tiptap/extension-drag-handle-vue-3": ["@tiptap/extension-drag-handle-vue-3@3.20.1", "", { "peerDependencies": { "@tiptap/extension-drag-handle": "^3.20.1", "@tiptap/pm": "^3.20.1", "@tiptap/vue-3": "^3.20.1", "vue": "^3.0.0" } }, "sha512-FJonOxcj/I7uBpJBSqfEfU+Fqem5aanEj+EsqG5ZQnxhp7cxbUYZkGH0tBBR7jeIIkt2Jk+1LoCAlYzCWbDLcQ=="], "@tiptap/extension-drag-handle-vue-3": ["@tiptap/extension-drag-handle-vue-3@3.22.0", "", { "peerDependencies": { "@tiptap/extension-drag-handle": "^3.22.0", "@tiptap/pm": "^3.22.0", "@tiptap/vue-3": "^3.22.0", "vue": "^3.0.0" } }, "sha512-QuA3Fxe/jxJAW1LzUazN5FEA0i8UyUQCRQ46IzpA878RI2yzeaOfxTiY26s0oA7uUaoAyPg/pLgVjIdnc1epfA=="],
"@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.20.1", "", { "peerDependencies": { "@tiptap/extensions": "^3.20.1" } }, "sha512-K18L9FX4znn+ViPSIbTLOGcIaXMx/gLNwAPE8wPLwswbHhQqdiY1zzdBw6drgOc1Hicvebo2dIoUlSXOZsOEcw=="], "@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.22.0", "", { "peerDependencies": { "@tiptap/extensions": "^3.22.0" } }, "sha512-yI0aMD4szbNdy/dlglPbZ9Ddc2UucRatJSifmtenCLg7YWyIIYton0T6Uym+FXAEUZ6KsaoNqEKiUbK5cRzZMQ=="],
"@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.20.1", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-BeDC6nfOesIMn5pFuUnkEjOxGv80sOJ8uk1mdt9/3Fkvra8cB9NIYYCVtd6PU8oQFmJ8vFqPrRkUWrG5tbqnOg=="], "@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.22.0", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-6Gg3I6n+YaCJyvpcKheWiOtU9Oy0M3lbwUGdLK7jTxgAG2YOJxEcx2CzDv3PtNcoyAVUVk9Eio21awVMOECLAQ=="],
"@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.20.1", "", { "peerDependencies": { "@tiptap/extensions": "^3.20.1" } }, "sha512-kZOtttV6Ai8VUAgEng3h4WKFbtdSNJ6ps7r0cRPY+FctWhVmgNb/JJwwyC+vSilR7nRENAhrA/Cv/RxVlvLw+g=="], "@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.22.0", "", { "peerDependencies": { "@tiptap/extensions": "^3.22.0" } }, "sha512-VsnaTU88PlA/eG9DtUvuB90z5gVZIaH6T/JVTxGasxR4CFsv0L4Zq5awwr0+SsYH9dKepRMgbanVU03c6k1SuA=="],
"@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-9sKpmg/IIdlLXimYWUZ3PplIRcehv4Oc7V1miTqlnAthMzjMqigDkjjgte4JZV67RdnDJTQkRw8TklCAU28Emg=="], "@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-F51pt3fgjbtWrY0Uud+5HoJW4f7w/aBZvmoCk19nrEY955vvuQQ2PD/DZtecl4A8fF50PpRjgilrYnnh99l0ew=="],
"@tiptap/extension-heading": ["@tiptap/extension-heading@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-unudyfQP6FxnyWinxvPqe/51DG91J6AaJm666RnAubgYMCgym+33kBftx4j4A6qf+ddWYbD00thMNKOnVLjAEQ=="], "@tiptap/extension-heading": ["@tiptap/extension-heading@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-SnOUBXzh9Dft7HY0rqaSL/kZKg4W9wlHfpnFPW8aIuewXvFDLKa6PisqxPpHsXSbG21kfs5E0MLdwdXtNP89XA=="],
"@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-rjFKFXNntdl0jay8oIGFvvykHlpyQTLmrH3Ag2fj3i8yh6MVvqhtaDomYQbw5sxECd5hBkL+T4n2d2DRuVw/QQ=="], "@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-9v08PcmJOumVmgGgcuFPZpAk+tf+m7+vaCNsNyf8Ce1i0m3GPSle1ZmxzjDU2FlpaCFrcgoUKlEjKYaFYFCJIg=="],
"@tiptap/extension-image": ["@tiptap/extension-image@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-/GPFSLNdYSZQ0E6VBXSAk0UFtDdn98HT1Aa2tO+STELqc5jAdFB42dfFnTC6KQzTvcUOUYkE2S1Q22YC5H2XNQ=="], "@tiptap/extension-image": ["@tiptap/extension-image@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-b3n86lWF3thywUrWmZv27EzTGhCBTjifXY/NzPfri3D22cKkxardrJi2WZIc0J65je91oTzqY7SkdQnyIrZksw=="],
"@tiptap/extension-italic": ["@tiptap/extension-italic@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-ZYRX13Kt8tR8JOzSXirH3pRpi8x30o7LHxZY58uXBdUvr3tFzOkh03qbN523+diidSVeHP/aMd/+IrplHRkQug=="], "@tiptap/extension-italic": ["@tiptap/extension-italic@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-+qq9QZF44O1MRqk6w1AMDZ8oDBs5AtdDdNEcdXpzVU54cJAtWyEPEfXtD0B68hOUp/RdZjMdL27fp+4Id7C1YA=="],
"@tiptap/extension-link": ["@tiptap/extension-link@3.20.1", "", { "dependencies": { "linkifyjs": "^4.3.2" }, "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-oYTTIgsQMqpkSnJAuAc+UtIKMuI4lv9e1y4LfI1iYm6NkEUHhONppU59smhxHLzb3Ww7YpDffbp5IgDTAiJztA=="], "@tiptap/extension-link": ["@tiptap/extension-link@3.22.0", "", { "dependencies": { "linkifyjs": "^4.3.2" }, "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-tGMBUAmni532G6R5gnaRvTb6c7+ST1qCHBV0p5kGGzdHaQTDd1R7S8fnuA3M7+6Sruc82iIY+Ur+6Tusvo/vLA=="],
"@tiptap/extension-list": ["@tiptap/extension-list@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-euBRAn0mkV7R2VEE+AuOt3R0j9RHEMFXamPFmtvTo8IInxDClusrm6mJoDjS8gCGAXsQCRiAe1SCQBPgGbOOwg=="], "@tiptap/extension-list": ["@tiptap/extension-list@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-NfSCAgX44NVLib6aN4HmsP1wi6fFfK3dt6TBb9EgcR82nzq6n7dq7VEBw9V1aKqeXQEtNpqMnQFd0SDayweyfQ=="],
"@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.20.1", "", { "peerDependencies": { "@tiptap/extension-list": "^3.20.1" } }, "sha512-tzgnyTW82lYJkUnadYbatwkI9dLz/OWRSWuFpQPRje/ItmFMWuQ9c9NDD8qLbXPdEYnvrgSAA+ipCD/1G0qA0Q=="], "@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.22.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.22.0" } }, "sha512-9cFvFLEtf0bnOc/LaGeX2D+c9wOxeqhzgabUl2Ztz8Xzoby4JtamXnrIpb7DG7hZMf3luJMF8bz0HSvAnNiISQ=="],
"@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.20.1", "", { "peerDependencies": { "@tiptap/extension-list": "^3.20.1" } }, "sha512-Dr0xsQKx0XPOgDg7xqoWwfv7FFwZ3WeF3eOjqh3rDXlNHMj1v+UW5cj1HLphrsAZHTrVTn2C+VWPJkMZrSbpvQ=="], "@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.22.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.22.0" } }, "sha512-uCtr5/g+Cwkmsb/VLctgo4VjKm0jv52moAmDyr/TLRjW94gnSLhwXFKzyd7BNIXBQHDyS44UEIJFD3ul4dUKdw=="],
"@tiptap/extension-mention": ["@tiptap/extension-mention@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1", "@tiptap/suggestion": "^3.20.1" } }, "sha512-KOGokj7oH1QpcM8P02V+o6wHsVE0g7XEtdIy2vtq2vlFE3npNNNFkMa8F8VWX6qyC+VeVrNU6SIzS5MFY2TORA=="], "@tiptap/extension-mention": ["@tiptap/extension-mention@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0", "@tiptap/suggestion": "^3.22.0" } }, "sha512-Ndjn8YTcZMyyEV5cm+3TupRsQ4b8+4gRrZXfsqbEbXN/IGXgl+ObBx+bxBU+aAe2ZRgqEeuVIBIVWBzYaxLxrg=="],
"@tiptap/extension-node-range": ["@tiptap/extension-node-range@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-+W/mQJxlkXMcwldWUqwdoR0eniJ1S9cVJoAy2Lkis0NhILZDWVNGKl9J4WFoCOXn8Myr17IllIxRYvAXJJ4FHQ=="], "@tiptap/extension-node-range": ["@tiptap/extension-node-range@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-tize9T79ABbOmkw3+7k8cFjSU/JMpzv1JyvFXuGD7lmiHSrFH9Ll8uOGJC7qhCGJWMsjVvrKmBHSOGbzfJI5hQ=="],
"@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.20.1", "", { "peerDependencies": { "@tiptap/extension-list": "^3.20.1" } }, "sha512-Y+3Ad7OwAdagqdYwCnbqf7/to5ypD4NnUNHA0TXRCs7cAHRA8AdgPoIcGFpaaSpV86oosNU3yfeJouYeroffog=="], "@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.22.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.22.0" } }, "sha512-B5JSJ2Xe2KPIYYG7jpZHVeAku/VJB+CCgPYl+qIHjZ4JGTnW23qkIA+6dWk6WljGmhQL1qusxZZn4UAnZtBLeQ=="],
"@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-QFrAtXNyv7JSnomMQc1nx5AnG9mMznfbYJAbdOQYVdbLtAzTfiTuNPNbQrufy5ZGtGaHxDCoaygu2QEfzaKG+Q=="], "@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-fwkPvbGI3xvzWrTJVGZVocgA99Pgqd5kW7iv7MEWlI9uOUa6Ifu31/seHV7j+QDW3y3mADcx+zyhxcMVELtLjA=="],
"@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@3.20.1", "", { "peerDependencies": { "@tiptap/extensions": "^3.20.1" } }, "sha512-k+jfbCugYGuIFBdojukgEopGazIMOgHrw46FnyN2X/6ICOIjQP2rh2ObslrsUOsJYoEevxCsNF9hZl1HvWX66g=="], "@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@3.22.0", "", { "peerDependencies": { "@tiptap/extensions": "^3.22.0" } }, "sha512-1GCc87DIPG3jgLH1Xg3ZW11LEzEWnpinLBIZ8RQqrEH0UwtRUW3QsbvwnKKgbbAEtA91GAdnc1eUQ0dpn+tSeQ=="],
"@tiptap/extension-strike": ["@tiptap/extension-strike@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-EYgyma10lpsY+rwbVQL9u+gA7hBlKLSMFH7Zgd37FSxukOjr+HE8iKPQQ+SwbGejyDsPlLT8Z5Jnuxo5Ng90Pg=="], "@tiptap/extension-strike": ["@tiptap/extension-strike@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-gCgFr1sIcqrJeV5Gdrh8KVZHA+0B1FpFBuOi6FzMyVfBB2sBBqKnjoInYTkPXXdP49Qu8L8hi4luFQtoj4zGzA=="],
"@tiptap/extension-text": ["@tiptap/extension-text@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-7PlIbYW8UenV6NPOXHmv8IcmPGlGx6HFq66RmkJAOJRPXPkTLAiX0N8rQtzUJ6jDEHqoJpaHFEHJw0xzW1yF+A=="], "@tiptap/extension-text": ["@tiptap/extension-text@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-FQ3lBRswZbSEbtxOnDF4T7pdsZRmKh/8q+M29zXaDHGfBc6nuGNPlNKSIy0Iryjhf/YmMVaWDpHvzk56KD7QtA=="],
"@tiptap/extension-underline": ["@tiptap/extension-underline@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1" } }, "sha512-fmHvDKzwCgnZUwRreq8tYkb1YyEwgzZ6QQkAQ0CsCRtvRMqzerr3Duz0Als4i8voZTuGDEL3VR6nAJbLAb/wPg=="], "@tiptap/extension-underline": ["@tiptap/extension-underline@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0" } }, "sha512-AxQOnXQwYmZNjagkEoCZZqbpJbLVmBcu1ivJ9dE0SAQsr1wRUp7mAg+g1SqhbMAvrXvv7yhhNevSdQKmXsnFyA=="],
"@tiptap/extensions": ["@tiptap/extensions@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-JRc/v+OBH0qLTdvQ7HvHWTxGJH73QOf1MC0R8NhOX2QnAbg2mPFv1h+FjGa2gfLGuCXBdWQomjekWkUKbC4e5A=="], "@tiptap/extensions": ["@tiptap/extensions@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-En8p1FiFBT3V9CduErCyLPFxDRsYLISb2cCtLKTeYVeCRn2vQZK4B8WuOgHI4IBipz3I3XidmDhra4yt8mmi/w=="],
"@tiptap/markdown": ["@tiptap/markdown@3.20.1", "", { "dependencies": { "marked": "^17.0.1" }, "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-dNrtP7kmabDomgjv9G/6+JSFL6WraPaFbmKh1eHSYKdDGvIwBfJnVPTV2VS3bP1OuYJEDJN/2ydtiCHyOTrQsQ=="], "@tiptap/markdown": ["@tiptap/markdown@3.22.0", "", { "dependencies": { "marked": "^17.0.1" }, "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-lvYqID2ui4+HEgh/zULQJoKfb7ILe6ahB9IeaJ5R48mrVyeudciqoJV6VmNOaucm/UpKExlDC3I+QlA9sLpYPw=="],
"@tiptap/pm": ["@tiptap/pm@3.20.1", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.24.1", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.38.1" } }, "sha512-6kCiGLvpES4AxcEuOhb7HR7/xIeJWMjZlb6J7e8zpiIh5BoQc7NoRdctsnmFEjZvC19bIasccshHQ7H2zchWqw=="], "@tiptap/pm": ["@tiptap/pm@3.22.0", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.24.1", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.38.1" } }, "sha512-O9kpzNnFX5837kFevwAM8yr7ImLHu8noIwIpoci0AwfJjiBMzfZBejhbzxnKEfTpFWnkvZ8rWohlb6CQdJ6Crg=="],
"@tiptap/starter-kit": ["@tiptap/starter-kit@3.20.1", "", { "dependencies": { "@tiptap/core": "^3.20.1", "@tiptap/extension-blockquote": "^3.20.1", "@tiptap/extension-bold": "^3.20.1", "@tiptap/extension-bullet-list": "^3.20.1", "@tiptap/extension-code": "^3.20.1", "@tiptap/extension-code-block": "^3.20.1", "@tiptap/extension-document": "^3.20.1", "@tiptap/extension-dropcursor": "^3.20.1", "@tiptap/extension-gapcursor": "^3.20.1", "@tiptap/extension-hard-break": "^3.20.1", "@tiptap/extension-heading": "^3.20.1", "@tiptap/extension-horizontal-rule": "^3.20.1", "@tiptap/extension-italic": "^3.20.1", "@tiptap/extension-link": "^3.20.1", "@tiptap/extension-list": "^3.20.1", "@tiptap/extension-list-item": "^3.20.1", "@tiptap/extension-list-keymap": "^3.20.1", "@tiptap/extension-ordered-list": "^3.20.1", "@tiptap/extension-paragraph": "^3.20.1", "@tiptap/extension-strike": "^3.20.1", "@tiptap/extension-text": "^3.20.1", "@tiptap/extension-underline": "^3.20.1", "@tiptap/extensions": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-opqWxL/4OTEiqmVC0wsU4o3JhAf6LycJ2G/gRIZVAIFLljI9uHfpPMTFGxZ5w9IVVJaP5PJysfwW/635kKqkrw=="], "@tiptap/starter-kit": ["@tiptap/starter-kit@3.22.0", "", { "dependencies": { "@tiptap/core": "^3.22.0", "@tiptap/extension-blockquote": "^3.22.0", "@tiptap/extension-bold": "^3.22.0", "@tiptap/extension-bullet-list": "^3.22.0", "@tiptap/extension-code": "^3.22.0", "@tiptap/extension-code-block": "^3.22.0", "@tiptap/extension-document": "^3.22.0", "@tiptap/extension-dropcursor": "^3.22.0", "@tiptap/extension-gapcursor": "^3.22.0", "@tiptap/extension-hard-break": "^3.22.0", "@tiptap/extension-heading": "^3.22.0", "@tiptap/extension-horizontal-rule": "^3.22.0", "@tiptap/extension-italic": "^3.22.0", "@tiptap/extension-link": "^3.22.0", "@tiptap/extension-list": "^3.22.0", "@tiptap/extension-list-item": "^3.22.0", "@tiptap/extension-list-keymap": "^3.22.0", "@tiptap/extension-ordered-list": "^3.22.0", "@tiptap/extension-paragraph": "^3.22.0", "@tiptap/extension-strike": "^3.22.0", "@tiptap/extension-text": "^3.22.0", "@tiptap/extension-underline": "^3.22.0", "@tiptap/extensions": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-3V0RysviBKbsvzHuupM30ftb/WLogSgINtIbmGQHZK4Ta1YzwzW63nAWPGKOoFw8r1HRFFO6LjtqrT6iJsPnzQ=="],
"@tiptap/suggestion": ["@tiptap/suggestion@3.20.1", "", { "peerDependencies": { "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1" } }, "sha512-ng7olbzgZhWvPJVJygNQK5153CjquR2eJXpkLq7bRjHlahvt4TH4tGFYvGdYZcXuzbe2g9RoqT7NaPGL9CUq9w=="], "@tiptap/suggestion": ["@tiptap/suggestion@3.22.0", "", { "peerDependencies": { "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0" } }, "sha512-qdZ4v7sXkM4p2+M7OWCGVoNmHmiaHhiGQN3lUzaNG7mG1gZ2SDirSaeOfL8lHxpnPlGzmE4tUe5eCWMpNKHypA=="],
"@tiptap/vue-3": ["@tiptap/vue-3@3.20.1", "", { "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.20.1", "@tiptap/extension-floating-menu": "^3.20.1" }, "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "^3.20.1", "@tiptap/pm": "^3.20.1", "vue": "^3.0.0" } }, "sha512-06IsNzIkAC0HPHNSdXf4AgtExnr02BHBLXmUiNrjjhgHdxXDKIB0Yq3hEWEfbWNPr3lr7jK6EaFu2IKFMhoUtQ=="], "@tiptap/vue-3": ["@tiptap/vue-3@3.22.0", "", { "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.22.0", "@tiptap/extension-floating-menu": "^3.22.0" }, "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "^3.22.0", "@tiptap/pm": "^3.22.0", "vue": "^3.0.0" } }, "sha512-CVThMRtCAYmz+N5RtnxRAjfPZwavdqi0RqjznF3M0Tc5UVrwfJG5YYtE7uzySmzuxW7VrkjTUC+Zx9YM2hQEXQ=="],
"@tiptap/y-tiptap": ["@tiptap/y-tiptap@3.0.2", "", { "dependencies": { "lib0": "^0.2.100" }, "peerDependencies": { "prosemirror-model": "^1.7.1", "prosemirror-state": "^1.2.3", "prosemirror-view": "^1.9.10", "y-protocols": "^1.0.1", "yjs": "^13.5.38" } }, "sha512-flMn/YW6zTbc6cvDaUPh/NfLRTXDIqgpBUkYzM74KA1snqQwhOMjnRcnpu4hDFrTnPO6QGzr99vRyXEA7M44WA=="], "@tiptap/y-tiptap": ["@tiptap/y-tiptap@3.0.2", "", { "dependencies": { "lib0": "^0.2.100" }, "peerDependencies": { "prosemirror-model": "^1.7.1", "prosemirror-state": "^1.2.3", "prosemirror-view": "^1.9.10", "y-protocols": "^1.0.1", "yjs": "^13.5.38" } }, "sha512-flMn/YW6zTbc6cvDaUPh/NfLRTXDIqgpBUkYzM74KA1snqQwhOMjnRcnpu4hDFrTnPO6QGzr99vRyXEA7M44WA=="],
@@ -474,29 +474,29 @@
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="], "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
"@unhead/vue": ["@unhead/vue@2.1.12", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.12" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-zEWqg0nZM8acpuTZE40wkeUl8AhIe0tU0OkilVi1D4fmVjACrwoh5HP6aNqJ8kUnKsoy6D+R3Vi/O+fmdNGO7g=="], "@unhead/vue": ["@unhead/vue@2.1.12", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.12" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-zEWqg0nZM8acpuTZE40wkeUl8AhIe0tU0OkilVi1D4fmVjACrwoh5HP6aNqJ8kUnKsoy6D+R3Vi/O+fmdNGO7g=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="], "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg=="],
"@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="], "@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="],
@@ -512,39 +512,39 @@
"@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@1.5.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.0", "@vue/compiler-sfc": "^3.5.18" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w=="], "@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@1.5.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.0", "@vue/compiler-sfc": "^3.5.18" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w=="],
"@vue/compiler-core": ["@vue/compiler-core@3.6.0-beta.7", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.6.0-beta.7", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-fYOlA9PCFuf5l8AtKJSxAM+K+MLoM8dyN9Xsa/GDE0Uf6wxFe33AEU6xyVqYMPy6uxp4RtgE4KuQ7JdYvE0Jnw=="], "@vue/compiler-core": ["@vue/compiler-core@3.6.0-beta.9", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.6.0-beta.9", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-/qlfk4cbU//tgdwWeAbl4kBvH5ZocjJ6CMGigzRKk1So/0jl6kRpFxVbc6oNsYYH6Xj28ByA+msAes1FjExejg=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.6.0-beta.7", "", { "dependencies": { "@vue/compiler-core": "3.6.0-beta.7", "@vue/shared": "3.6.0-beta.7" } }, "sha512-4myAKKPogd6MAA/oKKn1+uDOPGJ6gB+pivaUxaBqfvXJch1QHSpcUgdHCJqDb9O/99EjcwppkTLGe7cRWMSqHQ=="], "@vue/compiler-dom": ["@vue/compiler-dom@3.6.0-beta.9", "", { "dependencies": { "@vue/compiler-core": "3.6.0-beta.9", "@vue/shared": "3.6.0-beta.9" } }, "sha512-Bh2h+Co2Vl85jhNjR+yBU1oIujqQ3XIhF7FzODBStvp8yn2WvsXmm5QlvnE7q+smJkfglTbfFcaou8fjR0Y4ag=="],
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.6.0-beta.7", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.6.0-beta.7", "@vue/compiler-dom": "3.6.0-beta.7", "@vue/compiler-ssr": "3.6.0-beta.7", "@vue/compiler-vapor": "3.6.0-beta.7", "@vue/shared": "3.6.0-beta.7", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-bf0NkJb0Hxoj1K8cZKYgduEY0FPP28WnoUGqHvYA/a1cjJfrhaypbiS5u6pEFVXug/JZJF9sI+c4JTHkgqRKDw=="], "@vue/compiler-sfc": ["@vue/compiler-sfc@3.6.0-beta.9", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.6.0-beta.9", "@vue/compiler-dom": "3.6.0-beta.9", "@vue/compiler-ssr": "3.6.0-beta.9", "@vue/compiler-vapor": "3.6.0-beta.9", "@vue/shared": "3.6.0-beta.9", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-13MwFqrIN+AFxR5uT88WYaMTxXycpNNI75sIllz5blXbFaBsDfiegQZj10Pj5G3cgH7WFy2Trs1rTlXrmSgG/w=="],
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.6.0-beta.7", "", { "dependencies": { "@vue/compiler-dom": "3.6.0-beta.7", "@vue/shared": "3.6.0-beta.7" } }, "sha512-fwO4qe6iXZ+SZedurh/vLwjBgjAX/wL5aI/eIECv0C79AgTAlJNjD6dwMafukhnZamQgNKYBxXUOkGl8sqn1PA=="], "@vue/compiler-ssr": ["@vue/compiler-ssr@3.6.0-beta.9", "", { "dependencies": { "@vue/compiler-dom": "3.6.0-beta.9", "@vue/shared": "3.6.0-beta.9" } }, "sha512-ufzHVMJoMz7QzMFop03a8KpWAWUh4hiof/tJXvxiDClIFUY7P5OrKk644fV9qlWgTP7V+eQvA7kVSEVRUgYjvA=="],
"@vue/compiler-vapor": ["@vue/compiler-vapor@3.6.0-beta.7", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-dom": "3.6.0-beta.7", "@vue/shared": "3.6.0-beta.7", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-fc6ZAFv9yRSoQQVsgN5gGDaUWdTPctDb6lVUtUGgnrCB7n+hQIBwuId7HIeic3PK6HPVZMoEbluNU/ge3DIveA=="], "@vue/compiler-vapor": ["@vue/compiler-vapor@3.6.0-beta.9", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-dom": "3.6.0-beta.9", "@vue/shared": "3.6.0-beta.9", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-1WWCcRjkH/scYJ8dsjNp8+NDf4f9SwQgOcd4Hbl5p0srNPflqrQYvAaJh4N2Szxj5FdkXw2t5uiK35TpFM5uKA=="],
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="], "@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
"@vue/devtools-core": ["@vue/devtools-core@8.0.7", "", { "dependencies": { "@vue/devtools-kit": "^8.0.7", "@vue/devtools-shared": "^8.0.7" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-PmpiPxvg3Of80ODHVvyckxwEW1Z02VIAvARIZS1xegINn3VuNQLm9iHUmKD+o6cLkMNWV8OG8x7zo0kgydZgdg=="], "@vue/devtools-core": ["@vue/devtools-core@8.1.1", "", { "dependencies": { "@vue/devtools-kit": "^8.1.1", "@vue/devtools-shared": "^8.1.1" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-bCCsSABp1/ot4j8xJEycM6Mtt2wbuucfByr6hMgjbYhrtlscOJypZKvy8f1FyWLYrLTchB5Qz216Lm92wfbq0A=="],
"@vue/devtools-kit": ["@vue/devtools-kit@8.0.7", "", { "dependencies": { "@vue/devtools-shared": "^8.0.7", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw=="], "@vue/devtools-kit": ["@vue/devtools-kit@8.1.1", "", { "dependencies": { "@vue/devtools-shared": "^8.1.1", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg=="],
"@vue/devtools-shared": ["@vue/devtools-shared@8.0.7", "", {}, "sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA=="], "@vue/devtools-shared": ["@vue/devtools-shared@8.1.1", "", {}, "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ=="],
"@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.7.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.56.0", "fast-glob": "^3.3.3", "typescript-eslint": "^8.56.0", "vue-eslint-parser": "^10.4.0" }, "peerDependencies": { "eslint": "^9.10.0 || ^10.0.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg=="], "@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.7.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.56.0", "fast-glob": "^3.3.3", "typescript-eslint": "^8.56.0", "vue-eslint-parser": "^10.4.0" }, "peerDependencies": { "eslint": "^9.10.0 || ^10.0.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg=="],
"@vue/language-core": ["@vue/language-core@3.2.5", "", { "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" } }, "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g=="], "@vue/language-core": ["@vue/language-core@3.2.6", "", { "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" } }, "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg=="],
"@vue/reactivity": ["@vue/reactivity@3.6.0-beta.7", "", { "dependencies": { "@vue/shared": "3.6.0-beta.7" } }, "sha512-d1Z7U3QqfrpnT1/m8glRDBGlzQVj4J3u7LvCxieyxvrH03ZcR9qZ8hMw9x/Q3K/VcT2BSHp8A8662cqigCiCEA=="], "@vue/reactivity": ["@vue/reactivity@3.6.0-beta.9", "", { "dependencies": { "@vue/shared": "3.6.0-beta.9" } }, "sha512-Mo0GjZB/q7LUJplPjBdKqAAk4mawkzvnk2nNf9CEetp4RQJP5/e/DdmgjYBsr/+UVX5ShasCalwdyknuuIepdw=="],
"@vue/runtime-core": ["@vue/runtime-core@3.6.0-beta.7", "", { "dependencies": { "@vue/reactivity": "3.6.0-beta.7", "@vue/shared": "3.6.0-beta.7" } }, "sha512-z6wXT1w0lFYS2uHAYuNZRutxNmg5i4S6yKYVf58/DVu+j/L1eq2ntPNVpPJpof71izXbv14ImvvPUCTTqHwAUA=="], "@vue/runtime-core": ["@vue/runtime-core@3.6.0-beta.9", "", { "dependencies": { "@vue/reactivity": "3.6.0-beta.9", "@vue/shared": "3.6.0-beta.9" } }, "sha512-IPbO/cmw0BSHec17fSE8Mbtv0UFBiiiGhvlUGiJWTa/eOlbiO+mZ9/PRRUQ+v/tkKuwFSzKye7KOKj5aFO1dgQ=="],
"@vue/runtime-dom": ["@vue/runtime-dom@3.6.0-beta.7", "", { "dependencies": { "@vue/reactivity": "3.6.0-beta.7", "@vue/runtime-core": "3.6.0-beta.7", "@vue/shared": "3.6.0-beta.7", "csstype": "^3.2.3" } }, "sha512-purk/iT3R5y2CizeV+YdxHhqPE1yAk3MSptggsCdO0zkShl2NA1/xec1WgVg1OL6IV0xNrknsMxl9Fit4g44hA=="], "@vue/runtime-dom": ["@vue/runtime-dom@3.6.0-beta.9", "", { "dependencies": { "@vue/reactivity": "3.6.0-beta.9", "@vue/runtime-core": "3.6.0-beta.9", "@vue/shared": "3.6.0-beta.9", "csstype": "^3.2.3" } }, "sha512-+WV0V1s/uf9iFwZt41lOtJHbIqKyYdEO+nmW0UOnQLjG9ELz6rKuZ92fEim6NPcKCsucv2BoB1mK9T82khYqgg=="],
"@vue/runtime-vapor": ["@vue/runtime-vapor@3.6.0-beta.7", "", { "dependencies": { "@vue/reactivity": "3.6.0-beta.7", "@vue/shared": "3.6.0-beta.7" }, "peerDependencies": { "@vue/runtime-dom": "3.6.0-beta.7" } }, "sha512-H+XdbkqtBI+9gsS957Om94qpTx+keklzvO6rllls4TcKlRo/xcXsDnMhWjghX0Irg6iM/MgBBK8bQkfUkF+xSg=="], "@vue/runtime-vapor": ["@vue/runtime-vapor@3.6.0-beta.9", "", { "dependencies": { "@vue/reactivity": "3.6.0-beta.9", "@vue/shared": "3.6.0-beta.9" }, "peerDependencies": { "@vue/runtime-dom": "3.6.0-beta.9" } }, "sha512-UlAziGF77byUa84RgkIdCT4bleZbB4UvQvmxdGHFfscGylSABBZHAMR8WJIwBsy1MsZnlpRSSeDAYB20RicrhQ=="],
"@vue/server-renderer": ["@vue/server-renderer@3.6.0-beta.7", "", { "dependencies": { "@vue/compiler-ssr": "3.6.0-beta.7", "@vue/shared": "3.6.0-beta.7" }, "peerDependencies": { "vue": "3.6.0-beta.7" } }, "sha512-VqX6+8+NyCC/nqkOvj/vKHeMtBr3Mxo7rccDLNyuSb6o5Mp+itlxdpaD0LD28SqHEaDjqfIUBTnFsfDJLN/cww=="], "@vue/server-renderer": ["@vue/server-renderer@3.6.0-beta.9", "", { "dependencies": { "@vue/compiler-ssr": "3.6.0-beta.9", "@vue/shared": "3.6.0-beta.9" }, "peerDependencies": { "vue": "3.6.0-beta.9" } }, "sha512-qyz37g6pmwHvtXbe7CWKm5+VTmCs4mHrC1uDw4OVZlvwDDZb9pWMzW0kOrzhj1iI6sZ4D9k5fYQIdzBqB7aOqw=="],
"@vue/shared": ["@vue/shared@3.6.0-beta.7", "", {}, "sha512-BNC0ohwv5hYybiUvZ/7gBjSzWFRxpZ2MzRQ/lH2zYZds085ylmAqMGEwbGlWFD+/WPRBsXQd7+e1hNGFksNuCg=="], "@vue/shared": ["@vue/shared@3.6.0-beta.9", "", {}, "sha512-nY6DXeelJsmveeNk5YZYGd8q0ulWXACL8Qs/qvkPC4HQkyhPgf+Ja00AkG2hYsEI8Uixia2UpQl4CbObzw0hag=="],
"@vue/tsconfig": ["@vue/tsconfig@0.8.1", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g=="], "@vue/tsconfig": ["@vue/tsconfig@0.8.1", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g=="],
@@ -580,23 +580,23 @@
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.13", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw=="],
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], "c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
"caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001784", "", {}, "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw=="],
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
@@ -646,7 +646,7 @@
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], "electron-to-chromium": ["electron-to-chromium@1.5.330", "", {}, "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA=="],
"embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
@@ -666,7 +666,7 @@
"embla-carousel-wheel-gestures": ["embla-carousel-wheel-gestures@8.1.0", "", { "dependencies": { "wheel-gestures": "^2.2.5" }, "peerDependencies": { "embla-carousel": "^8.0.0 || ~8.0.0-rc03" } }, "sha512-J68jkYrxbWDmXOm2n2YHl+uMEXzkGSKjWmjaEgL9xVvPb3HqVmg6rJSKfI3sqIDVvm7mkeTy87wtG/5263XqHQ=="], "embla-carousel-wheel-gestures": ["embla-carousel-wheel-gestures@8.1.0", "", { "dependencies": { "wheel-gestures": "^2.2.5" }, "peerDependencies": { "embla-carousel": "^8.0.0 || ~8.0.0-rc03" } }, "sha512-J68jkYrxbWDmXOm2n2YHl+uMEXzkGSKjWmjaEgL9xVvPb3HqVmg6rJSKfI3sqIDVvm7mkeTy87wtG/5263XqHQ=="],
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
@@ -674,13 +674,13 @@
"errx": ["errx@0.1.0", "", {}, "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q=="], "errx": ["errx@0.1.0", "", {}, "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q=="],
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.0.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.1.1", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ=="], "eslint": ["eslint@10.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.3", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA=="],
"eslint-plugin-vue": ["eslint-plugin-vue@10.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^7.1.0", "semver": "^7.6.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "vue-eslint-parser": "^10.0.0" }, "optionalPeers": ["@stylistic/eslint-plugin", "@typescript-eslint/parser"] }, "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA=="], "eslint-plugin-vue": ["eslint-plugin-vue@10.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^7.1.0", "semver": "^7.6.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "vue-eslint-parser": "^10.0.0" }, "optionalPeers": ["@stylistic/eslint-plugin", "@typescript-eslint/parser"] }, "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA=="],
@@ -724,7 +724,7 @@
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="], "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
"fontaine": ["fontaine@0.8.0", "", { "dependencies": { "@capsizecss/unpack": "^4.0.0", "css-tree": "^3.1.0", "magic-regexp": "^0.10.0", "magic-string": "^0.30.21", "pathe": "^2.0.3", "ufo": "^1.6.1", "unplugin": "^2.3.10" } }, "sha512-eek1GbzOdWIj9FyQH/emqW1aEdfC3lYRCHepzwlFCm5T77fBSRSyNRKE6/antF1/B1M+SfJXVRQTY9GAr7lnDg=="], "fontaine": ["fontaine@0.8.0", "", { "dependencies": { "@capsizecss/unpack": "^4.0.0", "css-tree": "^3.1.0", "magic-regexp": "^0.10.0", "magic-string": "^0.30.21", "pathe": "^2.0.3", "ufo": "^1.6.1", "unplugin": "^2.3.10" } }, "sha512-eek1GbzOdWIj9FyQH/emqW1aEdfC3lYRCHepzwlFCm5T77fBSRSyNRKE6/antF1/B1M+SfJXVRQTY9GAr7lnDg=="],
@@ -732,7 +732,7 @@
"fontless": ["fontless@0.2.1", "", { "dependencies": { "consola": "^3.4.2", "css-tree": "^3.1.0", "defu": "^6.1.4", "esbuild": "^0.27.0", "fontaine": "0.8.0", "jiti": "^2.6.1", "lightningcss": "^1.30.2", "magic-string": "^0.30.21", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1", "unifont": "^0.7.4", "unstorage": "^1.17.1" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-mUWZ8w91/mw2KEcZ6gHNoNNmsAq9Wiw2IypIux5lM03nhXm+WSloXGUNuRETNTLqZexMgpt7Aj/v63qqrsWraQ=="], "fontless": ["fontless@0.2.1", "", { "dependencies": { "consola": "^3.4.2", "css-tree": "^3.1.0", "defu": "^6.1.4", "esbuild": "^0.27.0", "fontaine": "0.8.0", "jiti": "^2.6.1", "lightningcss": "^1.30.2", "magic-string": "^0.30.21", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1", "unifont": "^0.7.4", "unstorage": "^1.17.1" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-mUWZ8w91/mw2KEcZ6gHNoNNmsAq9Wiw2IypIux5lM03nhXm+WSloXGUNuRETNTLqZexMgpt7Aj/v63qqrsWraQ=="],
"framer-motion": ["framer-motion@12.35.2", "", { "dependencies": { "motion-dom": "^12.35.2", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA=="], "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -748,11 +748,11 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"h3": ["h3@1.15.6", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ=="], "h3": ["h3@1.15.10", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg=="],
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="],
"human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
@@ -842,7 +842,7 @@
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
"magic-regexp": ["magic-regexp@0.10.0", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.12", "mlly": "^1.7.2", "regexp-tree": "^0.1.27", "type-level-regexp": "~0.1.17", "ufo": "^1.5.4", "unplugin": "^2.0.0" } }, "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg=="], "magic-regexp": ["magic-regexp@0.10.0", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.12", "mlly": "^1.7.2", "regexp-tree": "^0.1.27", "type-level-regexp": "~0.1.17", "ufo": "^1.5.4", "unplugin": "^2.0.0" } }, "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg=="],
@@ -852,7 +852,7 @@
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
"marked": ["marked@17.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ=="], "marked": ["marked@17.0.5", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg=="],
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
@@ -868,17 +868,17 @@
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="], "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
"motion-dom": ["motion-dom@12.35.2", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-pWXFMTwvGDbx1Fe9YL5HZebv2NhvGBzRtiNUv58aoK7+XrsuaydQ0JGRKK2r+bTKlwgSWwWxHbP5249Qr/BNpg=="], "motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
"motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="], "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
"motion-v": ["motion-v@1.10.3", "", { "dependencies": { "framer-motion": "^12.25.0", "hey-listen": "^1.0.8", "motion-dom": "^12.23.23" }, "peerDependencies": { "@vueuse/core": ">=10.0.0", "vue": ">=3.0.0" } }, "sha512-9Ewo/wwGv7FO3PqYJpllBF/Efc7tbeM1iinVrM73s0RUQrnXHwMZCaRX98u4lu0PQCrZghPPfCsQ14pWKIEbnQ=="], "motion-v": ["motion-v@2.2.0", "", { "dependencies": { "framer-motion": "^12.38.0", "hey-listen": "^1.0.8", "motion-dom": "^12.38.0", "motion-utils": "^12.36.0" }, "peerDependencies": { "@vueuse/core": ">=10.0.0", "vue": ">=3.0.0" } }, "sha512-8rrUBt9UMwy45MfLMwHer9J7xBY/rL31S+0U3ZUAouqjH3AOVHvS0NGuXwF4mfWfvb22rfZEv59ZsBc2f1epxQ=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
@@ -942,7 +942,7 @@
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
@@ -990,9 +990,9 @@
"prosemirror-trailing-node": ["prosemirror-trailing-node@3.0.0", "", { "dependencies": { "@remirror/core-constants": "3.0.0", "escape-string-regexp": "^4.0.0" }, "peerDependencies": { "prosemirror-model": "^1.22.1", "prosemirror-state": "^1.4.2", "prosemirror-view": "^1.33.8" } }, "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ=="], "prosemirror-trailing-node": ["prosemirror-trailing-node@3.0.0", "", { "dependencies": { "@remirror/core-constants": "3.0.0", "escape-string-regexp": "^4.0.0" }, "peerDependencies": { "prosemirror-model": "^1.22.1", "prosemirror-state": "^1.4.2", "prosemirror-view": "^1.33.8" } }, "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ=="],
"prosemirror-transform": ["prosemirror-transform@1.11.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw=="], "prosemirror-transform": ["prosemirror-transform@1.12.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="],
"prosemirror-view": ["prosemirror-view@1.41.6", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg=="], "prosemirror-view": ["prosemirror-view@1.41.7", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -1012,7 +1012,7 @@
"regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="],
"reka-ui": ["reka-ui@2.8.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.2.0" } }, "sha512-8lTKcJhmG+D3UyJxhBnNnW/720sLzm0pbA9AC1MWazmJ5YchJAyTSl+O00xP/kxBmEN0fw5JqWVHguiFmsGjzA=="], "reka-ui": ["reka-ui@2.9.3", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.4.0" } }, "sha512-C9lCVxsSC7uYD0Nbgik1+14FNndHNprZmf0zGQt0ZDYIt5KxXV3zD0hEqNcfRUsEEJvVmoRsUkJnASBVBeaaUw=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
@@ -1056,13 +1056,13 @@
"tailwind-variants": ["tailwind-variants@3.2.2", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="], "tailwind-variants": ["tailwind-variants@3.2.2", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="],
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@@ -1070,7 +1070,7 @@
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -1080,7 +1080,7 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="], "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
@@ -1104,9 +1104,9 @@
"unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="], "unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="],
"unplugin-vue-components": ["unplugin-vue-components@31.0.0", "", { "dependencies": { "chokidar": "^5.0.0", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "obug": "^2.1.1", "picomatch": "^4.0.3", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "@nuxt/kit": "^3.2.2 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@nuxt/kit"] }, "sha512-4ULwfTZTLuWJ7+S9P7TrcStYLsSRkk6vy2jt/WTfgUEUb0nW9//xxmrfhyHUEVpZ2UKRRwfRb8Yy15PDbVZf+Q=="], "unplugin-vue-components": ["unplugin-vue-components@31.1.0", "", { "dependencies": { "chokidar": "^5.0.0", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.2", "obug": "^2.1.1", "picomatch": "^4.0.3", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "@nuxt/kit": "^3.2.2 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@nuxt/kit"] }, "sha512-9EbV5ark21A4BOBt6RJGJXCVD2I1eoxTZL1TAvNgYTokcrFIiuxpufb8owyWn7n+z2x8daz/ltZq6IRRKL3ydQ=="],
"unstorage": ["unstorage@1.17.4", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.5", "lru-cache": "^11.2.0", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw=="], "unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="],
"untyped": ["untyped@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "defu": "^6.1.4", "jiti": "^2.4.2", "knitwork": "^1.2.0", "scule": "^1.3.0" }, "bin": { "untyped": "dist/cli.mjs" } }, "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g=="], "untyped": ["untyped@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "defu": "^6.1.4", "jiti": "^2.4.2", "knitwork": "^1.2.0", "scule": "^1.3.0" }, "bin": { "untyped": "dist/cli.mjs" } }, "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g=="],
@@ -1126,17 +1126,17 @@
"vite-plugin-inspect": ["vite-plugin-inspect@11.3.3", "", { "dependencies": { "ansis": "^4.1.0", "debug": "^4.4.1", "error-stack-parser-es": "^1.0.5", "ohash": "^2.0.11", "open": "^10.2.0", "perfect-debounce": "^2.0.0", "sirv": "^3.0.1", "unplugin-utils": "^0.3.0", "vite-dev-rpc": "^1.1.0" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0" } }, "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA=="], "vite-plugin-inspect": ["vite-plugin-inspect@11.3.3", "", { "dependencies": { "ansis": "^4.1.0", "debug": "^4.4.1", "error-stack-parser-es": "^1.0.5", "ohash": "^2.0.11", "open": "^10.2.0", "perfect-debounce": "^2.0.0", "sirv": "^3.0.1", "unplugin-utils": "^0.3.0", "vite-dev-rpc": "^1.1.0" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0" } }, "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA=="],
"vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@8.0.7", "", { "dependencies": { "@vue/devtools-core": "^8.0.7", "@vue/devtools-kit": "^8.0.7", "@vue/devtools-shared": "^8.0.7", "sirv": "^3.0.2", "vite-plugin-inspect": "^11.3.3", "vite-plugin-vue-inspector": "^5.3.2" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0 || ^8.0.0-0" } }, "sha512-BWj/ykGpqVAJVdPyHmSTUm44buz3jPv+6jnvuFdQSRH0kAgP1cEIE4doHiFyqHXOmuB5EQVR/nh2g9YRiRNs9g=="], "vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@8.1.1", "", { "dependencies": { "@vue/devtools-core": "^8.1.1", "@vue/devtools-kit": "^8.1.1", "@vue/devtools-shared": "^8.1.1", "sirv": "^3.0.2", "vite-plugin-inspect": "^11.3.3", "vite-plugin-vue-inspector": "^5.3.2" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-9qTpOmZ2vHpvlI9hdVXAQ1Ry4I8GcBArU7aPi0qfIaV7fQIXy0L1nb6X4mFY2Gw0dYshHuLbIl0Ulb572SCjsQ=="],
"vite-plugin-vue-inspector": ["vite-plugin-vue-inspector@5.3.2", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.0", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-typescript": "^7.22.15", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-dom": "^3.3.4", "kolorist": "^1.8.0", "magic-string": "^0.30.4" }, "peerDependencies": { "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q=="], "vite-plugin-vue-inspector": ["vite-plugin-vue-inspector@5.4.0", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.0", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-typescript": "^7.22.15", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-dom": "^3.3.4", "kolorist": "^1.8.0", "magic-string": "^0.30.4" }, "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-Iq/024CydcE46FZqWPU4t4lw4uYOdLnFSO1RNxJVt2qY9zxIjmnkBqhHnYaReWM82kmNnaXs7OkfgRrV2GEjyw=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"vue": ["vue@3.6.0-beta.7", "", { "dependencies": { "@vue/compiler-dom": "3.6.0-beta.7", "@vue/compiler-sfc": "3.6.0-beta.7", "@vue/runtime-dom": "3.6.0-beta.7", "@vue/runtime-vapor": "3.6.0-beta.7", "@vue/server-renderer": "3.6.0-beta.7", "@vue/shared": "3.6.0-beta.7" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-SlNdNG+c37xycRFIJ/KB9CmpZmWwKovbDTNl00ck0+ZSNZSgb+n6DKLuXtVvGZImNPu6jj+zzaM1gqNB5NeC6Q=="], "vue": ["vue@3.6.0-beta.9", "", { "dependencies": { "@vue/compiler-dom": "3.6.0-beta.9", "@vue/compiler-sfc": "3.6.0-beta.9", "@vue/runtime-dom": "3.6.0-beta.9", "@vue/runtime-vapor": "3.6.0-beta.9", "@vue/server-renderer": "3.6.0-beta.9", "@vue/shared": "3.6.0-beta.9" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-K/+HT52OAZvaNDDMvT+Lfk36e/97PsyJyDWXM0FDfLQKGbEDXsd4pSQz+BJViGssLcltQLSTlSqQlZ6fkGo5CA=="],
"vue-chartjs": ["vue-chartjs@5.3.3", "", { "peerDependencies": { "chart.js": "^4.1.1", "vue": "^3.0.0-0 || ^2.7.0" } }, "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA=="], "vue-chartjs": ["vue-chartjs@5.3.3", "", { "peerDependencies": { "chart.js": "^4.1.1", "vue": "^3.0.0-0 || ^2.7.0" } }, "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA=="],
"vue-component-type-helpers": ["vue-component-type-helpers@3.2.5", "", {}, "sha512-tkvNr+bU8+xD/onAThIe7CHFvOJ/BO6XCOrxMzeytJq40nTfpGDJuVjyCM8ccGZKfAbGk2YfuZyDMXM56qheZQ=="], "vue-component-type-helpers": ["vue-component-type-helpers@3.2.6", "", {}, "sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ=="],
"vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="], "vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
@@ -1144,9 +1144,9 @@
"vue-i18n": ["vue-i18n@11.3.0", "", { "dependencies": { "@intlify/core-base": "11.3.0", "@intlify/devtools-types": "11.3.0", "@intlify/shared": "11.3.0", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA=="], "vue-i18n": ["vue-i18n@11.3.0", "", { "dependencies": { "@intlify/core-base": "11.3.0", "@intlify/devtools-types": "11.3.0", "@intlify/shared": "11.3.0", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA=="],
"vue-router": ["vue-router@5.0.3", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw=="], "vue-router": ["vue-router@5.0.4", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg=="],
"vue-tsc": ["vue-tsc@3.2.5", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.2.5" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA=="], "vue-tsc": ["vue-tsc@3.2.6", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.2.6" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
@@ -1166,9 +1166,9 @@
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
"yjs": ["yjs@13.6.29", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ=="], "yjs": ["yjs@13.6.30", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
@@ -1184,21 +1184,23 @@
"@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@nuxtjs/color-mode/@nuxt/kit": ["@nuxt/kit@3.21.1", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-QORZRjcuTKgo++XP1Pc2c2gqwRydkaExrIRfRI9vFsPA3AzuHVn5Gfmbv1ic8y34e78mr5DMBvJlelUaeOuajg=="], "@nuxt/schema/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
"@nuxt/ui/reka-ui": ["reka-ui@2.9.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.4.0" } }, "sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw=="],
"@nuxtjs/color-mode/@nuxt/kit": ["@nuxt/kit@3.21.2", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "mlly": "^1.8.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-Bd6m6mrDrqpBEbX+g0rc66/ALd1sxlgdx5nfK9MAYO0yKLTOSK7McSYz1KcOYn3LQFCXOWfvXwaqih/b+REI1g=="],
"@nuxtjs/color-mode/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "@nuxtjs/color-mode/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"@nuxtjs/color-mode/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "@nuxtjs/color-mode/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"@tailwindcss/node/lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
@@ -1206,11 +1208,11 @@
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@unhead/vue/hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
"@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="], "@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"c12/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "c12/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
@@ -1226,7 +1228,7 @@
"markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
@@ -1242,8 +1244,6 @@
"unctx/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], "unctx/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
"unhead/hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
"unimport/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "unimport/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"unimport/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "unimport/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
@@ -1256,9 +1256,11 @@
"vaul-vue/@vueuse/core": ["@vueuse/core@10.11.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "10.11.1", "@vueuse/shared": "10.11.1", "vue-demi": ">=0.14.8" } }, "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww=="], "vaul-vue/@vueuse/core": ["@vueuse/core@10.11.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "10.11.1", "@vueuse/shared": "10.11.1", "vue-demi": ">=0.14.8" } }, "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww=="],
"vaul-vue/reka-ui": ["reka-ui@2.9.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.4.0" } }, "sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw=="],
"vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], "vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"vue-router/@vue/devtools-api": ["@vue/devtools-api@8.0.7", "", { "dependencies": { "@vue/devtools-kit": "^8.0.7" } }, "sha512-tc1TXAxclsn55JblLkFVcIRG7MeSJC4fWsPjfM7qu/IcmPUYnQ5Q8vzWwBpyDY24ZjmZTUCCwjRSNbx58IhlAA=="], "vue-router/@vue/devtools-api": ["@vue/devtools-api@8.1.1", "", { "dependencies": { "@vue/devtools-kit": "^8.1.1" } }, "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw=="],
"@nuxtjs/color-mode/@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@nuxtjs/color-mode/@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
@@ -1270,30 +1272,10 @@
"@nuxtjs/color-mode/pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "@nuxtjs/color-mode/pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
"@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
"@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
"@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
"@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
"@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
"@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="], "@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
"@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"@vue/devtools-api/@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], "@vue/devtools-api/@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@@ -1305,5 +1287,7 @@
"vaul-vue/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@10.11.1", "", {}, "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw=="], "vaul-vue/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@10.11.1", "", {}, "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw=="],
"vaul-vue/@vueuse/core/@vueuse/shared": ["@vueuse/shared@10.11.1", "", { "dependencies": { "vue-demi": ">=0.14.8" } }, "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA=="], "vaul-vue/@vueuse/core/@vueuse/shared": ["@vueuse/shared@10.11.1", "", { "dependencies": { "vue-demi": ">=0.14.8" } }, "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA=="],
"vaul-vue/reka-ui/@vueuse/core": ["@vueuse/core@14.2.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.1", "@vueuse/shared": "14.2.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ=="],
} }
} }

23
bo/components.d.ts vendored
View File

@@ -12,12 +12,15 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default'] CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default'] CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default']
copy: typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
CountryCurrencySwitch: typeof import('./src/components/inner/CountryCurrencySwitch.vue')['default']
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default'] Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default'] Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default'] En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default'] En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default'] FavoriteProducts: typeof import('./src/components/admin/FavoriteProducts.vue')['default']
LangSwitch: typeof import('./src/components/inner/LangSwitch.vue')['default']
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default'] PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default'] PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
PageOrders: typeof import('./src/components/customer/PageOrders.vue')['default'] PageOrders: typeof import('./src/components/customer/PageOrders.vue')['default']
@@ -30,17 +33,25 @@ declare module 'vue' {
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default'] Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default'] ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default'] ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default']
'ProductDetailView copy': typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
ProductEditor: typeof import('./src/components/inner/ProductEditor.vue')['default']
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default'] ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
Profile: typeof import('./src/components/customer-management/Profile.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
ThemeSwitch: typeof import('./src/components/inner/themeSwitch.vue')['default'] StorageFileBrowser: typeof import('./src/components/customer/StorageFileBrowser.vue')['default']
ThemeSwitch: typeof import('./src/components/inner/ThemeSwitch.vue')['default']
TopBar: typeof import('./src/components/TopBar.vue')['default'] TopBar: typeof import('./src/components/TopBar.vue')['default']
TopBarLogin: typeof import('./src/components/TopBarLogin.vue')['default'] TopBarLogin: typeof import('./src/components/TopBarLogin.vue')['default']
UAlert: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default'] UAlert: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
UApp: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default']
UButton: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default'] UButton: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
UCard: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default'] UCard: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
UCheckbox: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default'] UCheckbox: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
UDrawer: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default'] UDrawer: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
UDropdownMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default']
UEditor: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Editor.vue')['default']
UEditorToolbar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/EditorToolbar.vue')['default']
UForm: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Form.vue')['default'] UForm: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Form.vue')['default']
UFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default'] UFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
UIcon: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default'] UIcon: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
@@ -51,6 +62,12 @@ declare module 'vue' {
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default'] UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default'] USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default'] USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
UsersList: typeof import('./src/components/admin/UsersList.vue')['default']
UsersSearch: typeof import('./src/components/admin/UsersSearch.vue')['default']
USidebar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Sidebar.vue')['default']
UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default'] UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
UTabs: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
UTextarea: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
UTree: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tree.vue')['default']
} }
} }

View File

@@ -13,21 +13,22 @@
"format": "prettier --write --experimental-cli src/" "format": "prettier --write --experimental-cli src/"
}, },
"dependencies": { "dependencies": {
"@nuxt/ui": "^4.5.0", "@nuxt/ui": "^4.6.0",
"@tailwindcss/vite": "^4.2.0", "@tailwindcss/vite": "^4.2.2",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"tailwindcss": "^4.2.0", "reka-ui": "^2.9.3",
"tailwindcss": "^4.2.2",
"vue": "beta", "vue": "beta",
"vue-chartjs": "^5.3.3", "vue-chartjs": "^5.3.3",
"vue-i18n": "11", "vue-i18n": "^11.3.0",
"vue-router": "^5.0.2" "vue-router": "^5.0.4"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node24": "^24.0.4", "@tsconfig/node24": "^24.0.4",
"@types/node": "^24.10.13", "@types/node": "^24.12.0",
"@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue": "^6.0.5",
"@vue/eslint-config-typescript": "^14.6.0", "@vue/eslint-config-typescript": "^14.7.0",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
@@ -35,8 +36,8 @@
"prettier": "3.8.1", "prettier": "3.8.1",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "beta", "vite": "beta",
"vite-plugin-vue-devtools": "^8.0.6", "vite-plugin-vue-devtools": "^8.1.1",
"vue-tsc": "^3.2.4" "vue-tsc": "^3.2.6"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"

View File

@@ -1,9 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterView } from 'vue-router' import { computed } from 'vue'
import { TooltipProvider } from 'reka-ui'
import { RouterView, useRoute } from 'vue-router'
import DefaultLayout from '@/layouts/default.vue'
import EmptyLayout from '@/layouts/empty.vue'
import ManagementLayout from '@/layouts/management.vue'
import { useAuthStore } from './stores/customer/auth'
const authStore = useAuthStore()
const route = useRoute()
const layout = computed(() => (route.meta.layout as string) || 'default')
</script> </script>
<template> <template>
<Suspense> <Suspense>
<RouterView /> <TooltipProvider>
<component :is="layout === 'empty' ? EmptyLayout : layout === 'management' ? ManagementLayout : DefaultLayout">
<RouterView v-slot="{ Component }">
<component :is="Component" />
</RouterView>
</component>
</TooltipProvider>
</Suspense> </Suspense>
</template> </template>

View File

@@ -8,9 +8,6 @@ export const uiOptions: NuxtUIOptions = {
} }
}, },
button: { button: {
slots: {
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
},
}, },
input: { input: {
slots: { slots: {
@@ -40,16 +37,17 @@ export const uiOptions: NuxtUIOptions = {
}, },
selectMenu: { selectMenu: {
slots: { slots: {
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!', // base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! z-80 text-(--black)! dark:text-white!', // content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! z-80',
itemLeadingIcon: 'text-(--black)! dark:text-white!' // content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! z-80 text-(--black)! dark:text-white!',
// itemLeadingIcon: 'text-(--black)! dark:text-white!'
} }
}, },
table: { table: {
slots: { slots: {
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)', base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
tr: 'border-b! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! text-(--black)! dark:text-white!', // tr: 'border-b! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! text-(--black)! dark:text-white!',
} }
}, },
@@ -58,6 +56,11 @@ export const uiOptions: NuxtUIOptions = {
content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)', content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
} }
},
textarea: {
slots: {
base: 'ring-0! outline-0! rounded-md border border-(--border-light)! dark:border-(--border-dark)! text-base!'
}
} }
} }
} }

1
bo/src/assets/error.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path stroke="#5b5b5b" stroke-width="2" d="M3 11c0-3.771 0-5.657 1.172-6.828S7.229 3 11 3h2c3.771 0 5.657 0 6.828 1.172S21 7.229 21 11v2c0 3.771 0 5.657-1.172 6.828S16.771 21 13 21h-2c-3.771 0-5.657 0-6.828-1.172S3 16.771 3 13z"/><path fill="#5b5b5b" fill-rule="evenodd" d="m19 13.585l-.02-.02c-.39-.39-.726-.726-1.026-.979c-.317-.267-.662-.502-1.088-.63a3 3 0 0 0-1.732 0c-.426.129-.77.363-1.088.63c-.3.253-.636.59-1.025.98l-.029.028c-.307.306-.487.486-.628.601l-.02.017l-.012-.023c-.087-.16-.189-.393-.36-.792l-.053-.124l-.022-.052c-.356-.83-.655-1.528-.95-2.054c-.305-.541-.685-1.05-1.277-1.346a3 3 0 0 0-1.597-.307c-.66.055-1.201.386-1.685.775c-.406.327-.86.77-1.388 1.297V13q0 .774.003 1.411l1.114-1.114c.69-.69 1.15-1.147 1.525-1.45c.37-.298.53-.335.6-.34a1 1 0 0 1 .532.102c.061.031.196.124.43.54c.236.42.493 1.016.877 1.912l.053.124l.017.038c.149.348.287.67.425.924c.145.265.355.583.709.802a2 2 0 0 0 1.398.27c.41-.073.723-.29.956-.482c.222-.184.47-.432.738-.7l.03-.03c.425-.425.701-.7.928-.891c.218-.184.32-.228.376-.245a1 1 0 0 1 .578 0c.056.017.158.061.376.245c.227.191.503.466.929.892l1.35 1.35c.046-.718.054-1.61.056-2.773" clip-rule="evenodd"/><circle cx="16.5" cy="7.5" r="1.5" fill="#5b5b5b"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -5,6 +5,10 @@ body {
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
} }
li {
margin-left: 20px
}
.inter { .inter {
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
} }
@@ -15,9 +19,8 @@ body {
@theme { @theme {
--main-light: #FFFEFB; --main-light: #FFFEFB;
--second-light: #F5F6FA; --second-light: #F8FAFC;
--main-dark: #1A1A1A;
--main-dark: #212121;
--black: #1A1A1A; --black: #1A1A1A;
/* gray */ /* gray */
@@ -25,13 +28,15 @@ body {
--gray-dark: #6B7280; --gray-dark: #6B7280;
/* borders */ /* borders */
--border-light: #E8E7E0; --border-light: #E2E8F0;
--border-dark: #3F3E3D; --border-dark: #334155;
/* text */ /* text */
--accent-blue-dark: #3B82F6; --accent-blue-dark: #0EA5E91A;
--accent-blue-light:#2563EB; --accent-blue-light: #E0F2FE;
--text-dark: #FFFEFB; --text-dark: #FFFEFB;
--text-sky-light: #0284C7;
--text-sky-dark: #38BDF8;
/* placeholder */ /* placeholder */
--placeholder: #8C8C8A; --placeholder: #8C8C8A;
@@ -40,11 +45,11 @@ body {
--ui-border-accented: var(--border-light); --ui-border-accented: var(--border-light);
--ui-border: var(--border-light); --ui-border: var(--border-light);
--ui-color-neutral-700: var(--black); --ui-color-neutral-700: var(--black);
--ui-primary: var(--accent-blue-light)
} }
.dark { .dark {
--ui-bg: var(--black); --ui-bg: var(--black);
--ui-primary: var(--color-gray-500);
--ui-secondary: var(--accent-green-dark); --ui-secondary: var(--accent-green-dark);
--ui-border-accented: var(--border-dark); --ui-border-accented: var(--border-dark);
--ui-text-dimmed: var(--gray-dark); --ui-text-dimmed: var(--gray-dark);
@@ -53,6 +58,8 @@ body {
--ui-error: var(--dark-red); --ui-error: var(--dark-red);
--border: var(--border-dark); --border: var(--border-dark);
--tw-border-style: var(--border-dark); --tw-border-style: var(--border-dark);
--ui-text-inverted: #fff;
--ui-primary: var(--accent-blue-dark)
} }
.label-form { .label-form {
@@ -71,3 +78,6 @@ body {
@apply text-(--accent-green) dark:text-(--accent-green-dark) font-medium; @apply text-(--accent-green) dark:text-(--accent-green-dark) font-medium;
} }
.blue-button {
@apply bg-info! text-white!
}

View File

@@ -1,13 +1,51 @@
<template>
<header
class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
<div class="px-4">
<div class="flex items-center justify-between h-16">
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
<UIcon name="i-heroicons-clock" class="w-5 h-5" />
</div>
<span class="font-semibold text-gray-900 dark:text-white">{{ settings.app.name }}</span>
</RouterLink>
<UNavigationMenu :type="'trigger'" :ui="{
root: 'justify-center',
list: 'gap-4 text-(--black)',
linkLabel: 'text-(--black) text-sm!'
}" :items="menuItems" class="" />
<!-- orientation="vertical" -->
<div class="flex items-center gap-12">
<div class="flex items-center gap-2">
<CountryCurrencySwitch />
<LangSwitch />
</div>
<div class="flex items-center gap-2">
<ThemeSwitch />
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark) whitespace-nowrap">
{{ $t('general.logout') }}
</button>
</div>
</div>
</div>
</div>
</header>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import LangSwitch from './inner/langSwitch.vue' import LangSwitch from './inner/LangSwitch.vue'
import ThemeSwitch from './inner/themeSwitch.vue' import ThemeSwitch from './inner/ThemeSwitch.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/customer/auth'
import { computed, onMounted, ref } from 'vue' import { computed, ref } from 'vue'
import { currentLang } from '@/router/langs' import { currentLang } from '@/router/langs'
import type { LabelTrans, TopMenuItem } from '@/types' import type { LabelTrans, TopMenuItem } from '@/types'
import type { NavigationMenuItem } from '@nuxt/ui' import type { NavigationMenuItem } from '@nuxt/ui'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import CountryCurrencySwitch from './inner/CountryCurrencySwitch.vue'
import { settings } from '@/router/settings'
const authStore = useAuthStore() const authStore = useAuthStore()
let menu = ref() let menu = ref()
@@ -51,37 +89,3 @@ function transformMenu(items: TopMenuItem[], locale: string | undefined): Naviga
} }
await getTopMenu() await getTopMenu()
</script> </script>
<template>
<header
class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
<!-- px-4 sm:px-6 lg:px-8 -->
<div class="container mx-auto px-4">
<div class="flex items-center justify-between h-14">
<!-- Logo -->
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
<UIcon name="i-heroicons-clock" class="w-5 h-5" />
</div>
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
</RouterLink>
<UNavigationMenu :type="'trigger'" :ui="{
root: 'justify-center',
list: 'gap-4'
}" :items="menuItems" class="w-full"></UNavigationMenu>
<div class="flex items-center gap-2">
<!-- Language Switcher -->
<LangSwitch />
<!-- Theme Switcher -->
<ThemeSwitch />
<!-- Logout Button (only when authenticated) -->
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark)">
{{ $t('general.logout') }}
</button>
</div>
</div>
</div>
</header>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import LangSwitch from './inner/langSwitch.vue' import LangSwitch from './inner/LangSwitch.vue'
import ThemeSwitch from './inner/themeSwitch.vue' import ThemeSwitch from './inner/ThemeSwitch.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/customer/auth'
const authStore = useAuthStore() const authStore = useAuthStore()
</script> </script>

View File

@@ -0,0 +1,9 @@
<template>
<div>
</div>
</template>
<script setup lang="ts">
</script>

View File

@@ -1,24 +1,21 @@
<template> <template>
<suspense> <div class="flex flex-col md:flex-row gap-10">
<component :is="Default || 'div'">
<div class="flex gap-10">
<CategoryMenu /> <CategoryMenu />
<div class="w-full flex flex-col items-center gap-4"> <div class="w-full flex flex-col items-center gap-4">
<UTable :data="productsList" :columns="columns" class="flex-1 w-full" /> <UTable :data="productsList" :columns="columns" class="flex-1 w-full" :ui="{
root: 'max-w-100wv overflow-auto!'
}" />
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" /> <UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
</div> </div>
</div> </div>
</component>
</suspense>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, h, resolveComponent, computed } from 'vue' import { ref, watch, h, resolveComponent, computed } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import Default from '@/layouts/default.vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import CategoryMenu from '../inner/categoryMenu.vue' import CategoryMenu from '../inner/CategoryMenu.vue'
import type { Product } from '@/types/product' import type { Product } from '@/types/product'
const router = useRouter() const router = useRouter()
@@ -141,7 +138,7 @@ async function fetchProductList() {
if (route.params.category_id) if (route.params.category_id)
params.append('category_id', String(route.params.category_id)) params.append('category_id', String(route.params.category_id))
const url = `/api/v1/restricted/list/list-products?elems=${perPage.value}&${params.toString()}` const url = `/api/v1/restricted/product/list?elems=${perPage.value}&${params.toString()}`
try { try {
const response = await useFetchJson<ApiResponse>(url) const response = await useFetchJson<ApiResponse>(url)
productsList.value = response.items || [] productsList.value = response.items || []
@@ -153,10 +150,16 @@ async function fetchProductList() {
} }
} }
function goToProduct(productId: number) { function goToProduct(productId: number, linkRewrite: string) {
let path = {
name: route.name,
params: route.params,
query: route.query
}
localStorage.setItem('back_from_product', JSON.stringify(path))
router.push({ router.push({
name: 'customer-product-details', name: 'admin-product-details',
params: { product_id: productId } params: { product_id: productId, link_rewrite: linkRewrite }
}) })
} }
@@ -173,6 +176,7 @@ const UInput = resolveComponent('UInput')
const UButton = resolveComponent('UButton') const UButton = resolveComponent('UButton')
const UIcon = resolveComponent('UIcon') const UIcon = resolveComponent('UIcon')
import errorImg from '@/assets/error.svg'
const columns: TableColumn<Product>[] = [ const columns: TableColumn<Product>[] = [
{ {
accessorKey: 'product_id', accessorKey: 'product_id',
@@ -209,9 +213,13 @@ const columns: TableColumn<Product>[] = [
cell: ({ row }) => { cell: ({ row }) => {
return h('img', { return h('img', {
src: row.getValue('image_link') as string, src: row.getValue('image_link') as string,
style: 'width:40px;height:40px;object-fit:cover;' style: 'width:40px;height:40px;object-fit:cover;',
}) onError: (e: Event) => {
const target = e.target as HTMLImageElement
target.src = errorImg
} }
})
},
}, },
{ {
accessorKey: 'name', accessorKey: 'name',
@@ -270,10 +278,11 @@ const columns: TableColumn<Product>[] = [
cell: ({ row }) => { cell: ({ row }) => {
return h(UButton, { return h(UButton, {
onClick: () => { onClick: () => {
goToProduct(row.original.product_id) goToProduct(row.original.product_id, row.original.link_rewrite)
}, },
color: 'primary', class: 'cursor-pointer',
variant: 'solid' color: 'info',
variant: 'soft'
}, () => 'Show product') }, () => 'Show product')
}, },
} }

View File

@@ -0,0 +1,523 @@
<template>
<div class="flex items-center gap-2 mb-4">
<UIcon name="line-md:arrow-left" class="text-(--text-sky-light) dark:text-(--text-sky-dark)" />
<p class="cursor-pointer text-(--text-sky-light) dark:text-(--text-sky-dark)" @click="backFromProduct()">
Back to products</p>
</div>
<div class="container mx-auto ">
<div
class="gap-4 mb-6 bg-slate-50 dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
<div class="flex items-center justify-between">
<div class="flex flex-col items-center gap-3" v-if="!isTranslations">
<USelectMenu v-model="toLangId" :items="langs" value-key="id"
class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
<template #default>
<div class="flex flex-col items-start leading-tight">
<span class="text-xs text-gray-400">
Selected language
</span>
<span class="font-medium dark:text-white text-black">
{{langs.find(l => l.id === toLangId)?.name || 'Select language'}}
</span>
</div>
</template>
<template #item-leading="{ item }">
<div class="flex items-center gap-2 cursor-pointer">
<span class="text-lg">{{ item.flag }}</span>
<span class="font-medium dark:text-white text-black">
{{ item.name }}
</span>
</div>
</template>
</USelectMenu>
</div>
<div v-if="toLangId !== settingStore.shopDefaultLanguage && !isTranslations" class="flex gap-7">
<UButton @click="translateToSelectedLanguage" color="info" :loading="translating">
Translate from Polish to {{langs.find(l => l.id === toLangId)?.name}}
</UButton>
</div>
<div v-if="isTranslations" class="flex gap-3 w-full justify-end">
<UButton @click="() => {
toLangId = settingStore.shopDefaultLanguage
isTranslations = false
}" color="info" variant="outline">
Cancel and back to Polish
</UButton>
<UButton color="info" @click="productStore.saveProductDescription(productID, toLangId)">
Save translations
</UButton>
</div>
<!-- <div class="flex gap-7">
<div v-if="isTranslations === false">
<UButton @click="() => {
fetchForLanguage(toLangId)
isShowProductView = true
}" class="text-(--accent-blue-light) dark:text-(--accent-blue-dark)"
color="neutral" variant="outline">
<p class="dark:text-white"> Show product</p>
</UButton>
<UButton @click="translateToSelectedLanguage" color="primary" :loading="translating"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) flex items-center! justify-center! px-12">
Translate
</UButton>
</div>
<div v-else class="flex gap-3">
<UButton @click="() => {
toLangId = settingStore.shopDefaultLanguage
isTranslations = false
}" color="primary" class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark)">
Cancel
</UButton>
<UButton color="primary" @click="productStore.saveProductDescription(productID, toLangId)"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark)">
Save
</UButton>
</div>
</div> -->
</div>
</div>
<!-- <div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
<p class="text-lg font-medium dark:text-white text-black">Translating...</p>
</div>
</div> -->
<div v-if="productStore.loading" class="flex items-center justify-center py-20">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
</div>
<div v-else-if="productStore.error" class="flex items-center justify-center py-20">
<p class="text-red-500">{{ productStore.error }}</p>
</div>
<div v-else-if="productStore.productDescription" class="">
<div class="flex items-start gap-30">
<div class="w-80 h-80 bg-(--second-light) dark:bg-gray-700 rounded-lg flex items-center justify-center">
<span class="text-gray-500 dark:text-gray-400">Product Image</span>
</div>
<div class="flex flex-col gap-2">
<p class="text-[25px] font-bold text-black dark:text-white">
{{ productStore.productDescription.name || 'Product Name' }}
</p>
<p v-html="productStore.productDescription.description_short" class="text-black dark:text-white"></p>
<div class="space-y-[10px]">
<div class="flex items-center gap-1">
<UIcon name="lets-icons:done-ring-round-fill"
class="text-[20px] light:text-(--accent-green-light) dark:text-(--accent-green-dark)" />
<p class="text-[16px] font-bold text-(--accent-blue-light) dark:text-(--accent-blue-dark)">
{{ productStore.productDescription.available_now }}
</p>
</div>
<div class="flex items-center gap-1">
<UIcon name="marketeq:car-shipping"
class="text-[25px] light:text-(--accent-green-light) dark:text-(--accent-green-dark)" />
<p class="text-[18px] font-bold text-black dark:text-white">
{{ productStore.productDescription.delivery_in_stock || 'Delivery information' }}
</p>
</div>
</div>
</div>
</div>
<UTabs :items="items" v-model="activeTab" color="info" :content="true" :ui="{
list : 'w-auto!'
}">
<template #description>
<div v-if="!isTranslations" class="flex justify-end items-center gap-3 mb-4">
<UButton v-if="!isEditing" @click="activeTab === 'usage' ? enableEdit() : enableDescriptionEdit()"
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
<p class="text-white">Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
</UButton>
<UButton v-if="isEditing" @click="activeTab === 'usage' ? saveText : saveDescription" color="neutral"
variant="outline" class="p-2.5 cursor-pointer">
<p class="dark:text-white text-black">Save the edited text</p>
</UButton>
<UButton v-if="isEditing" @click="activeTab === 'usage' ? cancelEdit : cancelDescriptionEdit"
color="neutral" variant="outline" class="p-2.5 cursor-pointer">
Cancel
</UButton>
</div>
<UEditor v-if="isTranslations" v-slot="{ editor }" v-model="productStore.productDescription.description"
content-type="html" :ui="{ base: 'p-8 sm:px-16' }" class="w-full min-h-74" placeholder="Write there ...">
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
<template #link>
<EditorLinkPopover :editor="editor" auto-open />
</template>
</UEditorToolbar>
</UEditor>
<p v-else v-html="productStore.productDescription.description" class="text-black dark:text-white"></p>
</template>
<template #usage>
<div class="px-7">
<div class="flex justify-end items-center gap-3 mb-4">
<UButton v-if="!isEditing" @click="enableEdit"
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
<p class="text-white">Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
</UButton>
<UButton v-if="isEditing" @click="saveText" color="neutral" variant="outline"
class="p-2.5 cursor-pointer">
<p class="dark:text-white text-black">Save the edited text</p>
</UButton>
<UButton v-if="isEditing" @click="cancelEdit" color="neutral" variant="outline"
class="p-2.5 cursor-pointer">
Cancel
</UButton>
</div>
<UEditor v-if="isTranslations" v-slot="{ editor }" v-model="productStore.productDescription.usage"
content-type="html" :ui="{ base: 'p-8 sm:px-16' }" class="w-full min-h-74" placeholder="Write there ...">
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
<template #link>
<EditorLinkPopover :editor="editor" auto-open />
</template>
</UEditorToolbar>
</UEditor>
<p v-else v-html="productStore.productDescription.usage" class="text-black dark:text-white"></p>
</div>
</template>
</UTabs>
</div>
</div>
</template>
<script setup lang="ts">
import { useEditable } from '@/composable/useConteditable';
import { langs } from '@/router/langs';
import { useProductStore } from '@/stores/product';
import { useSettingsStore } from '@/stores/admin/settings';
import type { EditorToolbarItem } from '@nuxt/ui';
import { onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const toolbarItems: EditorToolbarItem[][] = [
// History controls
[{
kind: 'undo',
icon: 'i-lucide-undo',
tooltip: { text: 'Undo' }
}, {
kind: 'redo',
icon: 'i-lucide-redo',
tooltip: { text: 'Redo' }
}],
// Block types
[{
icon: 'i-lucide-heading',
tooltip: { text: 'Headings' },
content: {
align: 'start'
},
items: [{
kind: 'heading',
level: 1,
icon: 'i-lucide-heading-1',
label: 'Heading 1'
}, {
kind: 'heading',
level: 2,
icon: 'i-lucide-heading-2',
label: 'Heading 2'
}, {
kind: 'heading',
level: 3,
icon: 'i-lucide-heading-3',
label: 'Heading 3'
}, {
kind: 'heading',
level: 4,
icon: 'i-lucide-heading-4',
label: 'Heading 4'
}]
}, {
icon: 'i-lucide-list',
tooltip: { text: 'Lists' },
content: {
align: 'start'
},
items: [{
kind: 'bulletList',
icon: 'i-lucide-list',
label: 'Bullet List'
}, {
kind: 'orderedList',
icon: 'i-lucide-list-ordered',
label: 'Ordered List'
}]
}, {
kind: 'blockquote',
icon: 'i-lucide-text-quote',
tooltip: { text: 'Blockquote' }
}, {
kind: 'codeBlock',
icon: 'i-lucide-square-code',
tooltip: { text: 'Code Block' }
}, {
kind: 'horizontalRule',
icon: 'i-lucide-separator-horizontal',
tooltip: { text: 'Horizontal Rule' }
}],
// Text formatting
[{
kind: 'mark',
mark: 'bold',
icon: 'i-lucide-bold',
tooltip: { text: 'Bold' }
}, {
kind: 'mark',
mark: 'italic',
icon: 'i-lucide-italic',
tooltip: { text: 'Italic' }
}, {
kind: 'mark',
mark: 'underline',
icon: 'i-lucide-underline',
tooltip: { text: 'Underline' }
}, {
kind: 'mark',
mark: 'strike',
icon: 'i-lucide-strikethrough',
tooltip: { text: 'Strikethrough' }
}, {
kind: 'mark',
mark: 'code',
icon: 'i-lucide-code',
tooltip: { text: 'Code' }
}],
// Link
[{
kind: 'link',
icon: 'i-lucide-link',
tooltip: { text: 'Link' }
}],
// Text alignment
[{
icon: 'i-lucide-align-justify',
tooltip: { text: 'Text Align' },
content: {
align: 'end'
},
items: [{
kind: 'textAlign',
align: 'left',
icon: 'i-lucide-align-left',
label: 'Align Left'
}, {
kind: 'textAlign',
align: 'center',
icon: 'i-lucide-align-center',
label: 'Align Center'
}, {
kind: 'textAlign',
align: 'right',
icon: 'i-lucide-align-right',
label: 'Align Right'
}, {
kind: 'textAlign',
align: 'justify',
icon: 'i-lucide-align-justify',
label: 'Align Justify'
}]
}]
]
const router = useRouter()
function backFromProduct() {
let path = localStorage.getItem('back_from_product')
if (path) {
let res = JSON.parse(path)
router.push({
name: res.name,
params: res.params,
query: res.query
})
localStorage.removeItem('back_from_product')
} else {
router.push({
name: 'customer-products',
})
}
}
const route = useRoute()
const settingStore = useSettingsStore()
const productStore = useProductStore()
const isTranslations = ref(false)
const toLangId = ref<number>(settingStore.shopDefaultLanguage)
const productID = ref<number>(0)
const translating = ref(false)
const activeTab = ref('description')
const usageRef = ref<HTMLElement | null>(null)
const originalUsage = ref('')
const isEditing = ref(false)
const usageEdit = useEditable(usageRef)
const descriptionRef = ref<HTMLElement | null>(null)
const descriptionEdit = useEditable(descriptionRef)
const originalDescription = ref('')
const translateToSelectedLanguage = async () => {
if (toLangId.value && productID.value) {
translating.value = true
try {
await productStore.translateProductDescription(productID.value, toLangId.value)
} finally {
translating.value = false
enableEdit()
enableDescriptionEdit()
}
}
if (productStore.productDescription) {
isTranslations.value = true
}
}
const fetchForLanguage = async (langId: number | null) => {
if (productID.value) {
await productStore.getProductDescription(langId, productID.value)
removeInlineStylesFromAll(productStore.productDescription)
}
}
watch(toLangId, () => {
fetchForLanguage(toLangId.value)
})
onMounted(async () => {
const id = route.params.product_id
if (id) {
productID.value = Number(id)
await fetchForLanguage(toLangId.value)
}
})
const items = ref([
{ label: 'Description', slot: 'description', name: 'description' },
{ label: 'Usage', slot: 'usage', name: 'usage' }
])
// text edit
const enableEdit = () => {
console.log(usageRef.value, 'usageRef');
if (usageRef.value) {
originalUsage.value = usageRef.value.innerHTML
}
isEditing.value = true
usageEdit.enableEdit()
}
const enableDescriptionEdit = () => {
console.log(usageRef.value, 'usageRef');
if (descriptionRef.value) {
originalDescription.value = descriptionRef.value.innerHTML
}
descriptionEdit.enableEdit()
}
const saveText = () => {
if (usageRef.value) {
productStore.productDescription.usage = usageRef.value.innerHTML
}
usageEdit.disableEdit()
isEditing.value = false
productStore.saveProductDescription(productID.value, toLangId.value)
}
const cancelEdit = () => {
if (usageRef.value) {
usageRef.value.innerHTML = originalUsage.value
}
usageEdit.disableEdit()
isEditing.value = false
}
const saveDescription = async () => {
if (descriptionRef.value) {
productStore.productDescription.description = descriptionRef.value.innerHTML
}
descriptionEdit.disableEdit()
await productStore.saveProductDescription(productID.value, toLangId.value)
toLangId.value = settingStore.shopDefaultLanguage
isTranslations.value = false
}
const cancelDescriptionEdit = () => {
if (descriptionRef.value) {
descriptionRef.value.innerHTML = originalDescription.value
}
descriptionEdit.disableEdit()
}
function removeInlineStylesFromAll(product) {
if (!product) return
const removeStyles = (html: string) => {
if (!html) return ''
const div = document.createElement('div')
div.innerHTML = html
div.querySelectorAll('*').forEach(el => {
const bg = el.style.background || el.style.backgroundColor
if (bg) {
el.style.background = ''
el.style.backgroundColor = ''
}
})
div.querySelectorAll('p').forEach(p => {
if (p.querySelector('img')) {
p.style.display = 'flex'
p.style.gap = '20px'
p.style.padding = '30px 0'
}
})
div.querySelectorAll('table').forEach(table => {
table.querySelectorAll('tbody').forEach(tbody => {
tbody.style.setProperty('background-color', '#F8FAFC', 'important')
tbody.querySelectorAll('tr').forEach(tr => {
tr.querySelectorAll('td').forEach(td => {
td.style.setProperty('padding', '10px', 'important')
td.style.setProperty('border-top', '1pt solid black', 'important')
td.style.setProperty('border-left', '1pt solid black', 'important')
td.style.setProperty('border-bottom', '1pt solid black', 'important')
td.style.setProperty('border-right', '1pt solid black', 'important')
td.querySelectorAll('p').forEach(p => {
p.style.setProperty('display', 'flex', 'important')
p.style.setProperty('padding', '0px', 'important')
p.style.setProperty('align-items', 'start', 'important')
p.style.setProperty('color', 'black', 'important')
p.style.setProperty('font-size', '14px', 'important')
p.querySelectorAll('span').forEach(span => {
span.style.setProperty('color', 'black', 'important')
span.style.setProperty('font-size', '14px', 'important')
})
})
})
})
})
})
return div.innerHTML
}
product.description = removeStyles(product.description)
product.description_short = removeStyles(product.description_short)
product.usage = removeStyles(product.usage)
}
</script>

View File

@@ -1,37 +1,61 @@
<template> <template>
<component :is="Default || 'div'"> <div class="flex items-center gap-2 mb-4">
<div class="container my-10 mx-auto "> <UIcon name="line-md:arrow-left" class="text-(--text-sky-light) dark:text-(--text-sky-dark)" />
<p class="cursor-pointer text-(--text-sky-light) dark:text-(--text-sky-dark)" @click="backFromProduct()">
Back to products</p>
</div>
<div>
<div class="gap-4 mb-6 border-b border-(--border-light) dark:border-(--border-dark) py-4">
<div class="flex flex-wrap gap-4 items-center justify-between">
<div class="flex flex-col items-center gap-3" v-if="!isTranslations">
<USelectMenu v-model="toLangId" :items="langs" value-key="id"
class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
<template #default>
<div class="flex flex-col items-start leading-tight">
<span class="text-xs text-gray-400">
Selected language
</span>
<span class="font-medium dark:text-white text-black">
{{ selectedLangName }}
</span>
</div>
</template>
<div
class="flex items-end justify-between gap-4 mb-6 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
<div class="flex items-end gap-3">
<USelect v-model="selectedLanguage" :items="availableLangs" variant="outline" class="w-40!"
valueKey="iso_code">
<template #default="{ modelValue }">
<div class="flex items-center gap-2">
<span class="text-md">{{availableLangs.find(x => x.iso_code == modelValue)?.flag}}</span>
<span class="font-medium dark:text-white text-black">{{availableLangs.find(x => x.iso_code ==
modelValue)?.name}}</span>
</div>
</template>
<template #item-leading="{ item }"> <template #item-leading="{ item }">
<div class="flex items-center rounded-md cursor-pointer transition-colors"> <div class="flex items-center gap-2 cursor-pointer">
<span class="text-md">{{ item.flag }}</span> <span class="text-lg">{{ item.flag }}</span>
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span> <span class="font-medium dark:text-white text-black">
{{ item.name }}
</span>
</div> </div>
</template> </template>
</USelect> </USelectMenu>
</div> </div>
<UButton @click="translateToSelectedLanguage" color="primary" :loading="translating"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!"> <div v-if="toLangId !== settingStore.shopDefaultLanguage && !isTranslations"
Translate class="flex flex-wrap-reverse justify-between gap-2">
<UButton color="info" variant="outline" v-if="!isEditing" @click="isTranslations = true">
<p>Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-7.5" />
</UButton>
<UButton @click="translateToSelectedLanguage" color="info" :loading="translating">
Translate from Polish to {{langs.find(l => l.id === toLangId)?.name}}
</UButton>
</div>
<div v-if="isTranslations" class="flex gap-3 w-full justify-end">
<UButton @click="() => {
handleCancel()
isTranslations = false
}" color="info" variant="outline">
Cancel
</UButton>
<UButton color="info" @click="async () => {
await productStore.saveProductDescription(productID, toLangId)
isTranslations = false
}">
Save
</UButton> </UButton>
</div> </div>
<div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
<p class="text-lg font-medium dark:text-white text-black">Translating...</p>
</div> </div>
</div> </div>
@@ -41,209 +65,179 @@
<div v-else-if="productStore.error" class="flex items-center justify-center py-20"> <div v-else-if="productStore.error" class="flex items-center justify-center py-20">
<p class="text-red-500">{{ productStore.error }}</p> <p class="text-red-500">{{ productStore.error }}</p>
</div> </div>
<div v-else-if="productStore.productDescription" class="flex items-start gap-30">
<div class="w-80 h-80 bg-(--second-light) dark:bg-gray-700 rounded-lg flex items-center justify-center"> <div v-else-if="productStore.productDescription" class="space-y-7.5">
<span class="text-gray-500 dark:text-gray-400">Product Image</span> <div class="grid grid-cols-1 md:grid-cols-6 h-full w-full gap-6">
</div> <img
<div class="flex flex-col gap-2"> class="md:col-span-2 rounded-md bg-white rounded-md border border-(--border-light) dark:border-(--border-dark)"
<p class="text-[25px] font-bold text-black dark:text-white"> :src="productStore.productDescription.image_link" :alt="productStore.productDescription.name" @error="(e: Event) => {
{{ productStore.productDescription.name || 'Product Name' }} const target = e.target as HTMLImageElement
target.src = errorImg
}">
<div class="md:col-span-4 flex flex-col justify-between min-h-full w-full space-y-7.5">
<div class="space-y-7.5">
<div>
<p v-if="!isTranslations" class="text-[25px] font-bold text-black dark:text-white">
{{ productStore.productDescription.name || 'Product name not provided' }}
</p> </p>
<p v-html="productStore.productDescription.description_short" class="text-black dark:text-white"></p> <div class="" v-if="isTranslations">
<div class="space-y-[10px]"> <p>Title:</p>
<div class="flex items-center gap-1"> <UTextarea :rows="1" v-model="productStore.productDescription.name" autoresize :ui="{
<UIcon name="lets-icons:done-ring-round-fill" class="text-[20px] text-green-600" /> root: 'w-full bg-white!',
<p class="text-[16px] font-bold text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> base: 'text-2xl!',
}" />
</div>
</div>
<div class="" v-if="isTranslations">
<p>Link rewrite:</p>
<UTextarea :rows="1" v-model="productStore.productDescription.link_rewrite" autoresize :ui="{
root: 'w-full',
base: 'bg-inherit!',
}" />
</div>
<div class="" v-if="isTranslations">
<p>Link rewrite:</p>
<UTextarea :rows="1" v-model="productStore.productDescription.link_rewrite" autoresize :ui="{
root: 'w-full bg-white!'
}" />
</div>
<div>
<p v-if="!isTranslations"
v-html="productStore.productDescription.description_short || 'No short description available'"
class="text-black dark:text-white" />
<div class="" v-if="isTranslations">
<p>Short description:</p>
<ProductEditor v-model="productStore.productDescription.description_short" />
</div>
</div>
</div>
<div class="space-y-2.5">
<div v-if="productStore.productDescription.available_now" class="flex items-center gap-1">
<UIcon name="lets-icons:done-ring-round-fill"
class="text-[20px] light:text-(--accent-green-light) dark:text-(--accent-green-dark)" />
<p>
{{ productStore.productDescription.available_now }} {{ productStore.productDescription.available_now }}
</p> </p>
</div> </div>
<div class="flex items-center gap-1"> <div v-if="productStore.productDescription.delivery_in_stock" class="flex items-center gap-1">
<UIcon name="marketeq:car-shipping" class="text-[25px] text-green-600" /> <UIcon name="marketeq:car-shipping"
<p class="text-[18px] font-bold text-black dark:text-white"> class="text-[25px] light:text-(--accent-green-light) dark:text-(--accent-green-dark)" />
{{ productStore.productDescription.delivery_in_stock || 'Delivery information' }} <p>
{{ productStore.productDescription.delivery_in_stock }}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-if="productStore.productDescription" class="mt-16"> <UTabs :items="items" color="info" default-value="0" :ui="{
<div class="flex gap-4 my-6"> list: 'w-auto!',
<UButton @click="activeTab = 'description'" root: 'items-start!'
:class="['cursor-pointer', activeTab === 'description' ? 'bg-blue-500 text-white' : '']" color="neutral" }">
variant="outline"> <template #description>
<p class="dark:text-white">Description</p> <ProductEditor v-if="isTranslations" v-model="productStore.productDescription.description" />
</UButton> <p v-else v-html="productStore.productDescription.description || 'No description available'"
class="text-black dark:text-white" />
</template>
<UButton @click="activeTab = 'usage'" <template #usage>
:class="['cursor-pointer', activeTab === 'usage' ? 'bg-blue-500 text-white' : '']" color="neutral" <ProductEditor v-if="isTranslations" v-model="productStore.productDescription.usage" />
variant="outline"> <p v-else v-html="productStore.productDescription.usage || 'No usage information available'"
<p class="dark:text-white">Usage</p> class="text-black dark:text-white" />
</UButton> </template>
</UTabs>
</div> </div>
<div class=""></div>
<div v-if="activeTab === 'usage'"
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
<div class="flex justify-end items-center gap-3 mb-4">
<UButton v-if="!isEditing" @click="enableEdit"
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
<p class="text-white">Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
</UButton>
<UButton v-if="isEditing" @click="saveText" color="neutral" variant="outline" class="p-2.5 cursor-pointer">
<p class="dark:text-white text-black">Save the edited text</p>
</UButton>
<UButton v-if="isEditing" @click="cancelEdit" color="neutral" variant="outline"
class="p-2.5 cursor-pointer">
Cancel
</UButton>
</div> </div>
<p ref="usageRef" v-html="productStore.productDescription.usage"
class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p>
</div>
<div v-if="activeTab === 'description'"
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
<div class="flex items-center justify-end gap-3 mb-4">
<UButton v-if="!descriptionEdit.isEditing.value" @click="enableDescriptionEdit"
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
<p class="text-white">Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
</UButton>
<UButton v-if="descriptionEdit.isEditing.value" @click="saveDescription" color="neutral" variant="outline"
class="p-2.5 cursor-pointer">
<p class="dark:text-white text-black ">Save the edited text</p>
</UButton>
<UButton v-if="descriptionEdit.isEditing.value" @click="cancelDescriptionEdit" color="neutral"
variant="outline" class="p-2.5 cursor-pointer">Cancel</UButton>
</div>
<div ref="descriptionRef" v-html="productStore.productDescription.description"
class="flex flex-col justify-center dark:text-white text-black">
</div>
</div>
</div>
</div>
</component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue' import { langs } from '@/router/langs';
import { useRoute } from 'vue-router' import { useProductStore } from '@/stores/admin/product';
import { useProductStore } from '@/stores/product' import { useSettingsStore } from '@/stores/admin/settings';
import { useEditable } from '@/composable/useConteditable' import { computed, onMounted, ref, watch } from 'vue';
import { langs } from '@/router/langs' import { useRoute, useRouter } from 'vue-router';
import type { Language } from '@/types' import ProductEditor from '../inner/ProductEditor.vue';
import Default from '@/layouts/default.vue' import errorImg from '@/assets/error.svg'
const router = useRouter()
const route = useRoute() const route = useRoute()
const activeTab = ref('description') const settingStore = useSettingsStore()
const productStore = useProductStore() const productStore = useProductStore()
const translating = ref(false)
const isEditing = ref(false) const toLangId = ref<number>(settingStore.shopDefaultLanguage)
const availableLangs = computed(() => langs)
const selectedLanguage = ref('en')
const currentLangId = ref(2)
const productID = ref<number>(0) const productID = ref<number>(0)
const translating = ref(false)
const isEditing = ref(false)
const isTranslations = ref(false)
// Watch for language changes and refetch product description const selectedLangName = computed(() => {
watch(selectedLanguage, async (newLang: string) => { return langs.find(l => l.id === toLangId.value)?.name || 'Select language'
if (productID.value) {
await fetchForLanguage(newLang)
}
}) })
const fetchForLanguage = async (langCode: string) => {
const lang = langs.find((l: Language) => l.iso_code === langCode) function backFromProduct() {
if (lang && productID.value) { let path = localStorage.getItem('back_from_product')
await productStore.getProductDescription(lang.id, productID.value)
currentLangId.value = lang.id if (path) {
let res = JSON.parse(path)
router.push({
name: res.name,
params: res.params,
query: res.query
})
localStorage.removeItem('back_from_product')
} else {
router.push({
name: 'admin-products',
})
} }
} }
const translateToSelectedLanguage = async () => { const translateToSelectedLanguage = async () => {
const targetLang = langs.find((l: Language) => l.iso_code === selectedLanguage.value) if (toLangId.value && productID.value) {
if (targetLang && currentLangId.value && productID.value) {
translating.value = true translating.value = true
try { try {
await productStore.translateProductDescription(productID.value, currentLangId.value, targetLang.id) await productStore.translateProductDescription(productID.value, toLangId.value)
currentLangId.value = targetLang.id
} finally { } finally {
translating.value = false translating.value = false
} }
} }
if (productStore.productDescription) {
isTranslations.value = true
} }
}
const fetchForLanguage = async (langId: number | null) => {
if (productID.value) {
await productStore.getProductDescription(langId, productID.value)
}
}
const handleCancel = async () => {
await productStore.getProductDescription(toLangId.value, productID.value)
isTranslations.value = false
}
watch(toLangId, (lang) => {
fetchForLanguage(lang)
})
onMounted(async () => { onMounted(async () => {
const id = route.params.product_id const id = route.params.product_id
if (id) { if (id) {
productID.value = Number(id) productID.value = Number(id)
await fetchForLanguage(selectedLanguage.value) await fetchForLanguage(toLangId.value)
} }
}) })
const descriptionRef = ref<HTMLElement | null>(null) const items = ref([
const usageRef = ref<HTMLElement | null>(null) { label: 'Description', slot: 'description', name: 'description' },
{ label: 'Usage', slot: 'usage', name: 'usage' }
const descriptionEdit = useEditable(descriptionRef) ])
const usageEdit = useEditable(usageRef)
const originalDescription = ref('')
const originalUsage = ref('')
const saveDescription = async () => {
descriptionEdit.disableEdit()
await productStore.saveProductDescription(productID.value)
}
const cancelDescriptionEdit = () => {
if (descriptionRef.value) {
descriptionRef.value.innerHTML = originalDescription.value
}
descriptionEdit.disableEdit()
}
const enableDescriptionEdit = () => {
if (descriptionRef.value) {
originalDescription.value = descriptionRef.value.innerHTML
}
descriptionEdit.enableEdit()
}
const enableEdit = () => {
if (usageRef.value) {
originalUsage.value = usageRef.value.innerHTML
}
isEditing.value = true
usageEdit.enableEdit()
}
const saveText = () => {
usageEdit.disableEdit()
isEditing.value = false
productStore.saveProductDescription(productID.value)
}
const cancelEdit = () => {
if (usageRef.value) {
usageRef.value.innerHTML = originalUsage.value
}
usageEdit.disableEdit()
isEditing.value = false
}
</script> </script>
<style>
.images {
display: flex;
align-items: center;
gap: 70px;
margin: 20px 0 20px 0;
}
</style>

View File

@@ -0,0 +1,208 @@
<template>
<div class="flex flex-col md:flex-row gap-10">
<div class="w-full flex flex-col items-center gap-4">
<UTable :data="usersList" :columns="columns" class="flex-1 w-full"
:ui="{ root: 'max-w-100wv overflow-auto!' }" />
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, resolveComponent, h } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson'
import { useRoute, useRouter } from 'vue-router'
import CategoryMenu from '../inner/CategoryMenu.vue'
import type { TableColumn } from '@nuxt/ui'
import type { Customer } from '@/types/user'
const router = useRouter()
const route = useRoute()
const perPage = ref(15)
const page = computed({
get: () => Number(route.query.p) || 1,
set: (val: number) => {
router.push({ query: { ...route.query, p: val } })
}
})
const sortField = computed({
get: () => [
route.query.sort as string | undefined,
route.query.direction as 'asc' | 'desc' | undefined
],
set: ([sort]: [string, 'asc' | 'desc']) => {
const currentSort = route.query.sort as string | undefined
const currentDirection = route.query.direction as 'asc' | 'desc' | undefined
const query = { ...route.query }
if (currentSort === sort) {
if (currentDirection === 'asc') query.direction = 'desc'
else if (currentDirection === 'desc') {
delete query.sort
delete query.direction
} else {
query.direction = 'asc'
query.sort = sort
}
} else {
query.sort = sort
query.direction = 'asc'
}
router.push({ query })
}
})
const filters = computed<Record<string, string>>({
get: () => {
const q = { ...route.query }
delete q.page
delete q.sort
delete q.direction
return q as Record<string, string>
},
set: (val) => {
const baseQuery = { ...route.query }
Object.keys(baseQuery).forEach(k => {
if (!['page', 'sort', 'direction'].includes(k)) delete baseQuery[k]
})
router.push({ query: { ...baseQuery, ...val, page: 1 } })
}
})
function debounce(fn: Function, delay = 400) {
let t: any
return (...args: any[]) => {
clearTimeout(t)
t = setTimeout(() => fn(...args), delay)
}
}
const updateFilter = debounce((columnId: string, val: string) => {
const newFilters = { ...filters.value }
if (val) newFilters[columnId] = val
else delete newFilters[columnId]
filters.value = newFilters
}, 400)
const usersList = ref<Customer[]>([])
const total = ref(0)
const loading = ref(true)
const error = ref<string | null>(null)
async function fetchUsersList() {
loading.value = true
error.value = null
const params = new URLSearchParams(route.query as any).toString()
const url = `/api/v1/restricted/customer/list?elems=${perPage.value}&${params}`
try {
const res = await useFetchJson<{ items: { items: Customer[] }; count: number }>(url)
usersList.value = res.items?.items || []
total.value = res.count || 0
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load users'
} finally {
loading.value = false
}
}
function getIcon(name: string) {
if (sortField.value[0] === name) {
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
if (sortField.value[1] === 'desc') return 'i-lucide-arrow-down-wide-narrow'
}
return 'i-lucide-arrow-up-down'
}
const UInput = resolveComponent('UInput')
const UIcon = resolveComponent('UIcon')
const UButton = resolveComponent('UButton')
const columns: TableColumn<Customer>[] = [
{
accessorKey: 'user_id',
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => (sortField.value = ['user_id', 'asc'])
}, [h('span', 'Client ID'), h(UIcon, { name: getIcon('user_id') })]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
size: 'xs'
})
]),
cell: ({ row }) => `#${row.getValue('user_id')}`
},
{
accessorKey: 'name',
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => (sortField.value = ['last_name, first_name', 'asc'])
}, [h('span', 'Name/Surname'), h(UIcon, { name: getIcon('name') })]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
size: 'xs'
})
]),
cell: ({ row }) => `${row.original.first_name} ${row.original.last_name}`
},
{
accessorKey: 'email',
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => (sortField.value = ['email', 'asc'])
}, [h('span', 'Email'), h(UIcon, { name: getIcon('email') })]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
size: 'xs'
})
]),
cell: ({ row }) => row.getValue('email')
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UButton, {
// onClick: () => {
// goToProduct(row.original.product_id, row.original.link_rewrite)
// },
class: 'cursor-pointer',
color: 'info',
variant: 'soft'
}, () => 'Manage')
},
},
{
accessorKey: 'profile',
header: '',
cell: ({ row }) => {
const userId = row.original.user_id
return h(UButton, {
color: 'info',
size: 'sm',
variant: 'soft',
onClick: () => {
router.push({
name: 'customer-management-profile',
params: { user_id: userId }
})
}
}, () => 'Go to profile')
}
}
]
watch(() => route.query, fetchUsersList, { immediate: true })
</script>

View File

@@ -0,0 +1,242 @@
<template>
<div class="pt-70! flex flex-col items-center justify-center bg-gray-50 dark:bg-(--main-dark)">
<h1 class="text-6xl font-bold text-black dark:text-white mb-14">Search Users</h1>
<div class="w-full max-w-4xl">
<UInput icon="i-lucide-search" type="text" placeholder="Type user name or ID..." v-model="searchQuery"
class="w-full!" :ui="{ base: 'py-4! rounded-full!' }" />
</div>
<p v-if="loading">Loading...</p>
<p v-else-if="error" class="text-red-600">{{ error }}</p>
<div v-else-if="clients.length" class="w-full max-w-4xl mt-7">
<UTable :columns="columns" :data="clients" :ui="{ root: 'w-full!' }" />
</div>
<p v-else-if="searchQuery.length" class="pt-4 text-gray-700">
No users found with that name or ID
</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, resolveComponent, h } from 'vue'
import type { TableColumn } from '@nuxt/ui';
import { useRoute, useRouter } from 'vue-router';
import { useFetchJson } from '@/composable/useFetchJson';
interface Customer {
user_id: number;
email: string;
first_name: string;
last_name: string;
}
const searchQuery = ref('');
const clients = ref<Customer[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
const limit = ref(10);
const page = ref(1);
function debounce(fn: Function, delay = 400) {
let t: any;
return (...args: any[]) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), delay);
};
}
const searchClients = async () => {
if (!searchQuery.value) {
clients.value = [];
return;
}
loading.value = true;
error.value = null;
try {
const result = await useFetchJson(
`/api/v1/restricted/customer/list?search=${encodeURIComponent(searchQuery.value)}&elems=${limit.value}&page=${page.value}`
);
clients.value = result.items?.items || [];
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to fetch clients';
clients.value = [];
} finally {
loading.value = false;
}
};
watch(searchQuery, debounce(searchClients, 300));
const router = useRouter()
const route = useRoute()
const UInput = resolveComponent('UInput')
const UIcon = resolveComponent('UIcon')
const UButton = resolveComponent('UButton')
const sortField = computed({
get: () => [
route.query.sort as string | undefined,
route.query.direction as 'asc' | 'desc' | undefined
],
set: ([sort]: [string, 'asc' | 'desc']) => {
const currentSort = route.query.sort as string | undefined
const currentDirection = route.query.direction as 'asc' | 'desc' | undefined
const query = { ...route.query }
if (currentSort === sort) {
if (currentDirection === 'asc') query.direction = 'desc'
else if (currentDirection === 'desc') {
delete query.sort
delete query.direction
} else {
query.direction = 'asc'
query.sort = sort
}
} else {
query.sort = sort
query.direction = 'asc'
}
router.push({ query })
}
})
function getIcon(name: string) {
if (sortField.value[0] === name) {
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
if (sortField.value[1] === 'desc') return 'i-lucide-arrow-down-wide-narrow'
}
return 'i-lucide-arrow-up-down'
}
const filters = computed<Record<string, string>>({
get: () => {
const q = { ...route.query }
delete q.page
delete q.sort
delete q.direction
return q as Record<string, string>
},
set: (val) => {
const baseQuery = { ...route.query }
Object.keys(baseQuery).forEach(k => {
if (!['page', 'sort', 'direction'].includes(k)) delete baseQuery[k]
})
router.push({ query: { ...baseQuery, ...val, page: 1 } })
}
})
const updateFilter = debounce((columnId: string, val: string) => {
const newFilters = { ...filters.value }
if (val) newFilters[columnId] = val
else delete newFilters[columnId]
filters.value = newFilters
}, 400)
const columns: TableColumn<Customer>[] = [
{
accessorKey: 'user_id',
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => (sortField.value = ['user_id', 'asc'])
}, [h('span', 'Client ID'), h(UIcon, { name: getIcon('user_id') })]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
size: 'xs'
})
]),
cell: ({ row }) => `#${row.getValue('user_id')}`
},
{
accessorKey: 'name',
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => (sortField.value = ['last_name, first_name', 'asc'])
}, [h('span', 'Name/Surname'), h(UIcon, { name: getIcon('name') })]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
size: 'xs'
})
]),
cell: ({ row }) => `${row.original.first_name} ${row.original.last_name}`
},
{
accessorKey: 'email',
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => (sortField.value = ['email', 'asc'])
}, [h('span', 'Email'), h(UIcon, { name: getIcon('email') })]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
size: 'xs'
})
]),
cell: ({ row }) => row.getValue('email')
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UButton, {
// onClick: () => {
// goToProduct(row.original.product_id, row.original.link_rewrite)
// },
class: 'cursor-pointer',
color: 'info',
variant: 'soft'
}, () => 'Manage')
},
},
{
accessorKey: 'profile',
header: '',
cell: ({ row }) => {
const userId = row.original.user_id
return h(UButton, {
color: 'info',
size: 'sm',
variant: 'soft',
onClick: () => {
router.push({
name: 'customer-management-profile',
params: { user_id: userId }
})
}
}, () => 'Go to profile')
}
}
]
</script>
<style scoped>
input::placeholder {
color: #9ca3af;
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<div>customer-management</div>
</template>
<script setup lang="ts">
</script>

View File

@@ -1,6 +1,5 @@
<template> <template>
<component :is="Default || 'div'"> <div class="">
<div class="container mx-auto">
<h2 <h2
class="font-semibold text-black dark:text-white pb-6 text-2xl"> class="font-semibold text-black dark:text-white pb-6 text-2xl">
{{ t('Cart Items') }} {{ t('Cart Items') }}
@@ -48,15 +47,13 @@
</UButton> </UButton>
</div> </div>
</div> </div>
</component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useCartStore } from '@/stores/cart' import { useCartStore } from '@/stores/customer/cart'
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import Default from '@/layouts/default.vue'
const cartStore = useCartStore() const cartStore = useCartStore()
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()

View File

@@ -1,6 +1,5 @@
<template> <template>
<component :is="Default || 'div'"> <div class="">
<div class="container mx-auto">
<div class="flex flex-col gap-5 mb-6"> <div class="flex flex-col gap-5 mb-6">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1> <h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
<div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0"> <div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0">
@@ -8,22 +7,34 @@
<UInput v-model="searchQuery" type="text" :placeholder="t('Search address')" <UInput v-model="searchQuery" type="text" :placeholder="t('Search address')"
class="bg-white dark:bg-gray-800 text-black dark:text-white absolute" /> class="bg-white dark:bg-gray-800 text-black dark:text-white absolute" />
<UIcon name="ic:baseline-search" <UIcon name="ic:baseline-search"
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark) relative left-40" /> class="text-[20px] text-(--text-sky-light) dark:text-(--text-sky-dark) relative left-40" />
</div> </div>
<UButton color="primary" @click="openCreateModal" <UButton color="info" @click="openCreateModal">
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
<UIcon name="mdi:add-bold" /> <UIcon name="mdi:add-bold" />
{{ t('Add Address') }} {{ t('Add Address') }}
</UButton> </UButton>
</div> </div>
</div> </div>
<div v-if="paginatedAddresses.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> <div v-if="cartStore.addressLoading" class="text-center py-8 text-gray-500 dark:text-gray-400">
<div v-for="address in paginatedAddresses" :key="address.id" {{ t('Loading addresses...') }}
</div>
<div v-else-if="cartStore.addressError" class="text-center py-8 text-red-500 dark:text-red-400">
{{ cartStore.addressError }}
</div>
<div v-else-if="cartStore.paginatedAddresses.length"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div v-for="address in cartStore.paginatedAddresses" :key="address.id"
class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) hover:shadow-md transition-shadow flex justify-between"> class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) hover:shadow-md transition-shadow flex justify-between">
<div class="flex flex-col gap-2 items-strat justify-end"> <div class="flex flex-col gap-2 items-start justify-end">
<p class="text-black dark:text-white">{{ address.street }}</p> <p class="text-black dark:text-white font-semibold">{{ address.address_info.recipient }}</p>
<p class="text-black dark:text-white">{{ address.zipCode }}, {{ address.city }}</p> <p class="text-black dark:text-white">{{ address.address_info.street }} {{
<p class="text-black dark:text-white">{{ address.country }}</p> address.address_info.building_no }}{{ address.address_info.apartment_no ? '/' +
address.address_info.apartment_no : '' }}</p>
<p class="text-black dark:text-white">{{ address.address_info.postal_code }}, {{
address.address_info.city }}</p>
<p class="text-black dark:text-white">{{ address.address_info.voivodeship }}</p>
<p v-if="address.address_info.address_line2" class="text-black dark:text-white">{{
address.address_info.address_line2 }}</p>
</div> </div>
<div class="flex flex-col items-end justify-between gap-2"> <div class="flex flex-col items-end justify-between gap-2">
<button @click="confirmDelete(address.id)" <button @click="confirmDelete(address.id)"
@@ -32,7 +43,7 @@
<UIcon name="material-symbols:delete" class="text-[18px]" /> <UIcon name="material-symbols:delete" class="text-[18px]" />
</button> </button>
<UButton size="sm" color="neutral" variant="outline" @click="openEditModal(address)" <UButton size="sm" color="neutral" variant="outline" @click="openEditModal(address)"
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[13px]"> class="text-(--text-sky-light) dark:text-(--text-sky-dark) text-[13px]">
{{ t('edit') }} {{ t('edit') }}
<UIcon name="ic:sharp-edit" class="text-[15px]" /> <UIcon name="ic:sharp-edit" class="text-[15px]" />
</UButton> </UButton>
@@ -47,33 +58,24 @@
<template #content> <template #content>
<div class="p-6 flex flex-col gap-6"> <div class="p-6 flex flex-col gap-6">
<p class="text-[20px] text-black dark:text-white ">Address</p> <p class="text-[20px] text-black dark:text-white ">Address</p>
<UForm @submit.prevent="saveAddress" class="space-y-4" :validate="validate"> <UForm @submit="saveAddress" class="space-y-4" :validate="validate" :state="formData">
<div> <template v-for="field in formFieldKeys" :key="field">
<label class="block text-sm font-medium text-black dark:text-white mb-1">Street *</label> <UFormField :label="fieldLabel(field)" :name="field"
<UInput v-model="formData.street" placeholder="Enter street" name="street" class="w-full" /> :required="field !== 'address_line2'">
</div> <UInput v-model="formData[field]" :placeholder="fieldLabel(field)" class="w-full" />
<div> </UFormField>
<label class="block text-sm font-medium text-black dark:text-white mb-1">Zip Code *</label> </template>
<UInput v-model="formData.zipCode" placeholder="Enter zip code" name="zipCode"
class="w-full" /> <div class="flex justify-end gap-2">
</div> <UButton variant="outline" color="neutral" @click="closeModal">
<div> {{ t('Cancel') }}
<label class="block text-sm font-medium text-black dark:text-white mb-1">City *</label> </UButton>
<UInput v-model="formData.city" placeholder="Enter city" name="city" class="w-full" />
</div> <UButton type="submit" color="info" class="cursor-pointer">
<div> {{ t('Save') }}
<label class="block text-sm font-medium text-black dark:text-white mb-1">Country *</label> </UButton>
<UInput v-model="formData.country" placeholder="Enter country" name="country"
class="w-full" />
</div> </div>
</UForm> </UForm>
<div class="flex justify-end gap-2">
<UButton variant="outline" color="neutral" @click="closeModal"
class="text-black dark:text-white">{{ t('Cancel') }}</UButton>
<UButton variant="outline" color="neutral" @click="saveAddress"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
{{ t('Save') }}</UButton>
</div>
</div> </div>
</template> </template>
</UModal> </UModal>
@@ -99,82 +101,146 @@
</template> </template>
</UModal> </UModal>
</div> </div>
</component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useAddressStore } from '@/stores/address' import { useCartStore } from '@/stores/customer/cart'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Default from '@/layouts/default.vue' import { currentCountry } from '@/router/langs'
const addressStore = useAddressStore()
type AddressFormState = Record<string, string>
const cartStore = useCartStore()
const { t } = useI18n() const { t } = useI18n()
const searchQuery = ref('') const searchQuery = ref(cartStore.addressSearchQuery)
const showModal = ref(false) const showModal = ref(false)
const isEditing = ref(false) const isEditing = ref(false)
const editingAddressId = ref<number | null>(null) const editingAddressId = ref<number | null>(null)
const formData = ref({ street: '', zipCode: '', city: '', country: '' }) const addressTemplate = ref<AddressFormState | null>(null)
const formData = reactive<AddressFormState>({})
const showDeleteConfirm = ref(false) const showDeleteConfirm = ref(false)
const addressToDelete = ref<number | null>(null) const addressToDelete = ref<number | null>(null)
const page = ref(addressStore.currentPage) const page = computed<number>({
const paginatedAddresses = computed(() => addressStore.paginatedAddresses) get: () => cartStore.addressCurrentPage,
const totalItems = computed(() => addressStore.totalItems) set: (value: number) => cartStore.setAddressPage(value)
const pageSize = addressStore.pageSize
watch(page, (newPage) => addressStore.setPage(newPage))
watch(searchQuery, (val) => {
addressStore.setSearchQuery(val)
}) })
function openCreateModal() { const totalItems = computed(() => cartStore.totalAddressItems)
const pageSize = cartStore.addressPageSize
const formFieldKeys = computed(() => (addressTemplate.value ? Object.keys(addressTemplate.value) : []))
watch(searchQuery, (val) => {
cartStore.setAddressSearchQuery(val)
})
onMounted(() => {
cartStore.fetchAddresses()
})
function clearFormData() {
Object.keys(formData).forEach((key) => delete formData[key])
}
function fieldLabel(key: string) {
const labels: Record<string, string> = {
postal_code: 'Zip Code',
post_town: 'City',
city: 'City',
county: 'County',
region: 'Region',
voivodeship: 'Region / Voivodeship',
street: 'Street',
thoroughfare: 'Street',
building_no: 'Building No',
building_name: 'Building Name',
house_number: 'House Number',
orientation_number: 'Orientation Number',
apartment_no: 'Apartment No',
sub_building: 'Sub Building',
address_line2: 'Address Line 2',
recipient: 'Recipient'
}
return t(labels[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (chr) => chr.toUpperCase()))
}
async function openCreateModal() {
resetForm() resetForm()
isEditing.value = false isEditing.value = false
const template = await cartStore.getAddressTemplate(Number(currentCountry.value?.id) | 2).catch(() => null)
if (template) {
addressTemplate.value = template
clearFormData()
Object.assign(formData, template)
}
showModal.value = true showModal.value = true
} }
function openEditModal(address: any) {
formData.value = { async function openEditModal(address: any) {
street: address.street, currentCountry.value = address.country_id || 1
zipCode: address.zipCode, const template = await cartStore.getAddressTemplate(address.country_id | 2).catch(() => null)
city: address.city,
country: address.country if (template) {
addressTemplate.value = template
clearFormData()
Object.assign(formData, template)
} }
if (address.address_info) {
formFieldKeys.value.forEach((key) => {
formData[key] = address.address_info[key] ?? ''
})
}
isEditing.value = true isEditing.value = true
editingAddressId.value = address.id editingAddressId.value = address.id
showModal.value = true showModal.value = true
} }
function resetForm() { function resetForm() {
formData.value = { street: '', zipCode: '', city: '', country: '' } clearFormData()
editingAddressId.value = null editingAddressId.value = null
} }
function closeModal() { function closeModal() {
showModal.value = false showModal.value = false
resetForm() resetForm()
} }
function validate() { function validate() {
const errors = [] const errors: Array<{ name: string; message: string }> = []
if (!formData.value.street) errors.push({ name: 'street', message: 'Street required' }) const optionalFields = new Set(['address_line2'])
if (!formData.value.zipCode) errors.push({ name: 'zipCode', message: 'Zip Code required' })
if (!formData.value.city) errors.push({ name: 'city', message: 'City required' }) formFieldKeys.value.forEach((key) => {
if (!formData.value.country) errors.push({ name: 'country', message: 'Country required' }) if (!optionalFields.has(key) && !formData[key]?.trim()) {
return errors.length ? errors : null errors.push({ name: key, message: `${fieldLabel(key)} required` })
} }
function saveAddress() { })
if (validate()) return
return errors
}
async function saveAddress() {
if (isEditing.value && editingAddressId.value) { if (isEditing.value && editingAddressId.value) {
addressStore.updateAddress(editingAddressId.value, formData.value) await cartStore.updateAddress(editingAddressId.value, currentCountry.value?.id || 2, formData)
} else { } else {
addressStore.addAddress(formData.value) await cartStore.addAddress(currentCountry.value?.id || 2, formData)
} }
closeModal() closeModal()
} }
function confirmDelete(id: number) { function confirmDelete(id: number) {
addressToDelete.value = id addressToDelete.value = id
showDeleteConfirm.value = true showDeleteConfirm.value = true
} }
function deleteAddress() {
async function deleteAddress() {
if (addressToDelete.value) { if (addressToDelete.value) {
addressStore.deleteAddress(addressToDelete.value) await cartStore.deleteAddress(addressToDelete.value)
} }
showDeleteConfirm.value = false showDeleteConfirm.value = false
addressToDelete.value = null addressToDelete.value = null

View File

@@ -1,6 +1,5 @@
<template> <template>
<component :is="Default || 'div'"> <div class="flex flex-col gap-5 md:gap-10">
<div class="container mx-auto flex flex-col gap-5 md:gap-10">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Cart') }}</h1> <h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Cart') }}</h1>
<div class="flex flex-col lg:flex-row gap-5 md:gap-10"> <div class="flex flex-col lg:flex-row gap-5 md:gap-10">
<div class="flex-1"> <div class="flex-1">
@@ -43,7 +42,7 @@
name: 'customer-product', params: { name: 'customer-product', params: {
product_id: '51' product_id: '51'
} }
}" class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline"> }" class="inline-block mt-4 text-(--text-sky-light) dark:text-(--text-sky-dark) hover:underline">
{{ t('Continue Shopping') }} {{ t('Continue Shopping') }}
</RouterLink> </RouterLink>
</div> </div>
@@ -72,7 +71,7 @@
</div> </div>
<div class="flex justify-between mb-6"> <div class="flex justify-between mb-6">
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span> <span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{ <span class="text-(--text-sky-light) dark:text-(--text-sky-dark) font-bold text-lg">${{
cartStore.orderTotal.toFixed(2) }}</span> cartStore.orderTotal.toFixed(2) }}</span>
</div> </div>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
@@ -103,7 +102,7 @@
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20' ? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'"> : 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="address.id" v-model="selectedAddress" <input type="radio" :value="address.id" v-model="selectedAddress"
class="mt-1 w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" /> class="mt-1 w-4 h-4 text-(--text-sky-light) dark:text-(--text-sky-dark)" />
<div class="flex-1"> <div class="flex-1">
<p class="text-black dark:text-white font-medium">{{ address.street }}</p> <p class="text-black dark:text-white font-medium">{{ address.street }}</p>
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode }}, {{ address.city }}</p> <p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode }}, {{ address.city }}</p>
@@ -115,7 +114,7 @@
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" /> <UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p> <p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
<RouterLink :to="{ name: 'addresses' }" <RouterLink :to="{ name: 'addresses' }"
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline"> class="inline-block mt-2 text-(--text-sky-light) dark:text-(--text-sky-dark) hover:underline">
{{ t('Add Address') }} {{ t('Add Address') }}
</RouterLink> </RouterLink>
</div> </div>
@@ -131,11 +130,11 @@
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20' ? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'"> : 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="method.id" v-model="selectedDeliveryMethod" <input type="radio" :value="method.id" v-model="selectedDeliveryMethod"
class="w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" /> class="w-4 h-4 text-(--text-sky-light) dark:text-(--text-sky-dark)" />
<div class="flex-1"> <div class="flex-1">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-black dark:text-white font-medium">{{ method.name }}</span> <span class="text-black dark:text-white font-medium">{{ method.name }}</span>
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-medium"> <span class="text-(--text-sky-light) dark:text-(--text-sky-dark) font-medium">
{{ method.price > 0 ? `$${method.price.toFixed(2)}` : t('Free') }} {{ method.price > 0 ? `$${method.price.toFixed(2)}` : t('Free') }}
</span> </span>
</div> </div>
@@ -147,16 +146,14 @@
</div> </div>
</div> </div>
</div> </div>
</component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useCartStore } from '@/stores/cart' import { useCartStore } from '@/stores/customer/cart'
import { useAddressStore } from '@/stores/address' import { useAddressStore } from '@/stores/customer/address'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import Default from '@/layouts/default.vue'
const cartStore = useCartStore() const cartStore = useCartStore()
const addressStore = useAddressStore() const addressStore = useAddressStore()
const { t } = useI18n() const { t } = useI18n()

View File

@@ -1,9 +1,6 @@
<template> <template>
<component :is="Default || 'div'">
Orders page Orders page
</component>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Default from '@/layouts/default.vue'
</script> </script>

View File

@@ -1,21 +1,28 @@
<template> <template>
<component :is="Default || 'div'"> <div class="">
<div class="container mx-auto">
<div class="flex md:flex-row flex-col justify-between gap-8 my-6"> <div class="flex md:flex-row flex-col justify-between gap-8 my-6">
<div class="flex-1"> <div class="flex-1">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] "> <div
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] ">
<img :src="selectedColor?.image || productData.image" :alt="productData.name" <img :src="selectedColor?.image || productData.image" :alt="productData.name"
class="max-w-full h-auto object-contain" /> class="max-w-full h-auto object-contain" />
</div> </div>
</div> </div>
<div class="flex-1 flex flex-col gap-4"> <div class="flex-1 flex flex-col gap-4">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"> <h1 class="text-2xl font-bold text-gray-900 dark:text-white">
{{ productData.name }} {{ productData.name }}
</h1> </h1>
<UIcon name="material-symbols:favorite"
class="cursor-pointer text-2xl transition hover:scale-110"
:class="productData.is_favorite ? 'text-red-500' : 'text-gray-400'"
@click="toggleFavorite" />
</div>
<p class="text-gray-600 dark:text-gray-300"> <p class="text-gray-600 dark:text-gray-300">
{{ productData.description }} {{ productData.description }}
</p> </p>
<div class="text-3xl font-bold text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> <div class="text-3xl font-bold text-(--text-sky-light) dark:text-(--text-sky-dark)">
{{ productData.price }} {{ productData.price }}
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
@@ -32,7 +39,7 @@
</div> </div>
<div class="flex md:flex-row flex-col justify-between md:items-end items-start gap-5 md:gap-0 md:mb-8 mb-4"> <div class="flex md:flex-row flex-col justify-between md:items-end items-start gap-5 md:gap-0 md:mb-8 mb-4">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<span class="text-sm text-(--accent-blue-light) dark:text-(--accent-blue-dark) ">Colors:</span> <span class="text-sm text-(--text-sky-light) dark:text-(--text-sky-dark) ">Colors:</span>
<div class="flex gap-2"> <div class="flex gap-2">
<button v-for="color in productData.colors" :key="color.id" @click="selectedColor = color" <button v-for="color in productData.colors" :key="color.id" @click="selectedColor = color"
class="w-10 h-10 border-2 transition-all" :class="selectedColor?.id === color.id class="w-10 h-10 border-2 transition-all" :class="selectedColor?.id === color.id
@@ -43,7 +50,8 @@
</div> </div>
<div class="flex gap-5 items-end"> <div class="flex gap-5 items-end">
<UInputNumber v-model="value" /> <UInputNumber v-model="value" />
<UButton color="primary" class="px-14! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white"> <UButton color="primary"
class="px-14! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white">
Add to Cart Add to Cart
</UButton> </UButton>
</div> </div>
@@ -70,14 +78,14 @@
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" /> <hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
<ProductVariants /> <ProductVariants />
</div> </div>
</component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import ProductCustomization from './components/ProductCustomization.vue' import ProductCustomization from './components/ProductCustomization.vue'
import ProductVariants from './components/ProductVariants.vue' import ProductVariants from './components/ProductVariants.vue'
import Default from '@/layouts/default.vue' import { useFetchJson } from '@/composable/useFetchJson'
import { useRoute } from 'vue-router'
interface Color { interface Color {
id: string id: string
name: string name: string
@@ -97,6 +105,7 @@ interface ProductData {
howToUseText: string howToUseText: string
productDetailsText: string productDetailsText: string
documentsText: string documentsText: string
is_favorite: boolean
} }
const activeTab = ref('description') const activeTab = ref('description')
@@ -157,6 +166,24 @@ const activeTabContent = computed(() => {
if (productData.colors.length > 0) { if (productData.colors.length > 0) {
selectedColor.value = productData.colors[0] as Color selectedColor.value = productData.colors[0] as Color
} }
const route = useRoute()
async function toggleFavorite() {
const url = `/api/v1/restricted/product/favorite/${route.params.product_id}`
try {
if (!productData.is_favorite) {
await useFetchJson(url, { method: 'POST' })
} else {
await useFetchJson(url, { method: 'DELETE' })
}
productData.is_favorite = !productData.is_favorite
} catch (e: unknown) {
console.error(e)
}
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,7 +1,6 @@
<template> <template>
<suspense> <suspense>
<component :is="Default || 'div'"> <div class="">
<div class="container mx-auto">
<!-- <UNavigationMenu orientation="vertical" :items="listing" class="data-[orientation=vertical]:w-48"> <!-- <UNavigationMenu orientation="vertical" :items="listing" class="data-[orientation=vertical]:w-48">
<template #item="{ item, active }"> <template #item="{ item, active }">
<div class="flex items-center gap-2 px-3 py-2"> <div class="flex items-center gap-2 px-3 py-2">
@@ -11,51 +10,47 @@
</template> </template>
</UNavigationMenu> --> </UNavigationMenu> -->
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1> <h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1>
<div v-if="loading" class="text-center py-8"> <div v-if="customerProductStore.loading" class="text-center py-8">
<span class="text-gray-600 dark:text-gray-400">Loading products...</span> <span class="text-gray-600 dark:text-gray-400">Loading products...</span>
</div> </div>
<div v-else-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded"> <div v-else-if="customerProductStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ error }} {{ customerProductStore.error }}
</div> </div>
<div v-else class="overflow-x-auto"> <div v-else class="overflow-x-auto">
<div class="flex gap-2"> <div class="flex gap-2">
<CategoryMenu /> <CategoryMenu />
<UTable :data="productsList" :columns="columns" class="flex-1"> <UTable :data="customerProductStore.productsList" :columns="columns" class="flex-1">
<template #expanded="{ row }"> <template #expanded="{ row }">
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{ <UTable :data="customerProductStore.productsList.slice(0, 3)" :columns="columnsChild"
:ui="{
thead: 'hidden' thead: 'hidden'
}" /> }" />
</template> </template>
</UTable> </UTable>
</div> </div>
<div class="flex justify-center items-center py-8"> <div class="flex justify-center items-center py-8">
<UPagination v-model:page="page" :total="total" :page-size="perPage" /> <UPagination v-model:page="page" :total="customerProductStore.total"
:page-size="customerProductStore.perPage" />
</div> </div>
<div v-if="productsList.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400"> <div v-if="customerProductStore.productsList.length === 0"
class="text-center py-8 text-gray-500 dark:text-gray-400">
No products found No products found
</div> </div>
</div> </div>
</div> </div>
</component>
</suspense> </suspense>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, h, resolveComponent, computed } from 'vue' import { ref, watch, h, resolveComponent, computed } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson'
import Default from '@/layouts/default.vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import CategoryMenu from '../inner/categoryMenu.vue' import CategoryMenu from '../inner/CategoryMenu.vue'
import { useCustomerProductStore } from '@/stores/customer/customer-product'
import type { Product } from '@/stores/customer/customer-product'
interface Product {
reference: number
product_id: number
name: string
image_link: string
link_rewrite: string
}
const customerProductStore = useCustomerProductStore()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -101,18 +96,6 @@ const sortField = computed({
} }
}) })
const perPage = ref(15)
const total = ref(0)
interface ApiResponse {
message: string
items: Product[]
count: number
}
const productsList = ref<Product[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const filters = computed<Record<string, string>>({ const filters = computed<Record<string, string>>({
get: () => { get: () => {
const q = { ...route.query } const q = { ...route.query }
@@ -157,36 +140,16 @@ const updateFilter = debounce((columnId: string, val: string) => {
filters.value = newFilters filters.value = newFilters
}, 400) }, 400)
async function fetchProductList() {
loading.value = true
error.value = null
const params = new URLSearchParams()
Object.entries(route.query).forEach(([key, value]) => {
if (value) params.append(key, String(value))
})
const url = `/api/v1/restricted/list/list-products?${params}`
try {
const response = await useFetchJson<ApiResponse>(url)
productsList.value = response.items || []
total.value = response.count || 0
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load products'
} finally {
loading.value = false
}
}
function goToProduct(productId: number) { function goToProduct(productId: number) {
router.push({ router.push({
name: 'product-detail', name: 'customer-product-details',
params: { id: productId } params: { product_id: productId }
}) })
} }
const selectedCount = ref({ const selectedCount = ref({
product_id: null, product_id: null,
count: 0 count: 0
@@ -205,7 +168,7 @@ const UInput = resolveComponent('UInput')
const UButton = resolveComponent('UButton') const UButton = resolveComponent('UButton')
const UIcon = resolveComponent('UIcon') const UIcon = resolveComponent('UIcon')
const columns: TableColumn<Payment>[] = [ const columns: TableColumn<Product>[] = [
{ {
id: 'expand', id: 'expand',
cell: ({ row }) => cell: ({ row }) =>
@@ -351,6 +314,35 @@ const columns: TableColumn<Payment>[] = [
variant: 'solid' variant: 'solid'
}, 'Add to cart') }, 'Add to cart')
}, },
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UButton, {
onClick: () => {
goToProduct(row.original.product_id)
},
class: 'cursor-pointer',
color: 'info',
variant: 'soft'
}, () => 'Show product')
},
},
{
accessorKey: 'counta',
header: '',
cell: ({ row }) => {
return h(UIcon, {
onClick: () => customerProductStore.toggleFavorite(row.original),
class: [
'cursor-pointer text-[20px] transition-transform duration-200 hover:scale-125',
row.original.is_favorite ? 'text-red-500' : 'text-blue-500'
],
name: 'material-symbols:favorite',
variant: 'soft',
})
}
} }
] ]
@@ -417,13 +409,27 @@ const columnsChild: TableColumn<Payment>[] = [
variant: 'solid' variant: 'solid'
}, 'Add to cart') }, 'Add to cart')
}, },
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UButton, {
onClick: () => {
goToProduct(row.original.product_id)
},
class: 'cursor-pointer',
color: 'info',
variant: 'soft'
}, () => 'Show product')
},
} }
] ]
watch( watch(
() => route.query, () => route.query,
() => { () => {
fetchProductList() customerProductStore.fetchProductList()
}, },
{ immediate: true } { immediate: true }
) )

View File

@@ -1,6 +1,5 @@
<template> <template>
<component :is="Default || 'div'"> <div class="">
<div class="container mx-auto">
<div class="flex flex-col gap-5 mb-6"> <div class="flex flex-col gap-5 mb-6">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Customer Data') }}</h1> <h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Customer Data') }}</h1>
@@ -24,7 +23,7 @@
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-4"> class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-4">
<h2 class="text-xl font-semibold text-black dark:text-white mb-4 flex items-center gap-2"> <h2 class="text-xl font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
<UIcon name="mdi:domain" <UIcon name="mdi:domain"
class="text-[24px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" /> class="text-[24px] text-(--text-sky-light) dark:text-(--text-sky-dark)" />
{{ t('Company Information') }} {{ t('Company Information') }}
</h2> </h2>
<div class="grid grid-cols-1 gap-10"> <div class="grid grid-cols-1 gap-10">
@@ -65,7 +64,7 @@
<h2 <h2
class="text-xl font-semibold text-black dark:text-white mb-2 flex items-center gap-2"> class="text-xl font-semibold text-black dark:text-white mb-2 flex items-center gap-2">
<UIcon name="mdi:map-marker" <UIcon name="mdi:map-marker"
class="text-[24px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" /> class="text-[24px] text-(--text-sky-light) dark:text-(--text-sky-dark)" />
{{ t('Addresses') }} {{ t('Addresses') }}
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@@ -88,7 +87,7 @@
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<UButton color="primary" variant="outline" <UButton color="primary" variant="outline"
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) border-(--accent-blue-light) dark:border-(--accent-blue-dark)" class="text-(--text-sky-light) dark:text-(--text-sky-dark) border-(--accent-blue-light) dark:border-(--accent-blue-dark)"
@click="goToCreateAccount"> @click="goToCreateAccount">
<UIcon name="ic:sharp-edit" /> <UIcon name="ic:sharp-edit" />
{{ t('Edit Account') }} {{ t('Edit Account') }}
@@ -97,16 +96,14 @@
</div> </div>
</div> </div>
</div> </div>
</component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useCustomerStore } from '@/stores/customer' import { useCustomerStore } from '@/stores/customer'
import { useAddressStore } from '@/stores/address' import { useAddressStore } from '@/stores/customer/address'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Default from '@/layouts/default.vue'
const router = useRouter() const router = useRouter()
const customerStore = useCustomerStore() const customerStore = useCustomerStore()
const addressStore = useAddressStore() const addressStore = useAddressStore()

View File

@@ -1,6 +1,5 @@
<template> <template>
<component :is="Default || 'div'"> <div class="">
<div class="container mx-auto">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="flex flex-col gap-5 mb-6"> <div class="flex flex-col gap-5 mb-6">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Create Account') }}</h1> <h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Create Account') }}</h1>
@@ -11,7 +10,7 @@
<h2 <h2
class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2"> class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
<UIcon name="mdi:domain" <UIcon name="mdi:domain"
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" /> class="text-[20px] text-(--text-sky-light) dark:text-(--text-sky-dark)" />
{{ t('Company Information') }} {{ t('Company Information') }}
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -54,7 +53,7 @@
<h2 <h2
class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2"> class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
<UIcon name="mdi:map-marker" <UIcon name="mdi:map-marker"
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" /> class="text-[20px] text-(--text-sky-light) dark:text-(--text-sky-dark)" />
{{ t('Select Addresses') }} {{ t('Select Addresses') }}
</h2> </h2>
<div class="bg-(--second-light) dark:bg-(--main-dark)"> <div class="bg-(--second-light) dark:bg-(--main-dark)">
@@ -70,7 +69,7 @@
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20' ? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'"> : 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="address.id" v-model="selectedAddress" <input type="radio" :value="address.id" v-model="selectedAddress"
class="mt-1 w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" /> class="mt-1 w-4 h-4 text-(--text-sky-light) dark:text-(--text-sky-dark)" />
<div class="flex-1"> <div class="flex-1">
<p class="text-black dark:text-white font-medium">{{ address.street }} <p class="text-black dark:text-white font-medium">{{ address.street }}
</p> </p>
@@ -87,7 +86,7 @@
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" /> <UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p> <p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
<RouterLink :to="{ name: 'addresses' }" <RouterLink :to="{ name: 'addresses' }"
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline"> class="inline-block mt-2 text-(--text-sky-light) dark:text-(--text-sky-dark) hover:underline">
{{ t('Add Address') }} {{ t('Add Address') }}
</RouterLink> </RouterLink>
</div> </div>
@@ -109,17 +108,15 @@
</div> </div>
</div> </div>
</div> </div>
</component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useCustomerStore } from '@/stores/customer' import { useCustomerStore } from '@/stores/customer'
import { useAddressStore } from '@/stores/address' import { useAddressStore } from '@/stores/customer/address'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useCartStore } from '@/stores/cart' import { useCartStore } from '@/stores/customer/cart'
import Default from '@/layouts/default.vue'
const router = useRouter() const router = useRouter()
const customerStore = useCustomerStore() const customerStore = useCustomerStore()
const addressStore = useAddressStore() const addressStore = useAddressStore()

View File

@@ -1,9 +1,6 @@
<template> <template>
<component :is="Default || 'div'">
Statistic page Statistic page
</component>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Default from '@/layouts/default.vue'
</script> </script>

View File

@@ -0,0 +1,158 @@
<template>
<div class="p-4">
<div v-if="loading" class="flex justify-center py-8">
<ULoader />
</div>
<div v-else-if="error" class="text-red-500">
{{ error }}
</div>
<UTree v-if="showTree" :items="treeItems" v-model:expanded="expandedFolders" :key="treeKey" @toggle="onToggle" :get-key="item => item.value">
<template #item-wrapper="{ item }">
<div class="flex items-start cursor-pointer" @click.stop="!item.isFolder">
<div class="flex items-center gap-1">
<UIcon :name="item.icon" :size="30" />
<div class="flex gap-1 items-center">
<span class="text-[15px] font-medium">
{{ item.label }}
</span>
<UButton v-if="!item.isFolder && item.fileName" size="xxs" color="neutral"
variant="outline" icon="i-lucide-download"
@click.stop="downloadFile(item.path, item.fileName)" :ui="{ base: 'ring-0!' }" />
</div>
</div>
</div>
</template>
</UTree>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson'
interface FileItemRaw {
Name: string
IsFolder: boolean
}
interface FileItem {
name: string
type: 'file' | 'folder'
}
interface TreeItem {
label: string
icon: string
children?: TreeItem[]
isFolder: boolean
path: string
value: string
fileName?: string
}
const props = defineProps<{ initialPath?: string }>()
const currentPath = ref(props.initialPath || '')
const allData = ref<Record<string, FileItem[]>>({})
const expandedFolders = ref<string[]>([])
const treeKey = ref(0)
const loading = ref(false)
const error = ref<string | null>(null)
const showTree = computed(() => !error.value)
async function fetchFolderContents(path: string): Promise<FileItem[]> {
const url = `/api/v1/restricted/storage/list-content/${path}`
const data = await useFetchJson<FileItemRaw[]>(url)
return (data.items || []).map(i => ({
name: i.Name,
type: i.IsFolder ? 'folder' : 'file'
}))
}
async function loadFolder(path: string) {
if (allData.value[path]) return
try {
const items = await fetchFolderContents(path)
allData.value[path] = items
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load folder contents'
}
}
function buildTree(path: string): TreeItem[] {
const items = allData.value[path] || []
return items.map(item => {
const itemPath = path ? `${path}/${item.name}` : item.name
const isFolder = item.type === 'folder'
const isExpanded = expandedFolders.value.includes(itemPath)
const isLoaded = !!allData.value[itemPath]
return {
label: item.name,
icon: isFolder ? 'fxemoji:folder' : 'flat-color-icons:file',
isFolder,
path: isFolder ? itemPath : path,
value: itemPath,
fileName: isFolder ? undefined : item.name,
children: isFolder && isExpanded && isLoaded
? buildTree(itemPath)
: []
}
})
}
const treeItems = computed(() => buildTree(currentPath.value))
async function toggleFolder(item: TreeItem) {
if (!item.isFolder) return
const isOpen = expandedFolders.value.includes(item.value)
if (!isOpen) {
await loadFolder(item.value)
treeKey.value++
} else {
treeKey.value++
}
}
function onToggle(_event: unknown, item: TreeItem) {
console.log('Toggle:', item)
if (item.isFolder) {
toggleFolder(item)
}
}
async function downloadFile(path: string, fileName: string) {
try {
const response = await fetch(`/api/v1/restricted/storage/download-file/${path}/${fileName}`)
if (!response.ok) throw new Error('Download failed')
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(url)
} catch (e) {
console.error(e)
alert('Download failed')
}
}
loadFolder(currentPath.value)
</script>

View File

@@ -1,5 +1,7 @@
<template> <template>
<UNavigationMenu orientation="vertical" type="single" :items="items" class="data-[orientation=vertical]:w-72" /> <UNavigationMenu orientation="vertical" type="single" :items="items" class="data-[orientation=vertical]:w-72" :ui="{
root: ''
}" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -37,9 +39,10 @@ function adaptMenu(menu: NavigationMenuItem[]) {
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
item.open = path && path.includes(item.category_id) ? true : openAll.value item.open = path && path.includes(item.category_id) ? true : openAll.value
adaptMenu(item.children); adaptMenu(item.children);
item.children.unshift({ item.children.unshift({
label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: {
name: 'customer-products-category', params: { name: item.params.to, params: {
category_id: item.params.category_id, category_id: item.params.category_id,
link_rewrite: item.params.link_rewrite link_rewrite: item.params.link_rewrite
} }
@@ -47,7 +50,7 @@ function adaptMenu(menu: NavigationMenuItem[]) {
}) })
} else { } else {
item.to = { item.to = {
name: 'customer-products-category', params: { name: item.params.to, params: {
category_id: item.params.category_id, category_id: item.params.category_id,
link_rewrite: item.params.link_rewrite link_rewrite: item.params.link_rewrite
} }

View File

@@ -0,0 +1,53 @@
<template>
<USelectMenu v-model="country" :items="countries" value-key="id"
class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
<template #default>
<div class="flex flex-col items-start leading-tight">
<span class="text-xs text-gray-400">
Country/Currency
</span>
<span v-if="country" class="font-medium dark:text-white text-black">
{{ country?.name || 'Country' }} / {{ country?.ps_currency || 'Currency' }}
</span>
</div>
</template>
<template #item-leading="{ item }">
<div class="flex items-center gap-2 cursor-pointer">
<span class="text-lg">{{ item.flag }}</span>
<span class="font-medium dark:text-white text-black">
{{ item.name }}
</span>
</div>
</template>
</USelectMenu>
</template>
<script setup lang="ts">
import { countries, currentCountry, switchLocalization } from '@/router/langs'
import { useCookie } from '@/composable/useCookie'
import { computed, watch } from 'vue'
const cookie = useCookie()
const country = computed({
get() {
return currentCountry.value
},
set(value: string) {
currentCountry.value = countries.find((x) => x.id == Number(value))
cookie.setCookie('country_id', `${countries.find((x) => x.id == Number(value))?.id}`, { days: 60, secure: true, sameSite: 'Lax' })
switchLocalization()
},
})
watch(
() => country,
(newCountry) => {
if (newCountry) {
currentCountry.value = countries.find((x) => x.id == Number(newCountry.value?.id))
}
},
{ immediate: true },
)
</script>

View File

@@ -1,34 +1,47 @@
<template> <template>
<USelectMenu v-model="locale" :items="langs" <USelectMenu v-model="locale" :items="langs" value-key="iso_code"
class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!" valueKey="iso_code" class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
:searchInput="false"> <template #default>
<template #default="{ modelValue }"> <div class="flex items-center gap-2">
<div class="flex items-center gap-1"> <!-- <span class="text-lg">{{ selectedLang?.flag }}</span> -->
<!-- <span class="text-md dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.flag}}</span> -->
<span class="font-medium dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.name}}</span> <div class="flex flex-col leading-tight items-start">
<span class="text-xs text-gray-400">
Language
</span>
<span class="font-medium dark:text-white text-black">
{{ selectedLang?.name || 'Select language' }}
</span>
</div>
</div> </div>
</template> </template>
<template #item-leading="{ item }"> <template #item-leading="{ item }">
<div class="flex items-center rounded-md cursor-pointer transition-colors"> <div class="flex items-center gap-2">
<!-- <span class="text-md ">{{ item.flag }}</span> --> <span class="text-lg">{{ item.flag }}</span>
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span> <span class="font-medium dark:text-white text-black">
{{ item.name }}
</span>
</div> </div>
</template> </template>
</USelectMenu> </USelectMenu>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { langs, currentLang } from '@/router/langs' import { langs, currentLang, switchLocalization } from '@/router/langs'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useCookie } from '@/composable/useCookie' import { useCookie } from '@/composable/useCookie'
import { computed, watch } from 'vue' import { computed, watch } from 'vue'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import { useFetchJson } from '@/composable/useFetchJson'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const cookie = useCookie() const selectedLang = computed(() =>
langs.find(item => item.iso_code === locale.value)
)
const cookie = useCookie()
const locale = computed({ const locale = computed({
get() { get() {
return currentLang.value?.iso_code || i18n.locale.value return currentLang.value?.iso_code || i18n.locale.value
@@ -52,21 +65,10 @@ const locale = computed({
} }
} }
changeLang() switchLocalization()
}, },
}) })
async function changeLang() {
try {
const { items } = await useFetchJson('/api/v1/public/auth/update-choice', {
method: 'POST'
})
} catch (error) {
console.log(error)
}
}
watch( watch(
() => route.params.locale, () => route.params.locale,
(newLocale) => { (newLocale) => {

View File

@@ -0,0 +1,160 @@
<template>
<UEditor v-slot="{ editor }" v-model="localValue" content-type="html"
:ui="{ base: 'p-8 sm:px-16', root: 'p-2' }"
class="min-w-full border rounded-md bg-white! border-(--border-light)" placeholder="Write there ...">
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8 flex-wrap!">
<template #link>
<EditorLinkPopover :editor="editor" auto-open />
</template>
</UEditorToolbar>
</UEditor>
</template>
<script lang="ts" setup>
import type { EditorToolbarItem } from '@nuxt/ui';
import { computed } from 'vue';
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const localValue = computed({
get: () => props.modelValue,
set: (value: string) => emit('update:modelValue', value)
})
const toolbarItems: EditorToolbarItem[][] = [
// History controls
[{
kind: 'undo',
icon: 'i-lucide-undo',
tooltip: { text: 'Undo' }
}, {
kind: 'redo',
icon: 'i-lucide-redo',
tooltip: { text: 'Redo' }
}],
// Block types
[{
icon: 'i-lucide-heading',
tooltip: { text: 'Headings' },
content: {
align: 'start'
},
items: [{
kind: 'heading',
level: 1,
icon: 'i-lucide-heading-1',
label: 'Heading 1'
}, {
kind: 'heading',
level: 2,
icon: 'i-lucide-heading-2',
label: 'Heading 2'
}, {
kind: 'heading',
level: 3,
icon: 'i-lucide-heading-3',
label: 'Heading 3'
}, {
kind: 'heading',
level: 4,
icon: 'i-lucide-heading-4',
label: 'Heading 4'
}]
}, {
icon: 'i-lucide-list',
tooltip: { text: 'Lists' },
content: {
align: 'start'
},
items: [{
kind: 'bulletList',
icon: 'i-lucide-list',
label: 'Bullet List'
}, {
kind: 'orderedList',
icon: 'i-lucide-list-ordered',
label: 'Ordered List'
}]
}, {
kind: 'blockquote',
icon: 'i-lucide-text-quote',
tooltip: { text: 'Blockquote' }
}, {
kind: 'codeBlock',
icon: 'i-lucide-square-code',
tooltip: { text: 'Code Block' }
}, {
kind: 'horizontalRule',
icon: 'i-lucide-separator-horizontal',
tooltip: { text: 'Horizontal Rule' }
}],
// Text formatting
[{
kind: 'mark',
mark: 'bold',
icon: 'i-lucide-bold',
tooltip: { text: 'Bold' }
}, {
kind: 'mark',
mark: 'italic',
icon: 'i-lucide-italic',
tooltip: { text: 'Italic' }
}, {
kind: 'mark',
mark: 'underline',
icon: 'i-lucide-underline',
tooltip: { text: 'Underline' }
}, {
kind: 'mark',
mark: 'strike',
icon: 'i-lucide-strikethrough',
tooltip: { text: 'Strikethrough' }
}, {
kind: 'mark',
mark: 'code',
icon: 'i-lucide-code',
tooltip: { text: 'Code' }
}],
// Link
[{
kind: 'link',
icon: 'i-lucide-link',
tooltip: { text: 'Link' }
}],
// Text alignment
[{
icon: 'i-lucide-align-justify',
tooltip: { text: 'Text Align' },
content: {
align: 'end'
},
items: [{
kind: 'textAlign',
align: 'left',
icon: 'i-lucide-align-left',
label: 'Align Left'
}, {
kind: 'textAlign',
align: 'center',
icon: 'i-lucide-align-center',
label: 'Align Center'
}, {
kind: 'textAlign',
align: 'right',
icon: 'i-lucide-align-right',
label: 'Align Right'
}, {
kind: 'textAlign',
align: 'justify',
icon: 'i-lucide-align-justify',
label: 'Align Justify'
}]
}]
]
</script>

View File

@@ -0,0 +1,37 @@
<template>
<UButton variant="outline" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton>
<!-- <UButton variant="solid" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton>
<UButton variant="soft" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton>
<UButton variant="subtle" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton>
<UButton variant="ghost" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton>
<UButton variant="link" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton> -->
</template>
<script setup lang="ts">
import { useThemeStore } from '@/stores/admin/theme'
const themeStorage = useThemeStore()
</script>

View File

@@ -1,12 +0,0 @@
<template>
<UButton variant="ghost" size="sm" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton>
</template>
<script setup lang="ts">
import { useThemeStore } from '@/stores/theme'
const themeStorage = useThemeStore()
</script>

View File

@@ -29,7 +29,7 @@ export async function useFetchJson<T = unknown>(url: string, opt?: RequestInit):
// Handle 401 — access token expired; try to refresh via the HTTPOnly refresh_token cookie // Handle 401 — access token expired; try to refresh via the HTTPOnly refresh_token cookie
if (res.status === 401) { if (res.status === 401) {
const { useAuthStore } = await import('@/stores/auth') const { useAuthStore } = await import('../stores/customer/auth')
const authStore = useAuthStore() const authStore = useAuthStore()
const refreshed = await authStore.refreshAccessToken() const refreshed = await authStore.refreshAccessToken()

View File

@@ -1,14 +1,301 @@
<script setup lang="ts">
import TopBar from '@/components/TopBar.vue';
</script>
<template> <template>
<div class="h-screen grid grid-rows-[auto_1fr_auto]"> <div class="flex flex-1 overflow-x-hidden h-svh">
<main class="pt-20 pb-10"> <USidebar v-model:open="open" collapsible="icon" rail :ui="{
<TopBar /> container: 'h-full z-80',
<div class="container mx-auto px-4"> inner: 'bg-elevated/25 divide-transparent',
body: 'py-0'
}">
<template #header>
<UDropdownMenu :items="teamsItems" :content="{ align: 'start', collisionPadding: 12 }"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
<UButton v-bind="selectedTeam" trailing-icon="i-lucide-chevrons-up-down" color="neutral" variant="ghost"
square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
trailingIcon: 'text-dimmed ms-auto'
}" />
</UDropdownMenu>
</template>
<template #default="{ state }">
<UNavigationMenu :key="state" :items="menuItems" orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }" />
</template>
<template #footer>
<UDropdownMenu :items="userItems" :content="{ align: 'center', collisionPadding: 12 }"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
<UButton v-bind="userStore.user" :label="userStore.user?.email" trailing-icon="i-lucide-chevrons-up-down"
color="neutral" variant="ghost" square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
trailingIcon: 'text-dimmed ms-auto'
}" />
</UDropdownMenu>
<!-- first_name: '', last_name: '' -->
</template>
</USidebar>
<div class="flex-1 flex flex-col">
<div class="flex h-(--ui-header-height) shrink-0 items-center justify-between px-4 border-b border-default">
<div class="flex items-center gap-2">
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
@click="open = !open" />
<span class="text-[20px] font-medium">{{ pageTitle }}</span>
</div>
<div class="hidden md:flex items-center gap-12">
<div class="flex items-center gap-2">
<CountryCurrencySwitch />
<LangSwitch />
</div>
<div class="flex items-center gap-2">
<ThemeSwitch />
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark) whitespace-nowrap">
{{ $t('general.logout') }}
<UIcon name="ic:baseline-logout" class="text-red-700 dark:text-red-500"/>
</button>
</div>
</div>
</div>
<div class="flex-1 p-4 bg-slate-50 dark:bg-(--main-dark)">
<slot /> <slot />
</div> </div>
</main> </div>
</div> </div>
</template> </template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useColorMode } from '@vueuse/core'
import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
import { defineShortcuts, extractShortcuts } from '@nuxt/ui/runtime/composables/defineShortcuts.js'
import { useAuthStore } from '../stores/customer/auth'
const authStore = useAuthStore()
const userStore = useUserStore()
const route = useRoute()
const pageTitle = computed(() => route.meta.name ?? 'Default Page')
await userStore.getUser()
const open = ref(true)
const colorMode = useColorMode()
const teams = ref([
{
label: 'Nuxt',
avatar: {
src: 'https://github.com/nuxt.png',
alt: 'Nuxt'
}
},
{
label: 'Vue',
avatar: {
src: 'https://github.com/vuejs.png',
alt: 'Vue'
}
},
{
label: 'UnJS',
avatar: {
src: 'https://github.com/unjs.png',
alt: 'UnJS'
}
}
])
const selectedTeam = ref(teams.value[0])
const teamsItems = computed<DropdownMenuItem[][]>(() => {
return [
teams.value.map((team, index) => ({
...team,
kbds: ['meta', String(index + 1)],
onSelect() {
selectedTeam.value = team
}
})),
[
{
label: 'Create team',
icon: 'i-lucide-circle-plus'
}
]
]
})
function getItems(state: 'collapsed' | 'expanded') {
return [
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Issues',
icon: 'i-lucide-square-dot'
},
{
label: 'Activity',
icon: 'i-lucide-square-activity'
},
{
label: 'Settings',
icon: 'i-lucide-settings',
defaultOpen: true,
children:
state === 'expanded'
? [
{
label: 'General',
icon: 'i-lucide-house'
},
{
label: 'Team',
icon: 'i-lucide-users'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
}
]
: []
}
] satisfies NavigationMenuItem[]
}
//
import { useRoute, useRouter } from 'vue-router'
import { currentLang } from '@/router/langs'
import { useFetchJson } from '@/composable/useFetchJson'
import CountryCurrencySwitch from '@/components/inner/CountryCurrencySwitch.vue'
import LangSwitch from '@/components/inner/LangSwitch.vue'
import ThemeSwitch from '@/components/inner/ThemeSwitch.vue'
import type { LabelTrans, TopMenuItem } from '@/types'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const menu = ref<TopMenuItem[] | null>(null)
async function getTopMenu() {
try {
const { items } = await useFetchJson<TopMenuItem[]>('/api/v1/restricted/menu/get-top-menu')
menu.value = items
} catch (err) {
console.log(err)
}
}
onMounted(() => {
getTopMenu()
})
const menuItems = computed(() => {
if (!menu.value?.length) return []
return transformMenu(
menu.value || [],
currentLang.value?.iso_code
)
})
function transformMenu(
items: TopMenuItem[],
locale: string | undefined
): NavigationMenuItem[] {
return items.map((item) => {
const route: NavigationMenuItem = {
icon: item.label.icon || 'i-lucide-house',
label:
item.label.trans?.[locale as keyof LabelTrans]?.label ||
item.label.trans?.en?.label ||
'—',
children: item.children
? transformMenu(item.children, locale)
: undefined,
onSelect: () => {
router.push({
name: item.params.route.name,
params: {
...(item.params.route.params || {}),
locale: currentLang.value?.iso_code
}
})
}
}
return route
})
}
const userItems = computed<DropdownMenuItem[][]>(() => [
[
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-settings',
to: '/settings'
}
],
[
{
label: 'Appearance',
icon: 'i-lucide-sun-moon',
children: [
{
label: 'Light',
icon: 'i-lucide-sun',
type: 'checkbox',
checked: colorMode.value === 'light',
onUpdateChecked(checked: boolean) {
if (checked) {
colorMode.preference = 'light'
}
},
onSelect(e: Event) {
e.preventDefault()
}
},
{
label: 'Dark',
icon: 'i-lucide-moon',
type: 'checkbox',
checked: colorMode.value === 'dark',
onUpdateChecked(checked: boolean) {
if (checked) {
colorMode.preference = 'dark'
}
},
onSelect(e: Event) {
e.preventDefault()
}
}
]
}
],
[
{
label: 'GitHub',
icon: 'i-simple-icons-github',
to: 'https://github.com/nuxt/ui',
target: '_blank'
},
{
label: 'Log out',
icon: 'i-lucide-log-out'
}
]
])
defineShortcuts(extractShortcuts(teamsItems.value))
</script>

View File

@@ -0,0 +1,309 @@
<template>
<div class="flex flex-1 overflow-x-hidden h-svh">
<USidebar v-model:open="open" collapsible="icon" rail :ui="{
container: 'h-full z-80',
inner: 'bg-elevated/25 divide-transparent',
body: 'py-0'
}">
<template #header>
<UDropdownMenu :items="teamsItems" :content="{ align: 'start', collisionPadding: 12 }"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
<UButton v-bind="selectedTeam" trailing-icon="i-lucide-chevrons-up-down" color="neutral"
variant="ghost" square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
trailingIcon: 'text-dimmed ms-auto'
}" />
</UDropdownMenu>
</template>
<template #default="{ state }">
<UNavigationMenu :key="state" :items="menuItems" orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }" />
</template>
<template #footer>
<UDropdownMenu :items="userItems" :content="{ align: 'center', collisionPadding: 12 }"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
<UButton v-bind="userStore.user" :label="userStore.user?.email"
trailing-icon="i-lucide-chevrons-up-down" color="neutral" variant="ghost" square
class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
trailingIcon: 'text-dimmed ms-auto'
}" />
</UDropdownMenu>
<!-- first_name: '', last_name: '' -->
</template>
</USidebar>
<div class="flex-1 flex flex-col">
<div class="flex h-(--ui-header-height) shrink-0 items-center justify-between px-4 border-b border-default">
<div class="flex items-center gap-2">
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
@click="open = !open" />
<p class="font-bold text-xl">Customer-Management: <span class="text-[20px] font-medium">{{ pageTitle
}}</span></p>
</div>
<div class="hidden md:flex items-center gap-12">
<div class="flex items-center gap-2">
<CountryCurrencySwitch />
<LangSwitch />
</div>
<div class="flex items-center gap-2">
<ThemeSwitch />
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark) whitespace-nowrap">
{{ $t('general.logout') }}
<UIcon name="ic:baseline-logout" class="text-red-700 dark:text-red-500" />
</button>
</div>
</div>
</div>
<div class="flex-1 p-4 bg-slate-50 dark:bg-(--main-dark)">
<slot />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useColorMode } from '@vueuse/core'
import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
import { defineShortcuts, extractShortcuts } from '@nuxt/ui/runtime/composables/defineShortcuts.js'
import { useAuthStore } from '../stores/customer/auth'
import { useRoute } from 'vue-router'
const route = useRoute()
const pageTitle = computed(() => route.meta.name ?? 'Default Page')
const authStore = useAuthStore()
const userStore = useUserStore()
const open = ref(true)
const colorMode = useColorMode()
const teams = ref([
{
label: 'Nuxt',
avatar: {
src: 'https://github.com/nuxt.png',
alt: 'Nuxt'
}
},
{
label: 'Vue',
avatar: {
src: 'https://github.com/vuejs.png',
alt: 'Vue'
}
},
{
label: 'UnJS',
avatar: {
src: 'https://github.com/unjs.png',
alt: 'UnJS'
}
}
])
const selectedTeam = ref(teams.value[0])
const teamsItems = computed<DropdownMenuItem[][]>(() => {
return [
teams.value.map((team, index) => ({
...team,
kbds: ['meta', String(index + 1)],
onSelect() {
selectedTeam.value = team
}
})),
[
{
label: 'Create team',
icon: 'i-lucide-circle-plus'
}
]
]
})
function getItems(state: 'collapsed' | 'expanded') {
return [
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Issues',
icon: 'i-lucide-square-dot'
},
{
label: 'Activity',
icon: 'i-lucide-square-activity'
},
{
label: 'Settings',
icon: 'i-lucide-settings',
defaultOpen: true,
children:
state === 'expanded'
? [
{
label: 'General',
icon: 'i-lucide-house'
},
{
label: 'Team',
icon: 'i-lucide-users'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
}
]
: []
}
] satisfies NavigationMenuItem[]
}
//
import { useRouter } from 'vue-router'
import { currentLang } from '@/router/langs'
import { useFetchJson } from '@/composable/useFetchJson'
import CountryCurrencySwitch from '@/components/inner/CountryCurrencySwitch.vue'
import LangSwitch from '@/components/inner/LangSwitch.vue'
import ThemeSwitch from '@/components/inner/ThemeSwitch.vue'
import type { LabelTrans, TopMenuItem } from '@/types'
import { useUserStore } from '@/stores/user'
import { watch } from 'vue'
const router = useRouter()
const menu = ref<TopMenuItem[] | null>(null)
const Id = Number(route.params.user_id)
async function cmGetTopMenu() {
try {
const { items } = await useFetchJson<TopMenuItem[]>(`/api/v1/restricted/menu/get-top-menu?target_user_id=${Id}`)
menu.value = items
} catch (err) {
console.log(err)
}
}
watch(
() => route.params.user_id,
() => {
if (route.params.user_id) {
cmGetTopMenu()
}
},
{ immediate: true }
)
const menuItems = computed(() => {
if (!menu.value?.length) return []
return transformMenu(
menu.value || [],
currentLang.value?.iso_code
)
})
function transformMenu(
items: TopMenuItem[],
locale: string | undefined
): NavigationMenuItem[] {
return items.map((item) => {
const route: NavigationMenuItem = {
icon: item.label.icon || 'i-lucide-house',
label:
item.label.trans?.[locale as keyof LabelTrans]?.label ||
item.label.trans?.en?.label ||
'—',
children: item.children
? transformMenu(item.children, locale)
: undefined,
onSelect: () => {
router.push({
name: item.params.route.name,
params: {
...(item.params.route.params || {}),
locale: currentLang.value?.iso_code
}
})
}
}
return route
})
}
const userItems = computed<DropdownMenuItem[][]>(() => [
[
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-settings',
to: '/settings'
}
],
[
{
label: 'Appearance',
icon: 'i-lucide-sun-moon',
children: [
{
label: 'Light',
icon: 'i-lucide-sun',
type: 'checkbox',
checked: colorMode.value === 'light',
onUpdateChecked(checked: boolean) {
if (checked) {
colorMode.preference = 'light'
}
},
onSelect(e: Event) {
e.preventDefault()
}
},
{
label: 'Dark',
icon: 'i-lucide-moon',
type: 'checkbox',
checked: colorMode.value === 'dark',
onUpdateChecked(checked: boolean) {
if (checked) {
colorMode.preference = 'dark'
}
},
onSelect(e: Event) {
e.preventDefault()
}
}
]
}
],
[
{
label: 'GitHub',
icon: 'i-simple-icons-github',
to: 'https://github.com/nuxt/ui',
target: '_blank'
},
{
label: 'Log out',
icon: 'i-lucide-log-out'
}
]
])
defineShortcuts(extractShortcuts(teamsItems.value))
</script>

View File

@@ -1,11 +1,12 @@
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import { initLangs, langs } from '@/router/langs' import { initCountryCurrency, initLangs, langs } from '@/router/langs'
import { watch } from 'vue' import { watch } from 'vue'
import { createI18n, type PathValue } from 'vue-i18n' import { createI18n, type PathValue } from 'vue-i18n'
// const x = // const x =
await initLangs() await initLangs()
await initCountryCurrency()
export const i18ninstall = createI18n({ export const i18ninstall = createI18n({
legacy: false, // you must set `false`, to use Composition API legacy: false, // you must set `false`, to use Composition API
locale: 'en', locale: 'en',

View File

@@ -1,24 +1,42 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { currentLang, langs } from './langs' import { currentLang, langs, switchLocalization } from './langs'
import { getSettings } from './settings' import { getSettings } from './settings'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/customer/auth'
import { getRoutes } from './menu' import { getRoutes } from './menu'
function isAuthenticated(): boolean { function isAuthenticated(): boolean {
if (typeof document === 'undefined') return false if (typeof document === 'undefined') return false
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1') return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
} }
await switchLocalization()
await getSettings() await getSettings()
const routes = await getRoutes() const routes = await getRoutes()
let newRoutes = [] let newRoutes = []
function getLayoutFromComponent(path: string) {
const emptyLayouts = [
'LoginView.vue',
'RegisterView.vue',
'PasswordRecoveryView.vue',
'VerifyEmailView.vue',
'ResetPasswordForm.vue'
]
return emptyLayouts.some((name) => path.includes(name)) ? 'empty' : 'default'
}
for (let r of routes) { for (let r of routes) {
const component = () => import(/* @vite-ignore */ `..${r.component}`) const component = () => import(/* @vite-ignore */ `..${r.component}`)
const parsedMeta = r.meta ? JSON.parse(r.meta) : {}
const layout = parsedMeta.layout ?? getLayoutFromComponent(r.component)
newRoutes.push({ newRoutes.push({
path: r.path, path: r.path,
component, component,
name: r.name, name: r.name,
meta: r.meta ? JSON.parse(r.meta) : {}, meta: {
...parsedMeta,
layout,
},
}) })
} }
@@ -72,14 +90,22 @@ async function setRoutes() {
} }
const importedComponent = (await importer()).default const importedComponent = (await importer()).default
const parsedMeta = item.meta ? JSON.parse(item.meta) : {}
const layout = parsedMeta.layout ?? getLayoutFromComponent(item.component)
router.addRoute('locale', { router.addRoute('locale', {
path: item.path, path: item.path,
component: importedComponent, component: importedComponent,
name: item.name, name: item.name,
meta: item.meta ? JSON.parse(item.meta) : {} meta: {
...parsedMeta,
layout,
}
}) })
} }
console.log(router);
// await router.replace(router.currentRoute.value.fullPath)
} }
await setRoutes() await setRoutes()

View File

@@ -1,12 +1,16 @@
import { useCookie } from "@/composable/useCookie" import { useCookie } from "@/composable/useCookie"
import { useFetchJson } from "@/composable/useFetchJson" import { useFetchJson } from "@/composable/useFetchJson"
import type { Language } from "@/types" import type { Country, Language } from "@/types"
import { reactive, ref } from "vue" import { reactive, ref } from "vue"
export const langs = reactive([] as Language[]) export const langs = reactive([] as Language[])
export const currentLang = ref<Language>() export const currentLang = ref<Language>()
const deflang = ref<Language>() export const countries = reactive([] as Country[])
export const currentCountry = ref<Country>()
const defLang = ref<Language>()
const defCountry = ref<Country>()
const cookie = useCookie() const cookie = useCookie()
// Get available language codes for route matching // Get available language codes for route matching
// export const availableLocales = computed(() => langs.map((l) => l.lang_code)) // export const availableLocales = computed(() => langs.map((l) => l.lang_code))
@@ -22,9 +26,42 @@ export async function initLangs() {
if (cc) { if (cc) {
idfromcookie = langs.find((x) => x.id == parseInt(cc)) idfromcookie = langs.find((x) => x.id == parseInt(cc))
} }
deflang.value = items.find((x) => x.is_default == true) defLang.value = items.find((x) => x.is_default == true)
currentLang.value = idfromcookie ?? deflang.value currentLang.value = idfromcookie ?? defLang.value
} catch (error) { } catch (error) {
console.error('Failed to fetch languages:', error) console.error('Failed to fetch languages:', error)
} }
} }
// Initialize country/currency from API
export async function initCountryCurrency() {
try {
const { items } = await useFetchJson<Country[]>('/api/v1/restricted/langs-and-countries/get-countries')
countries.push(...items)
let idfromcookie = null
const cc = cookie.getCookie('country_id')
if (cc) {
idfromcookie = langs.find((x) => x.id == parseInt(cc))
}
defCountry.value = items.find((x) => x.id === defLang.value?.id)
currentCountry.value = idfromcookie ?? defCountry.value
console.log(defCountry.value);
console.log(currentCountry.value);
} catch (error) {
console.error('Failed to fetch languages:', error)
}
}
export async function switchLocalization() {
try {
await useFetchJson('/api/v1/public/auth/update-choice', {
method: 'POST'
})
} catch (error) {
console.log(error)
}
}

View File

@@ -1,144 +0,0 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface AddressFormData {
street: string
zipCode: string
city: string
country: string
}
export interface Address {
id: number
street: string
zipCode: string
city: string
country: string
}
export const useAddressStore = defineStore('address', () => {
const addresses = ref<Address[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const currentPage = ref(1)
const pageSize = 20
const searchQuery = ref('')
function initMockData() {
addresses.value = [
{ id: 1, street: 'Main Street 123', zipCode: '10-001', city: 'New York', country: 'United States' },
{ id: 2, street: 'Oak Avenue 123', zipCode: '90-001', city: 'Los Angeles', country: 'United States' },
{ id: 3, street: 'Pine Road 123', zipCode: '60-601', city: 'Chicago', country: 'United States' }
]
}
const filteredAddresses = computed(() => {
if (!searchQuery.value) return addresses.value
const query = searchQuery.value.toLowerCase()
return addresses.value.filter(addr =>
addr.street.toLowerCase().includes(query) ||
addr.city.toLowerCase().includes(query) ||
addr.country.toLowerCase().includes(query) ||
addr.zipCode.toLowerCase().includes(query)
)
})
const totalItems = computed(() => filteredAddresses.value.length)
const totalPages = computed(() => Math.ceil(totalItems.value / pageSize))
const paginatedAddresses = computed(() => {
const start = (currentPage.value - 1) * pageSize
return filteredAddresses.value.slice(start, start + pageSize)
})
function getAddressById(id: number) {
return addresses.value.find(addr => addr.id === id)
}
function normalize(data: AddressFormData): AddressFormData {
return {
street: data.street.trim(),
zipCode: data.zipCode.trim(),
city: data.city.trim(),
country: data.country.trim()
}
}
function generateId(): number {
return Math.max(0, ...addresses.value.map(a => a.id)) + 1
}
function addAddress(formData: AddressFormData): Address {
const newAddress: Address = {
id: generateId(),
...normalize(formData)
}
addresses.value.unshift(newAddress)
resetPagination()
return newAddress
}
function updateAddress(id: number, formData: AddressFormData): boolean {
const index = addresses.value.findIndex(a => a.id === id)
if (index === -1) return false
const existing = addresses.value[index]
if (!existing) return false
addresses.value[index] = {
id: existing.id,
...normalize(formData)
}
return true
}
function deleteAddress(id: number): boolean {
const index = addresses.value.findIndex(a => a.id === id)
if (index === -1) return false
addresses.value.splice(index, 1)
resetPagination()
return true
}
function setPage(page: number) {
currentPage.value = page
}
function setSearchQuery(query: string) {
searchQuery.value = query
currentPage.value = 1
}
function resetPagination() {
currentPage.value = 1
}
initMockData()
return {
addresses,
loading,
error,
currentPage,
pageSize,
totalItems,
totalPages,
searchQuery,
filteredAddresses,
paginatedAddresses,
getAddressById,
addAddress,
updateAddress,
deleteAddress,
setPage,
setSearchQuery,
resetPagination
}
})

View File

@@ -1,6 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import type { Address } from './address'
export interface CustomerData { export interface CustomerData {
companyName: string companyName: string

View File

@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import type { ProductDescription } from '@/types/product' import type { ProductDescription } from '@/types/product'
import { useSettingsStore } from './settings'
export interface Product { export interface Product {
id: number id: number
@@ -23,21 +24,21 @@ export interface ProductResponse {
} }
export const useProductStore = defineStore('product', () => { export const useProductStore = defineStore('product', () => {
const productDescription = ref()
const currentProduct = ref<Product | null>(null)
const loading = ref(false) const loading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const productDescription = ref()
const copyProductDescription = ref()
async function getProductDescription(langId = 1, productID: number) { async function getProductDescription(langId: number | null, productID: number) {
loading.value = true loading.value = true
error.value = null error.value = null
try { try {
const response = await useFetchJson<ProductDescription>( const response = await useFetchJson<ProductDescription>(
`/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}` `/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId ? langId : settingStore.shopDefaultLanguage}`
) )
productDescription.value = response.items productDescription.value = structuredClone(response.items)
copyProductDescription.value = structuredClone(response.items)
} catch (e: unknown) { } catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load product description' error.value = e instanceof Error ? e.message : 'Failed to load product description'
} finally { } finally {
@@ -45,35 +46,13 @@ export const useProductStore = defineStore('product', () => {
} }
} }
async function saveProductDescription(productID?: number) { const settingStore = useSettingsStore()
const id = productID || 1 async function translateProductDescription(productID: number, toLangId: number, model: string = 'Google') {
try {
const data = await useFetchJson(
`/api/v1/restricted/product-description/save-product-description?productID=${id}&productShopID=1&productLangID=1`,
{
method: 'POST',
body: JSON.stringify(
{
description: productDescription.value.description,
description_short: productDescription.value.description_short,
meta_description: productDescription.value.meta_description,
available_now: productDescription.value.available_now,
usage: productDescription.value.usage
})
}
)
return data
} catch (e) {
console.error(e)
}
}
async function translateProductDescription(productID: number, fromLangId: number, toLangId: number) {
loading.value = true loading.value = true
error.value = null error.value = null
try { try {
const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-description/translate-product-description?productID=${productID}&productShopID=1&productFromLangID=${fromLangId}&productToLangID=${toLangId}&model=OpenAI`) const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-translation/translate-product-description?productID=${productID}&productFromLangID=${settingStore.shopDefaultLanguage}&productToLangID=${toLangId}&model=${model}`)
productDescription.value = response.items productDescription.value = response.items
return response.items return response.items
} catch (e: any) { } catch (e: any) {
@@ -84,18 +63,46 @@ export const useProductStore = defineStore('product', () => {
} }
} }
function clearCurrentProduct() { async function saveProductDescription(productID?: number, langId?: number | null) {
currentProduct.value = null const id = productID || 1
const lang = langId || 1
try {
const data = await useFetchJson(
`/api/v1/restricted/product-translation/save-product-description?productID=${id}&productLangID=${lang}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: productDescription.value?.name || '',
description: productDescription.value?.description || '',
description_short: productDescription.value?.description_short || '',
meta_title: productDescription.value?.meta_title || '',
meta_description: productDescription.value?.meta_description || '',
available_now: productDescription.value?.available_now || '',
available_later: productDescription.value?.available_later || '',
usage: productDescription.value?.usage || '',
})
}
)
await getProductDescription(langId, productID)
return data
} catch (e) {
console.error(e)
}
} }
return { return {
productDescription, productDescription,
currentProduct,
loading, loading,
error, error,
getProductDescription, copyProductDescription,
clearCurrentProduct,
saveProductDescription,
translateProductDescription, translateProductDescription,
getProductDescription,
saveProductDescription
} }
}) })

View File

@@ -0,0 +1,21 @@
import { useFetchJson } from '@/composable/useFetchJson'
import type { Resp } from '@/types'
import type { Settings } from '@/types/settings'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useSettingsStore = defineStore('settings', () => {
const settings = ref<Settings | null>(null)
const loaded = ref(false)
const shopDefaultLanguage = computed(() => settings.value?.app?.shop_default_language ?? 1)
async function getSettings(): Promise<Settings | null> {
if (loaded.value && settings.value) return settings.value
const resp = await useFetchJson<Settings>('/api/v1/settings')
settings.value = resp.items
loaded.value = true
return resp.items
}
return { settings, loaded, shopDefaultLanguage, getSettings }
})

View File

@@ -0,0 +1,231 @@
import { useFetchJson } from '@/composable/useFetchJson'
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { AddressTemplate } from './cart'
export interface AddressFormData {
street: string
zipCode: string
city: string
country: string
}
export interface Address {
id: number
street: string
zipCode: string
city: string
country: string
}
export const useAddressStore = defineStore('address', () => {
const loading = ref(false)
const error = ref<string | null>(null)
const currentPage = ref(1)
const pageSize = 20
const searchQuery = ref('')
function initMockData() {
addresses.value = [
{ id: 1, street: 'Main Street 123', zipCode: '10-001', city: 'New York', country: 'United States' },
{ id: 2, street: 'Oak Avenue 123', zipCode: '90-001', city: 'Los Angeles', country: 'United States' },
{ id: 3, street: 'Pine Road 123', zipCode: '60-601', city: 'Chicago', country: 'United States' }
]
}
const filteredAddresses = computed(() => {
if (!searchQuery.value) return addresses.value
const query = searchQuery.value.toLowerCase()
return addresses.value.filter(addr =>
addr.street.toLowerCase().includes(query) ||
addr.city.toLowerCase().includes(query) ||
addr.country.toLowerCase().includes(query) ||
addr.zipCode.toLowerCase().includes(query)
)
})
const totalItems = computed(() => filteredAddresses.value.length)
const totalPages = computed(() => Math.ceil(totalItems.value / pageSize))
function getAddressById(id: number) {
return addresses.value.find(addr => addr.id === id)
}
function normalize(data: AddressFormData): AddressFormData {
return {
street: data.street.trim(),
zipCode: data.zipCode.trim(),
city: data.city.trim(),
country: data.country.trim()
}
}
function generateId(): number {
return Math.max(0, ...addresses.value.map(a => a.id)) + 1
}
function setPage(page: number) {
currentPage.value = page
}
function setSearchQuery(query: string) {
searchQuery.value = query
currentPage.value = 1
}
function resetPagination() {
currentPage.value = 1
}
initMockData()
const addresses = ref<Address[]>([])
const addressLoading = ref(false)
const addressError = ref<string | null>(null)
const addressSearchQuery = ref('')
const addressCurrentPage = ref(1)
const addressPageSize = 20
const addressTotalCount = ref(0)
function transformAddressResponse(address: any): Address {
const info = address.address_info || address.addressInfo || {}
return {
id: address.id ?? 0,
country_id: address.country_id ?? address.countryId ?? 1,
customer_id: address.customer_id ?? address.customerId ?? 0,
address_info: info as Record<string, string>
}
}
async function fetchAddresses() {
addressLoading.value = true
addressError.value = null
try {
const queryParam = addressSearchQuery.value ? `&query=${encodeURIComponent(addressSearchQuery.value)}` : ''
const response = await useFetchJson<Address[]>(`/api/v1/restricted/addresses/retrieve-addresses?page=${addressCurrentPage.value}&elems=${addressPageSize}${queryParam}`)
addresses.value = response.items || []
addressTotalCount.value = response.count ?? addresses.value.length
} catch (error: unknown) {
addressError.value = error instanceof Error ? error.message : 'Failed to load addresses'
} finally {
addressLoading.value = false
}
}
async function addAddress(countryId: number, formData: AddressTemplate): Promise<Address | null> {
addressLoading.value = true
addressError.value = null
try {
const response = await useFetchJson<any>(`/api/v1/restricted/addresses/add-new-address?country_id=${countryId}`, {
method: 'POST',
body: JSON.stringify(formData)
})
await fetchAddresses()
return transformAddressResponse(response.items ?? response)
} catch (error: unknown) {
addressError.value = error instanceof Error ? error.message : 'Failed to create address'
return null
} finally {
addressLoading.value = false
}
}
async function getAddressTemplate(countryId: number): Promise<AddressTemplate> {
const response = await useFetchJson<any>(`/api/v1/restricted/addresses/get-template?country_id=${countryId}`)
return response.items ?? {}
}
async function updateAddress(id: number, countryId: number, formData: AddressTemplate): Promise<boolean> {
addressLoading.value = true
addressError.value = null
try {
await useFetchJson<any>(`/api/v1/restricted/addresses/modify-address?country_id=${countryId}&address_id=${id}`, {
method: 'POST',
body: JSON.stringify(formData)
})
await fetchAddresses()
return true
} catch (error: unknown) {
addressError.value = error instanceof Error ? error.message : 'Failed to update address'
return false
} finally {
addressLoading.value = false
}
}
async function deleteAddress(id: number): Promise<boolean> {
addressLoading.value = true
addressError.value = null
try {
await useFetchJson<any>(`/api/v1/restricted/addresses/delete-address?address_id=${id}`, {
method: 'DELETE'
})
await fetchAddresses()
return true
} catch (error: unknown) {
addressError.value = error instanceof Error ? error.message : 'Failed to delete address'
return false
} finally {
addressLoading.value = false
}
}
const totalAddressItems = computed(() => addressTotalCount.value || addresses.value.length)
const totalAddressPages = computed(() => Math.ceil(totalAddressItems.value / addressPageSize))
const paginatedAddresses = computed(() => addresses.value)
function setAddressPage(page: number) {
addressCurrentPage.value = page
return fetchAddresses()
}
function setAddressSearchQuery(query: string) {
addressSearchQuery.value = query
addressCurrentPage.value = 1
return fetchAddresses()
}
return {
addresses,
loading,
error,
currentPage,
pageSize,
totalItems,
totalPages,
searchQuery,
filteredAddresses,
paginatedAddresses,
getAddressById,
addAddress,
updateAddress,
deleteAddress,
setPage,
setSearchQuery,
resetPagination,
addressLoading,
addressError,
addressSearchQuery,
addressCurrentPage,
addressPageSize,
totalAddressItems,
totalAddressPages,
fetchAddresses,
setAddressPage,
setAddressSearchQuery,
getAddressTemplate
}
})

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import { useUserStore } from '../user'
export interface User { export interface User {
id: string id: string
@@ -41,6 +42,8 @@ export const useAuthStore = defineStore('auth', () => {
_isAuthenticated.value = readIsAuthenticatedCookie() _isAuthenticated.value = readIsAuthenticatedCookie()
} }
// const auth = useAuthStore()
// const userStore = useUserStore()
async function login(email: string, password: string) { async function login(email: string, password: string) {
loading.value = true loading.value = true
error.value = null error.value = null
@@ -60,6 +63,7 @@ export const useAuthStore = defineStore('auth', () => {
user.value = response.user user.value = response.user
_syncAuthState() _syncAuthState()
// await userStore.getUser()
return true return true
} catch (e: any) { } catch (e: any) {

Some files were not shown because too many files have changed in this diff Show More