Compare commits
9 Commits
mailisearc
...
0853424c4e
| Author | SHA1 | Date | |
|---|---|---|---|
| 0853424c4e | |||
| 12e9e49f9b | |||
|
|
25ad592be3 | ||
| a984d2ac0d | |||
|
|
26e6a3c384 | ||
| a4c1773415 | |||
| f60d1bb6de | |||
| 95b73b9836 | |||
| 99fe11fbeb |
4
.env
4
.env
@@ -21,6 +21,10 @@ AUTH_JWT_SECRET=5c020e6ed3d8d6e67e5804d67c83c4bd5ae474df749af6d63d8f20e7e2ba29b3
|
||||
AUTH_JWT_EXPIRATION=86400
|
||||
AUTH_REFRESH_EXPIRATION=604800
|
||||
|
||||
# Meili search
|
||||
MEILISEARCH_URL=http://localhost:7700
|
||||
MEILISEARCH_API_KEY=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
|
||||
# Google Translate Client
|
||||
GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json
|
||||
GOOGLE_CLOUD_PROJECT_ID=translation-343517
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/langsAndCountriesService"
|
||||
"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"
|
||||
)
|
||||
|
||||
// LangsAndCountriesHandler for getting languages and countries data
|
||||
type LangsAndCountriesHandler struct {
|
||||
langsAndCountriesService *langsAndCountriesService.LangsAndCountriesService
|
||||
}
|
||||
|
||||
// NewLangsAndCountriesHandler creates a new LangsAndCountriesHandler instance
|
||||
func NewLangsAndCountriesHandler() *LangsAndCountriesHandler {
|
||||
langsAndCountriesService := langsAndCountriesService.New()
|
||||
return &LangsAndCountriesHandler{
|
||||
langsAndCountriesService: langsAndCountriesService,
|
||||
}
|
||||
}
|
||||
|
||||
func LangsAndCountriesHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewLangsAndCountriesHandler()
|
||||
|
||||
r.Get("/get-languages", handler.GetLanguages)
|
||||
r.Get("/get-countries", handler.GetCountries)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *LangsAndCountriesHandler) GetLanguages(c fiber.Ctx) error {
|
||||
languages, err := h.langsAndCountriesService.GetLanguages()
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&languages, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *LangsAndCountriesHandler) GetCountries(c fiber.Ctx) error {
|
||||
countries, err := h.langsAndCountriesService.GetCountriesAndCurrencies()
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&countries, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
@@ -52,20 +52,13 @@ func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
|
||||
// "override_currency": c.Query("override_currency", ""),
|
||||
// }
|
||||
|
||||
id_shop_attribute := c.Query("shopID")
|
||||
id_shop, err := strconv.Atoi(id_shop_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
listing, err := h.listProductsService.GetListing(uint(id_shop), uint(id_lang), paging, filters)
|
||||
listing, err := h.listProductsService.GetListing(uint(id_lang), paging, filters)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
|
||||
52
app/delivery/web/api/restricted/localeSelector.go
Normal file
52
app/delivery/web/api/restricted/localeSelector.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/localeSelectorService"
|
||||
"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"
|
||||
)
|
||||
|
||||
// LocaleSelectorHandler for getting languages and countries data
|
||||
type LocaleSelectorHandler struct {
|
||||
localeSelectorService *localeSelectorService.LocaleSelectorService
|
||||
}
|
||||
|
||||
// NewLocaleSelectorHandler creates a new LocaleSelectorHandler instance
|
||||
func NewLocaleSelectorHandler() *LocaleSelectorHandler {
|
||||
localeSelectorService := localeSelectorService.New()
|
||||
return &LocaleSelectorHandler{
|
||||
localeSelectorService: localeSelectorService,
|
||||
}
|
||||
}
|
||||
|
||||
func LocaleSelectorHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewLocaleSelectorHandler()
|
||||
|
||||
r.Get("/get-languages", handler.GetLanguages)
|
||||
r.Get("/get-countries", handler.GetCountries)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error {
|
||||
languages, err := h.localeSelectorService.GetLanguages()
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&languages, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *LocaleSelectorHandler) GetCountries(c fiber.Ctx) error {
|
||||
countries, err := h.localeSelectorService.GetCountriesAndCurrencies()
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&countries, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
65
app/delivery/web/api/restricted/meiliSearch.go
Normal file
65
app/delivery/web/api/restricted/meiliSearch.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService"
|
||||
"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 MeiliSearchHandler struct {
|
||||
meiliService *meiliService.MeiliService
|
||||
}
|
||||
|
||||
func NewMeiliSearchHandler() *MeiliSearchHandler {
|
||||
meiliService := meiliService.New()
|
||||
return &MeiliSearchHandler{
|
||||
meiliService: meiliService,
|
||||
}
|
||||
}
|
||||
|
||||
func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewMeiliSearchHandler()
|
||||
|
||||
r.Get("/test", handler.Test)
|
||||
r.Get("/create-index", handler.CreateIndex)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
|
||||
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
err = h.meiliService.CreateIndex(uint(id_lang))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
nothing := ""
|
||||
return c.JSON(response.Make(¬hing, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *MeiliSearchHandler) Test(c fiber.Ctx) error {
|
||||
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
test, err := h.meiliService.Test(uint(id_lang))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&test, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
@@ -31,21 +31,13 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
}
|
||||
|
||||
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
|
||||
|
||||
id_shop_attribute := c.Query("shopID")
|
||||
id_shop, err := strconv.Atoi(id_shop_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
menu, err := h.menuService.GetMenu(uint(id_shop), uint(id_lang))
|
||||
menu, err := h.menuService.GetMenu(uint(id_lang))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
|
||||
@@ -54,13 +54,6 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
productShopID_attribute := c.Query("productShopID")
|
||||
productShopID, err := strconv.Atoi(productShopID_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
productLangID_attribute := c.Query("productLangID")
|
||||
productLangID, err := strconv.Atoi(productLangID_attribute)
|
||||
if err != nil {
|
||||
@@ -68,7 +61,7 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID))
|
||||
description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productLangID))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
@@ -77,7 +70,7 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
|
||||
return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
// SaveProductDescription saves the description for a given product ID, in given shop and language
|
||||
// SaveProductDescription saves the description for a given product ID, in given language
|
||||
func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
@@ -92,13 +85,6 @@ func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
productShopID_attribute := c.Query("productShopID")
|
||||
productShopID, err := strconv.Atoi(productShopID_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
productLangID_attribute := c.Query("productLangID")
|
||||
productLangID, err := strconv.Atoi(productLangID_attribute)
|
||||
if err != nil {
|
||||
@@ -112,7 +98,7 @@ func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
}
|
||||
|
||||
err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID), updates)
|
||||
err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productLangID), updates)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
@@ -136,13 +122,6 @@ func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) err
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
productShopID_attribute := c.Query("productShopID")
|
||||
productShopID, err := strconv.Atoi(productShopID_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
productFromLangID_attribute := c.Query("productFromLangID")
|
||||
productFromLangID, err := strconv.Atoi(productFromLangID_attribute)
|
||||
if err != nil {
|
||||
@@ -163,7 +142,7 @@ func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) err
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productShopID), uint(productFromLangID), uint(productToLangID), aiModel)
|
||||
description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productFromLangID), uint(productToLangID), aiModel)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
|
||||
@@ -97,15 +97,19 @@ func (s *Server) Setup() error {
|
||||
listProducts := s.restricted.Group("/list-products")
|
||||
restricted.ListProductsHandlerRoutes(listProducts)
|
||||
|
||||
// changing the JWT cookies routes (restricted)
|
||||
// in reality it just handles changing user's country and language
|
||||
langsAndCountries := s.restricted.Group("/langs-and-countries")
|
||||
restricted.LangsAndCountriesHandlerRoutes(langsAndCountries)
|
||||
// locale selector (restricted)
|
||||
// this is basically for changing user's selected language and country
|
||||
localeSelector := s.restricted.Group("/langs-and-countries")
|
||||
restricted.LocaleSelectorHandlerRoutes(localeSelector)
|
||||
|
||||
// menu (restricted)
|
||||
menu := s.restricted.Group("/menu")
|
||||
restricted.MenuHandlerRoutes(menu)
|
||||
|
||||
// meili search (restricted)
|
||||
meiliSearch := s.restricted.Group("/meili-search")
|
||||
restricted.MeiliSearchHandlerRoutes(meiliSearch)
|
||||
|
||||
// // Restricted routes example
|
||||
// restricted := s.api.Group("/restricted")
|
||||
// restricted.Use(middleware.AuthMiddleware())
|
||||
|
||||
@@ -18,3 +18,11 @@ type ProductDescription struct {
|
||||
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
|
||||
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
|
||||
}
|
||||
|
||||
type MeiliSearchProduct struct {
|
||||
ProductID uint
|
||||
Name string
|
||||
Description string
|
||||
DescriptionShort string
|
||||
Usage string
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package categoriesRepo
|
||||
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"
|
||||
)
|
||||
|
||||
type UICategoriesRepo interface {
|
||||
GetAllCategories(id_shop uint, id_lang uint) ([]model.ScannedCategory, error)
|
||||
GetAllCategories(id_lang uint) ([]model.ScannedCategory, error)
|
||||
}
|
||||
|
||||
type CategoriesRepo struct{}
|
||||
@@ -15,7 +16,7 @@ func New() UICategoriesRepo {
|
||||
return &CategoriesRepo{}
|
||||
}
|
||||
|
||||
func (repo *CategoriesRepo) GetAllCategories(id_shop uint, id_lang uint) ([]model.ScannedCategory, error) {
|
||||
func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCategory, error) {
|
||||
var allCategories []model.ScannedCategory
|
||||
|
||||
err := db.DB.Raw(`
|
||||
@@ -34,7 +35,7 @@ func (repo *CategoriesRepo) GetAllCategories(id_shop uint, id_lang uint) ([]mode
|
||||
LEFT JOIN ps_category_shop
|
||||
ON ps_category_shop.id_category = ps_category.id_category
|
||||
AND ps_category_shop.id_shop = ?`,
|
||||
id_shop, id_lang, id_shop).
|
||||
constdata.SHOP_ID, id_lang, constdata.SHOP_ID).
|
||||
Scan(&allCategories).Error
|
||||
|
||||
return allCategories, err
|
||||
@@ -3,12 +3,13 @@ package listProductsRepo
|
||||
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 UIListProductsRepo interface {
|
||||
GetListing(id_shop uint, id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
|
||||
GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
|
||||
}
|
||||
|
||||
type ListProductsRepo struct{}
|
||||
@@ -17,7 +18,7 @@ func New() UIListProductsRepo {
|
||||
return &ListProductsRepo{}
|
||||
}
|
||||
|
||||
func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
var listing []model.ProductInList
|
||||
var total int64
|
||||
|
||||
@@ -60,7 +61,7 @@ func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Pagi
|
||||
) any_image
|
||||
ON ps_product.id_product = any_image.id_product
|
||||
LIMIT ? OFFSET ?`,
|
||||
id_shop, id_lang, id_shop, p.Limit(), p.Offset()).
|
||||
constdata.SHOP_ID, id_lang, constdata.SHOP_ID, p.Limit(), p.Offset()).
|
||||
Scan(&listing).Error
|
||||
if err != nil {
|
||||
return find.Found[model.ProductInList]{}, err
|
||||
@@ -1,22 +1,22 @@
|
||||
package langsAndCountriesRepo
|
||||
package localeSelectorRepo
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
)
|
||||
|
||||
type UILangsAndCountriesRepo interface {
|
||||
type UILocaleSelectorRepo interface {
|
||||
GetLanguages() ([]model.Language, error)
|
||||
GetCountriesAndCurrencies() ([]model.Country, error)
|
||||
}
|
||||
|
||||
type LangsAndCountriesRepo struct{}
|
||||
type LocaleSelectorRepo struct{}
|
||||
|
||||
func New() UILangsAndCountriesRepo {
|
||||
return &LangsAndCountriesRepo{}
|
||||
func New() UILocaleSelectorRepo {
|
||||
return &LocaleSelectorRepo{}
|
||||
}
|
||||
|
||||
func (repo *LangsAndCountriesRepo) GetLanguages() ([]model.Language, error) {
|
||||
func (repo *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) {
|
||||
var languages []model.Language
|
||||
|
||||
err := db.DB.Table("b2b_language").Scan(&languages).Error
|
||||
@@ -24,7 +24,7 @@ func (repo *LangsAndCountriesRepo) GetLanguages() ([]model.Language, error) {
|
||||
return languages, err
|
||||
}
|
||||
|
||||
func (repo *LangsAndCountriesRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
|
||||
func (repo *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
|
||||
var countries []model.Country
|
||||
|
||||
err := db.DB.Table("b2b_countries").
|
||||
@@ -5,12 +5,13 @@ 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"
|
||||
)
|
||||
|
||||
type UIProductDescriptionRepo interface {
|
||||
GetProductDescription(productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error)
|
||||
CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error
|
||||
UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error
|
||||
GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error)
|
||||
CreateIfDoesNotExist(productID uint, productLangID uint) error
|
||||
UpdateFields(productID uint, productLangID uint, updates map[string]string) error
|
||||
}
|
||||
|
||||
type ProductDescriptionRepo struct{}
|
||||
@@ -20,12 +21,12 @@ func New() UIProductDescriptionRepo {
|
||||
}
|
||||
|
||||
// We assume that any user has access to all product descriptions
|
||||
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) {
|
||||
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error) {
|
||||
var ProductDescription model.ProductDescription
|
||||
|
||||
err := db.DB.
|
||||
Table("ps_product_lang").
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||
First(&ProductDescription).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database error: %w", err)
|
||||
@@ -35,16 +36,16 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productSh
|
||||
}
|
||||
|
||||
// If it doesn't exist, returns an error.
|
||||
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error {
|
||||
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLangID uint) error {
|
||||
record := model.ProductDescription{
|
||||
ProductID: productID,
|
||||
ShopID: productShopID,
|
||||
ShopID: constdata.SHOP_ID,
|
||||
LangID: productLangID,
|
||||
}
|
||||
|
||||
err := db.DB.
|
||||
Table("ps_product_lang").
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||
FirstOrCreate(&record).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("database error: %w", err)
|
||||
@@ -53,7 +54,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productSho
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
|
||||
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint, updates map[string]string) error {
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -64,7 +65,7 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID uint
|
||||
|
||||
err := db.DB.
|
||||
Table("ps_product_lang").
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||
Updates(updatesIface).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("database error: %w", err)
|
||||
@@ -1,26 +0,0 @@
|
||||
package langsAndCountriesService
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/repository/langsAndCountriesRepo"
|
||||
)
|
||||
|
||||
// LangsAndCountriesService literally sends back language and countries information.
|
||||
type LangsAndCountriesService struct {
|
||||
repo langsAndCountriesRepo.UILangsAndCountriesRepo
|
||||
}
|
||||
|
||||
// NewLangsAndCountriesService creates a new LangsAndCountries service
|
||||
func New() *LangsAndCountriesService {
|
||||
return &LangsAndCountriesService{
|
||||
repo: langsAndCountriesRepo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LangsAndCountriesService) GetLanguages() ([]model.Language, error) {
|
||||
return s.repo.GetLanguages()
|
||||
}
|
||||
|
||||
func (s *LangsAndCountriesService) GetCountriesAndCurrencies() ([]model.Country, error) {
|
||||
return s.repo.GetCountriesAndCurrencies()
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package langsService
|
||||
|
||||
import (
|
||||
langs_repo "git.ma-al.com/goc_daniel/b2b/app/langs"
|
||||
langs_repo "git.ma-al.com/goc_daniel/b2b/app/repos/langsRepo"
|
||||
"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"
|
||||
|
||||
@@ -2,9 +2,9 @@ package listProductsService
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/listProductsRepo"
|
||||
"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/repository/listProductsRepo"
|
||||
)
|
||||
|
||||
type ListProductsService struct {
|
||||
@@ -17,7 +17,7 @@ func New() *ListProductsService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ListProductsService) GetListing(id_shop uint, id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
func (s *ListProductsService) GetListing(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
var products find.Found[model.ProductInList]
|
||||
|
||||
// currencyIso := c.Cookies("currency_iso", "")
|
||||
@@ -30,7 +30,7 @@ func (s *ListProductsService) GetListing(id_shop uint, id_lang uint, p find.Pagi
|
||||
// countryIso = overrides["override_country"]
|
||||
// }
|
||||
|
||||
products, err := s.listProductsRepo.GetListing(id_shop, id_lang, p, filters)
|
||||
products, err := s.listProductsRepo.GetListing(id_lang, p, filters)
|
||||
if err != nil {
|
||||
return products, err
|
||||
}
|
||||
|
||||
26
app/service/localeSelectorService/localeSelectorService.go
Normal file
26
app/service/localeSelectorService/localeSelectorService.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package localeSelectorService
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/localeSelectorRepo"
|
||||
)
|
||||
|
||||
// LocaleSelectorService literally sends back language and countries information.
|
||||
type LocaleSelectorService struct {
|
||||
repo localeSelectorRepo.UILocaleSelectorRepo
|
||||
}
|
||||
|
||||
// NewLocaleSelectorService creates a new LocaleSelector service
|
||||
func New() *LocaleSelectorService {
|
||||
return &LocaleSelectorService{
|
||||
repo: localeSelectorRepo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocaleSelectorService) GetLanguages() ([]model.Language, error) {
|
||||
return s.repo.GetLanguages()
|
||||
}
|
||||
|
||||
func (s *LocaleSelectorService) GetCountriesAndCurrencies() ([]model.Country, error) {
|
||||
return s.repo.GetCountriesAndCurrencies()
|
||||
}
|
||||
155
app/service/meiliService/meiliService.go
Normal file
155
app/service/meiliService/meiliService.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package meiliService
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
)
|
||||
|
||||
type MeiliService struct {
|
||||
meiliClient meilisearch.ServiceManager
|
||||
}
|
||||
|
||||
func New() *MeiliService {
|
||||
meiliURL := os.Getenv("MEILISEARCH_URL")
|
||||
meiliAPIKey := os.Getenv("MEILISEARCH_API_KEY")
|
||||
|
||||
client := meilisearch.New(
|
||||
meiliURL,
|
||||
meilisearch.WithAPIKey(meiliAPIKey),
|
||||
)
|
||||
|
||||
return &MeiliService{
|
||||
meiliClient: client,
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================== FOR SUPERADMIN ONLY ====================================
|
||||
func (s *MeiliService) CreateIndex(id_lang uint) error {
|
||||
var products []model.ProductDescription
|
||||
|
||||
err := db.DB.
|
||||
Table("ps_product_lang").
|
||||
Where("id_shop = ? AND id_lang = ?", constdata.SHOP_ID, id_lang).
|
||||
Scan(&products).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("database error: %w", err)
|
||||
}
|
||||
|
||||
var meiliProducts []model.MeiliSearchProduct
|
||||
for i := 0; i < len(products); i++ {
|
||||
var nextMeiliProduct model.MeiliSearchProduct
|
||||
|
||||
nextMeiliProduct.ProductID = products[i].ProductID
|
||||
nextMeiliProduct.Name = products[i].Name
|
||||
nextMeiliProduct.Description = cleanHTML(products[i].Description)
|
||||
nextMeiliProduct.DescriptionShort = cleanHTML(products[i].DescriptionShort)
|
||||
nextMeiliProduct.Usage = cleanHTML(products[i].Usage)
|
||||
|
||||
meiliProducts = append(meiliProducts, nextMeiliProduct)
|
||||
}
|
||||
|
||||
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||
primaryKey := "product_id"
|
||||
docOptions := &meilisearch.DocumentOptions{
|
||||
PrimaryKey: &primaryKey,
|
||||
SkipCreation: false,
|
||||
}
|
||||
|
||||
task, err := s.meiliClient.Index(indexName).AddDocuments(meiliProducts, docOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("meili AddDocuments error: %w", err)
|
||||
}
|
||||
|
||||
finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||
fmt.Printf("Task status: %s\n", finishedTask.Status)
|
||||
fmt.Printf("Task error: %s\n", finishedTask.Error)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ==================================== FOR DEBUG ONLY ====================================
|
||||
func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) {
|
||||
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||
|
||||
searchReq := &meilisearch.SearchRequest{
|
||||
Limit: 3,
|
||||
}
|
||||
|
||||
// Perform search
|
||||
results, err := s.meiliClient.Index(indexName).Search("walek", searchReq)
|
||||
if err != nil {
|
||||
fmt.Printf("Meilisearch error: %v\n", err)
|
||||
return meilisearch.SearchResponse{}, err
|
||||
}
|
||||
|
||||
fmt.Printf("Search results for query 'walek' in %s: %d hits\n", indexName, len(results.Hits))
|
||||
|
||||
return *results, nil
|
||||
}
|
||||
|
||||
// Search performs a full-text search on the specified index
|
||||
func (s *MeiliService) Search(indexName string, query string, limit int) (meilisearch.SearchResponse, error) {
|
||||
searchReq := &meilisearch.SearchRequest{
|
||||
Limit: int64(limit),
|
||||
}
|
||||
|
||||
results, err := s.meiliClient.Index(indexName).Search(query, searchReq)
|
||||
if err != nil {
|
||||
fmt.Printf("Meilisearch search error: %v\n", err)
|
||||
return meilisearch.SearchResponse{}, err
|
||||
}
|
||||
|
||||
return *results, nil
|
||||
}
|
||||
|
||||
// HealthCheck checks if Meilisearch is healthy and accessible
|
||||
func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) {
|
||||
health, err := s.meiliClient.Health()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("meilisearch health check failed: %w", err)
|
||||
}
|
||||
|
||||
return health, nil
|
||||
}
|
||||
|
||||
// remove all tags from HTML text
|
||||
func cleanHTML(s string) string {
|
||||
r := strings.NewReader(s)
|
||||
d := xml.NewDecoder(r)
|
||||
|
||||
text := ""
|
||||
|
||||
// Configure the decoder for HTML; leave off strict and autoclose for XHTML
|
||||
d.Strict = true
|
||||
d.AutoClose = xml.HTMLAutoClose
|
||||
d.Entity = xml.HTMLEntity
|
||||
for {
|
||||
token, err := d.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
switch v := token.(type) {
|
||||
case xml.StartElement:
|
||||
text += "\n"
|
||||
case xml.EndElement:
|
||||
case xml.CharData:
|
||||
text += string(v)
|
||||
case xml.Comment:
|
||||
case xml.ProcInst:
|
||||
case xml.Directive:
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"sort"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||
"git.ma-al.com/goc_daniel/b2b/repository/categoriesRepo"
|
||||
)
|
||||
|
||||
type MenuService struct {
|
||||
@@ -18,8 +18,8 @@ func New() *MenuService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MenuService) GetMenu(id_shop uint, id_lang uint) (model.Category, error) {
|
||||
all_categories, err := s.categoriesRepo.GetAllCategories(id_shop, id_lang)
|
||||
func (s *MenuService) GetMenu(id_lang uint) (model.Category, error) {
|
||||
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang)
|
||||
if err != nil {
|
||||
return model.Category{}, err
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
"cloud.google.com/go/translate/apiv3/translatepb"
|
||||
"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/repos/productDescriptionRepo"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/langsService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||
"git.ma-al.com/goc_daniel/b2b/repository/productDescriptionRepo"
|
||||
"github.com/openai/openai-go/v3"
|
||||
"github.com/openai/openai-go/v3/option"
|
||||
"github.com/openai/openai-go/v3/responses"
|
||||
@@ -82,12 +82,12 @@ func New() *ProductDescriptionService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) {
|
||||
return s.productDescriptionRepo.GetProductDescription(productID, productShopID, productLangID)
|
||||
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productLangID uint) (*model.ProductDescription, error) {
|
||||
return s.productDescriptionRepo.GetProductDescription(productID, productLangID)
|
||||
}
|
||||
|
||||
// Updates relevant fields with the "updates" map
|
||||
func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
|
||||
func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productLangID uint, updates map[string]string) error {
|
||||
// only some fields can be affected
|
||||
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
|
||||
for key := range updates {
|
||||
@@ -106,12 +106,12 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI
|
||||
}
|
||||
}
|
||||
|
||||
err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productShopID, productLangID)
|
||||
err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productLangID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.productDescriptionRepo.UpdateFields(productID, productShopID, productLangID, updates)
|
||||
return s.productDescriptionRepo.UpdateFields(productID, productLangID, updates)
|
||||
}
|
||||
|
||||
// TranslateProductDescription fetches the product description for productFromLangID,
|
||||
@@ -120,9 +120,9 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI
|
||||
//
|
||||
// The Google Cloud project must have the Cloud Translation API enabled and the
|
||||
// service account must hold the "Cloud Translation API User" role.
|
||||
func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productShopID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) {
|
||||
func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) {
|
||||
|
||||
productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productShopID, productFromLangID)
|
||||
productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productFromLangID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ package constdata
|
||||
|
||||
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
|
||||
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
|
||||
const SHOP_ID = 1
|
||||
|
||||
2
bo/components.d.ts
vendored
2
bo/components.d.ts
vendored
@@ -11,6 +11,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Cart1: typeof import('./src/components/customer/Cart1.vue')['default']
|
||||
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
|
||||
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
|
||||
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
|
||||
@@ -19,6 +20,7 @@ declare module 'vue' {
|
||||
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
|
||||
PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
|
||||
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
|
||||
PageProductsList: typeof import('./src/components/customer/PageProductsList.vue')['default']
|
||||
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
||||
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
||||
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
||||
|
||||
@@ -31,6 +31,12 @@ const authStore = useAuthStore()
|
||||
<RouterLink :to="{ name: 'cart' }">
|
||||
Cart
|
||||
</RouterLink>
|
||||
<RouterLink :to="{ name: 'cart1' }">
|
||||
Cart1
|
||||
</RouterLink>
|
||||
<RouterLink :to="{ name: 'products-list' }">
|
||||
Products List
|
||||
</RouterLink>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Language Switcher -->
|
||||
<LangSwitch />
|
||||
|
||||
@@ -14,9 +14,9 @@ const authStore = useAuthStore()
|
||||
<!-- 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" />
|
||||
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-5 h-5" />
|
||||
</div>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">B2B</span>
|
||||
</RouterLink>
|
||||
<!-- Right Side Actions -->
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<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" />
|
||||
|
||||
66
bo/src/components/customer/Cart1.vue
Normal file
66
bo/src/components/customer/Cart1.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="container mx-auto mt-15">
|
||||
<div
|
||||
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
||||
<h2
|
||||
class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ t('Cart Items') }}
|
||||
</h2>
|
||||
|
||||
<div v-if="cartStore.items.length > 0" class="divide-y divide-(--border-light) dark:divide-(--border-dark)">
|
||||
<div v-for="item in cartStore.items" :key="item.id" class="flex items-center justify-between p-4 gap-4">
|
||||
<div class="flex items-center gap-10 flex-1">
|
||||
<div
|
||||
class="w-20 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
|
||||
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
|
||||
<UIcon v-else name="mdi:package-variant" class="text-xl text-gray-400" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-black dark:text-white text-sm font-medium truncate">{{ item.name }}</p>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm">
|
||||
{{ t('Qty') }}: {{ item.quantity }} × ${{ item.price.toFixed(2) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right flex-shrink-0">
|
||||
<p class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="p-8 text-center">
|
||||
<UIcon name="mdi:cart-outline" class="text-5xl text-gray-300 dark:text-gray-600 mb-4 mx-auto" />
|
||||
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
|
||||
</div>
|
||||
<div v-if="cartStore.items.length > 0"
|
||||
class="p-4 border-t border-(--border-light) dark:border-(--border-dark) bg-(--second-light) dark:bg-(--main-dark) flex gap-4 justify-end items-center">
|
||||
<UButton color="primary" @click="handleContinueToCheckout"
|
||||
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
|
||||
{{ t('Continue to Checkout') }}
|
||||
</UButton>
|
||||
<UButton variant="outline" color="neutral" @click="handleCancel"
|
||||
class="text-black dark:text-white border-(--border-light) dark:border-(--border-dark) hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
{{ t('Cancel') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const cartStore = useCartStore()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
function handleCancel() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
function handleContinueToCheckout() {
|
||||
router.push({ name: 'cart' })
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="container mx-auto mt-10">
|
||||
<div class="flex flex-col mb-6">
|
||||
<div class="container mx-auto mt-15">
|
||||
<div class="flex flex-col gap-5 mb-6">
|
||||
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0">
|
||||
<div class="flex gap-2 items-center">
|
||||
<UInput v-model="searchQuery" type="text" :placeholder="t('Search address')"
|
||||
class="bg-white dark:bg-gray-800 text-black dark:text-white absolute" />
|
||||
@@ -16,7 +16,6 @@
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="paginatedAddresses.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div v-for="address in 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">
|
||||
@@ -35,11 +34,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</div>
|
||||
|
||||
<div class="mt-6 flex justify-center">
|
||||
<UPagination v-model:page="page" :total="totalItems" :page-size="pageSize" />
|
||||
</div>
|
||||
|
||||
<UModal v-model:open="showModal" :overlay="true" class="max-w-md mx-auto">
|
||||
<template #content>
|
||||
<div class="p-6 flex flex-col gap-6">
|
||||
@@ -74,7 +71,6 @@
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<UModal v-model:open="showDeleteConfirm" :overlay="true" class="max-w-md mx-auto">
|
||||
<template #content>
|
||||
<div class="p-6 flex flex-col gap-3">
|
||||
@@ -106,7 +102,6 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
const addressStore = useAddressStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const showModal = ref(false)
|
||||
const isEditing = ref(false)
|
||||
@@ -122,17 +117,14 @@ const totalItems = computed(() => addressStore.totalItems)
|
||||
const pageSize = addressStore.pageSize
|
||||
|
||||
watch(page, (newPage) => addressStore.setPage(newPage))
|
||||
|
||||
watch(searchQuery, (val) => {
|
||||
addressStore.setSearchQuery(val)
|
||||
})
|
||||
|
||||
function openCreateModal() {
|
||||
resetForm()
|
||||
isEditing.value = false
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function openEditModal(address: any) {
|
||||
formData.value = {
|
||||
street: address.street,
|
||||
@@ -144,17 +136,14 @@ function openEditModal(address: any) {
|
||||
editingAddressId.value = address.id
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.value = { street: '', zipCode: '', city: '', country: '' }
|
||||
editingAddressId.value = null
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
function validate() {
|
||||
const errors = []
|
||||
if (!formData.value.street) errors.push({ name: 'street', message: 'Street required' })
|
||||
@@ -163,7 +152,6 @@ function validate() {
|
||||
if (!formData.value.country) errors.push({ name: 'country', message: 'Country required' })
|
||||
return errors.length ? errors : null
|
||||
}
|
||||
|
||||
function saveAddress() {
|
||||
if (validate()) return
|
||||
if (isEditing.value && editingAddressId.value) {
|
||||
@@ -173,12 +161,18 @@ function saveAddress() {
|
||||
}
|
||||
closeModal()
|
||||
}
|
||||
|
||||
// const Lera = ref('')
|
||||
// function run (){
|
||||
// if(Lera.value==='lera'){
|
||||
// console.log('Leraa okokok')
|
||||
// }else{
|
||||
// console.log('LEra nonono')
|
||||
// }
|
||||
// }
|
||||
function confirmDelete(id: number) {
|
||||
addressToDelete.value = id
|
||||
showDeleteConfirm.value = true
|
||||
}
|
||||
|
||||
function deleteAddress() {
|
||||
if (addressToDelete.value) {
|
||||
addressStore.deleteAddress(addressToDelete.value)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<template>
|
||||
<div class="container mx-auto mt-20 px-4 py-8">
|
||||
<div class="container mx-auto mt-15">
|
||||
<h1 class="text-2xl font-bold text-black dark:text-white mb-8">{{ t('Shopping Cart') }}</h1>
|
||||
<div class="flex flex-col lg:flex-row gap-8 mb-8">
|
||||
<div class="flex-1">
|
||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
||||
<h2 class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
|
||||
<div
|
||||
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
||||
<h2
|
||||
class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ t('Selected Products') }}
|
||||
</h2>
|
||||
<div class="hidden md:grid grid-cols-12 gap-4 p-4 bg-(--second-light) dark:bg-(--main-dark) text-sm font-medium text-gray-600 dark:text-gray-300 border-b border-(--border-light) dark:border-(--border-dark)">
|
||||
<div
|
||||
class="hidden md:grid grid-cols-12 gap-4 p-4 bg-(--second-light) dark:bg-(--main-dark) text-sm font-medium text-gray-600 dark:text-gray-300 border-b border-(--border-light) dark:border-(--border-dark)">
|
||||
<div class="col-span-4">{{ t('Product') }}</div>
|
||||
<div class="col-span-2 text-right">{{ t('Price') }}</div>
|
||||
<div class="col-span-3 text-center">{{ t('Quantity') }}</div>
|
||||
@@ -15,10 +18,11 @@
|
||||
<div class="col-span-1 text-center">{{ t('Actions') }}</div>
|
||||
</div>
|
||||
<div v-if="cartStore.items.length > 0">
|
||||
<div v-for="item in cartStore.items" :key="item.id"
|
||||
<div v-for="item in cartStore.items" :key="item.id"
|
||||
class="grid grid-cols-1 md:grid-cols-12 gap-4 p-4 border-b border-(--border-light) dark:border-(--border-dark) items-center">
|
||||
<div class="col-span-4 flex items-center gap-4">
|
||||
<div class="w-16 h-16 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
class="w-16 h-16 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
|
||||
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
|
||||
<UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
|
||||
</div>
|
||||
@@ -29,22 +33,18 @@
|
||||
<span class="text-black dark:text-white">${{ item.price.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="col-span-3 flex items-center justify-center">
|
||||
<div class="flex items-center border border-(--border-light) dark:border-(--border-dark) rounded">
|
||||
<button @click="decreaseQuantity(item)" class="px-3 py-1 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<UIcon name="mdi:minus" />
|
||||
</button>
|
||||
<span class="px-3 py-1 text-black dark:text-white min-w-[40px] text-center">{{ item.quantity }}</span>
|
||||
<button @click="increaseQuantity(item)" class="px-3 py-1 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<UIcon name="mdi:plus" />
|
||||
</button>
|
||||
</div>
|
||||
<UInputNumber v-model="item.quantity" :min="1"
|
||||
@update:model-value="(val: number) => cartStore.updateQuantity(item.id, val)" />
|
||||
</div>
|
||||
<div class="col-span-2 text-right">
|
||||
<span class="md:hidden text-gray-500 dark:text-gray-400 text-sm">{{ t('Total') }}: </span>
|
||||
<span class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2) }}</span>
|
||||
<span class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="col-span-1 flex justify-center">
|
||||
<button @click="removeItem(item.id)" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors" :title="t('Remove')">
|
||||
<button @click="removeItem(item.id)"
|
||||
class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
|
||||
:title="t('Remove')">
|
||||
<UIcon name="material-symbols:delete" class="text-[20px]" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -53,14 +53,16 @@
|
||||
<div v-else class="p-8 text-center">
|
||||
<UIcon name="mdi:cart-outline" class="text-6xl text-gray-300 dark:text-gray-600 mb-4" />
|
||||
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
|
||||
<RouterLink :to="{ name: 'product-card-full' }" class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||
<RouterLink :to="{ name: 'product-card-full' }"
|
||||
class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||
{{ t('Continue Shopping') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:w-80">
|
||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
|
||||
<div
|
||||
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
|
||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Order Summary') }}</h2>
|
||||
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
|
||||
<div class="flex justify-between">
|
||||
@@ -74,13 +76,15 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0) }}%)</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0)
|
||||
}}%)</span>
|
||||
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-6">
|
||||
<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">${{ cartStore.orderTotal.toFixed(2) }}</span>
|
||||
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{
|
||||
cartStore.orderTotal.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
|
||||
@@ -97,19 +101,19 @@
|
||||
</div>
|
||||
<div class="flex flex-col lg:flex-row gap-8">
|
||||
<div class="flex-1">
|
||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
||||
<div
|
||||
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
|
||||
<div class="mb-4">
|
||||
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
|
||||
class="w-full bg-white dark:bg-gray-800 text-black dark:text-white" />
|
||||
</div>
|
||||
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
|
||||
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
|
||||
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
||||
:class="cartStore.selectedAddressId === address.id
|
||||
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
|
||||
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedAddressId === address.id
|
||||
? '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'">
|
||||
<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)" />
|
||||
<div class="flex-1">
|
||||
<p class="text-black dark:text-white font-medium">{{ address.street }}</p>
|
||||
@@ -121,22 +125,23 @@
|
||||
<div v-else class="text-center py-6">
|
||||
<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>
|
||||
<RouterLink :to="{ name: 'addresses' }" class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||
<RouterLink :to="{ name: 'addresses' }"
|
||||
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||
{{ t('Add Address') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
||||
<div
|
||||
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delivery Method') }}</h2>
|
||||
<div class="space-y-3">
|
||||
<label v-for="method in cartStore.deliveryMethods" :key="method.id"
|
||||
class="flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
||||
:class="cartStore.selectedDeliveryMethodId === method.id
|
||||
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||
<label v-for="method in cartStore.deliveryMethods" :key="method.id"
|
||||
class="flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedDeliveryMethodId === method.id
|
||||
? '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'">
|
||||
<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)" />
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -157,7 +162,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useCartStore, type CartItem } from '@/stores/cart'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useAddressStore } from '@/stores/address'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -186,19 +191,10 @@ watch(selectedDeliveryMethod, (newValue) => {
|
||||
})
|
||||
|
||||
const canPlaceOrder = computed(() => {
|
||||
return cartStore.items.length > 0 &&
|
||||
cartStore.selectedAddressId !== null &&
|
||||
cartStore.selectedDeliveryMethodId !== null
|
||||
return cartStore.items.length > 0 &&
|
||||
cartStore.selectedAddressId !== null &&
|
||||
cartStore.selectedDeliveryMethodId !== null
|
||||
})
|
||||
|
||||
function increaseQuantity(item: CartItem) {
|
||||
cartStore.updateQuantity(item.id, item.quantity + 1)
|
||||
}
|
||||
|
||||
function decreaseQuantity(item: CartItem) {
|
||||
cartStore.updateQuantity(item.id, item.quantity - 1)
|
||||
}
|
||||
|
||||
function removeItem(itemId: number) {
|
||||
cartStore.removeItem(itemId)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="container mt-14 mx-auto">
|
||||
<div class="flex justify-between gap-8 mb-6">
|
||||
<div class="container mt-15 mx-auto">
|
||||
<div class="flex md:flex-row flex-col justify-between gap-8 mb-6">
|
||||
<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]">
|
||||
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-end mb-8">
|
||||
<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">
|
||||
<span class="text-sm text-(--accent-blue-light) dark:text-(--accent-blue-dark) ">Colors:</span>
|
||||
<div class="flex gap-2">
|
||||
@@ -49,10 +49,10 @@
|
||||
</div>
|
||||
<ProductCustomization />
|
||||
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||
<div class="mb-6 w-[55%]">
|
||||
<div class="flex justify-between items-center gap-10 mb-8">
|
||||
<div class="mb-6 w-[100%] xl:w-[60%]">
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-10 mb-8">
|
||||
<UButton v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[
|
||||
'px-15 py-2 cursor-pointer',
|
||||
'px-15 py-2 cursor-pointer sm:text-nowrap flex items-center! justify-center!',
|
||||
activeTab === tab.id
|
||||
? 'bg-blue-600 hover:text-black hover:dark:text-white text-white'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
|
||||
90
bo/src/components/customer/PageProductsList.vue
Normal file
90
bo/src/components/customer/PageProductsList.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="container mx-auto mt-15">
|
||||
<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">
|
||||
<span class="text-gray-600 dark:text-gray-400">Loading products...</span>
|
||||
</div>
|
||||
<div v-else-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Image</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Product ID</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Name</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="product in productsList" :key="product.product_id"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<img :src="getImageUrl(product.ImageID, product.LinkRewrite,)" alt="product image"
|
||||
class="w-16 h-16 object-cover rounded" />
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{
|
||||
product.product_id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ product.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600 dark:text-blue-400">
|
||||
{{ product.LinkRewrite }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="productsList.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
No products found
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
|
||||
interface Product {
|
||||
product_id: number
|
||||
name: string
|
||||
ImageID: number
|
||||
LinkRewrite: string
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
message: string
|
||||
items: Product[]
|
||||
count: number
|
||||
}
|
||||
|
||||
const productsList = ref<Product[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
function getImageUrl(imageID: number, linkRewrite: string, size: string = 'small_default') {
|
||||
return `https://www.naluconcept.com/${imageID}-${size}/${linkRewrite}.webp`
|
||||
}
|
||||
async function fetchProductList() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await useFetchJson('/api/v1/restricted/list-products/get-listing?p&elems&shopID=1') as ApiResponse
|
||||
productsList.value = response.items || []
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(fetchProductList)
|
||||
</script>
|
||||
@@ -4,7 +4,7 @@
|
||||
<p class="text-[24px] font-bold">Product customization</p>
|
||||
<p class="text-[15px]">Don't forget to save your customization to be able to add to cart</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-10">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 gap-5 md:gap-10 ">
|
||||
<UInput label="Podaj kolor kanapy narożnej" placeholder="Podaj kolor kanapy narożnej" class="dark:text-white text-black"/>
|
||||
<UInput label="Podaj kolor fotela" placeholder="Podaj kolor fotela" class="dark:text-white text-black"/>
|
||||
<UInput label="Podaj kolor kwadratu" placeholder="Podaj kolor kwadratu" class="dark:text-white text-black"/>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import TopBar from '@/components/TopBar.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
||||
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
|
||||
|
||||
@@ -9,11 +9,7 @@ function isAuthenticated(): boolean {
|
||||
if (typeof document === 'undefined') return false
|
||||
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
|
||||
}
|
||||
|
||||
|
||||
await getSettings()
|
||||
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.VITE_BASE_URL),
|
||||
routes: [
|
||||
@@ -34,6 +30,8 @@ const router = createRouter({
|
||||
{ path: 'product-card-full/', component: () => import('../components/customer/PageProductCardFull.vue'), name: 'product-card-full' },
|
||||
{ path: 'addresses', component: () => import('../components/customer/PageAddresses.vue'), name: 'addresses' },
|
||||
{ path: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' },
|
||||
{ path: 'cart1', component: () => import('../components/customer/Cart1.vue'), name: 'cart1' },
|
||||
{ path: 'products-list', component: () => import('../components/customer/PageProductsList.vue'), name: 'products-list' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -55,7 +53,6 @@ const router = createRouter({
|
||||
router.beforeEach((to, from, next) => {
|
||||
const locale = to.params.locale as string
|
||||
const localeLang = langs.find((x) => x.iso_code == locale)
|
||||
|
||||
if (locale && langs.length > 0) {
|
||||
const authStore = useAuthStore()
|
||||
console.log(authStore.isAuthenticated, to, from)
|
||||
@@ -77,7 +74,6 @@ router.beforeEach((to, from, next) => {
|
||||
if (!locale && to.path !== '/') {
|
||||
return next(`/${currentLang.value?.iso_code}${to.path}`)
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
@@ -95,7 +95,6 @@ export const useAddressStore = defineStore('address', () => {
|
||||
id: existing.id,
|
||||
...normalize(formData)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
function deleteAddress(id: number): boolean {
|
||||
|
||||
@@ -65,6 +65,7 @@ export const useCartStore = defineStore('cart', () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function removeItem(itemId: number) {
|
||||
const index = items.value.findIndex(i => i.id === itemId)
|
||||
if (index !== -1) {
|
||||
|
||||
@@ -42,7 +42,7 @@ export const useProductStore = defineStore('product', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveProductDescription() {
|
||||
async function saveProductDescription() {
|
||||
try {
|
||||
const data = await useFetchJson(
|
||||
`/api/v1/restricted/product-description/save-product-description?productID=1&productShopID=1&productLangID=1`,
|
||||
@@ -92,6 +92,6 @@ export const useProductStore = defineStore('product', () => {
|
||||
getProductDescription,
|
||||
clearCurrentProduct,
|
||||
saveProductDescription,
|
||||
translateProductDescription
|
||||
translateProductDescription,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -63,7 +63,6 @@ const PrivacyComponent = computed(() =>
|
||||
<UButton @click="showTherms = false" class="mx-auto px-12">{{ $t('general.close') }}</UButton>
|
||||
</template>
|
||||
</UDrawer>
|
||||
<!-- PrivacyPolicyView -->
|
||||
<UDrawer v-model:open="showPrivacy" :overlay="false">
|
||||
<template #body>
|
||||
<component :is="PrivacyComponent" />
|
||||
@@ -76,9 +75,9 @@ const PrivacyComponent = computed(() =>
|
||||
<div class="text-center mb-15">
|
||||
<div
|
||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
||||
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||
</div>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<UForm :validate="validate" @submit="handleLogin" class="space-y-5">
|
||||
@@ -94,11 +93,12 @@ const PrivacyComponent = computed(() =>
|
||||
|
||||
<UFormField :label="$t('general.password')" name="password" required class="w-full dark:text-white text-black">
|
||||
<UInput v-model="password" :placeholder="$t('general.enter_your_password')"
|
||||
:type="showPassword ? 'text' : 'password'" class="w-full placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
|
||||
:type="showPassword ? 'text' : 'password'" class="w-full placeholder:text-(--placeholder)"
|
||||
:ui="{ trailing: 'pe-1' }">
|
||||
<template #trailing>
|
||||
<UIcon color="neutral" variant="link" size="sm" :name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
||||
:aria-label="showPassword ? 'Hide password' : 'Show password'" :aria-pressed="showPassword"
|
||||
aria-controls="password" @click="showPassword = !showPassword" class="mr-2"/>
|
||||
aria-controls="password" @click="showPassword = !showPassword" class="mr-2" />
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
@@ -114,15 +114,11 @@ const PrivacyComponent = computed(() =>
|
||||
{{ $t('general.sign_in') }}
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="flex items-center gap-3 my-1">
|
||||
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $t('general.or') }}</span>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
||||
</div>
|
||||
|
||||
<!-- Google Sign In -->
|
||||
<UButton type="button" color="neutral" variant="outline" size="lg" block :disabled="authStore.loading"
|
||||
@click="authStore.loginWithGoogle()"
|
||||
class="flex items-center justify-center gap-2 dark:text-white text-black cursor-pointer">
|
||||
|
||||
@@ -40,9 +40,9 @@ function validate(): FormError[] {
|
||||
<div class="text-center mb-15">
|
||||
<div
|
||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
||||
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||
</div>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
<div class="text-center mb-15">
|
||||
<div
|
||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
||||
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||
</div>
|
||||
<div class="w-full max-w-md">
|
||||
<UForm :validate="validate" @submit="handleRegister" class="flex flex-col gap-3">
|
||||
|
||||
@@ -181,7 +181,7 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="container mx-auto">
|
||||
<div class="p-6 bg-(--main-light) dark:bg-(--black) font-sans">
|
||||
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }}
|
||||
</h1>
|
||||
|
||||
@@ -53,9 +53,9 @@ function validate(): FormError[] {
|
||||
<div class="text-center mb-15">
|
||||
<div
|
||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
||||
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||
</div>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
|
||||
|
||||
@@ -73,9 +73,9 @@ function goToLogin() {
|
||||
<div class="text-center mb-8">
|
||||
<div
|
||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
||||
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||
</div>
|
||||
|
||||
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
|
||||
|
||||
1
go.mod
1
go.mod
@@ -29,6 +29,7 @@ require (
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/meilisearch/meilisearch-go v0.36.1 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -109,6 +109,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/meilisearch/meilisearch-go v0.36.1 h1:mJTCJE5g7tRvaqKco6DfqOuJEjX+rRltDEnkEC02Y0M=
|
||||
github.com/meilisearch/meilisearch-go v0.36.1/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
|
||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM=
|
||||
|
||||
Reference in New Issue
Block a user