6 Commits

39 changed files with 549 additions and 3622 deletions

4
.env
View File

@@ -25,9 +25,6 @@ AUTH_REFRESH_EXPIRATION=604800
MEILISEARCH_URL=http://localhost:7700
MEILISEARCH_API_KEY=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
# OpenAI
OPENAI_KEY=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
# Google Translate Client
GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json
GOOGLE_CLOUD_PROJECT_ID=translation-343517
@@ -36,7 +33,6 @@ GOOGLE_CLOUD_PROJECT_ID=translation-343517
OAUTH_GOOGLE_CLIENT_ID=331979954218-9vrpe08oqhhcgj6bvu6d4lds0dt630m9.apps.googleusercontent.com
OAUTH_GOOGLE_CLIENT_SECRET=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
OAUTH_GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/public/auth/google/callback
# Email Configuration (SMTP)
# Set EMAIL_ENABLED=true to require email verification
EMAIL_ENABLED=true

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/service/listProductsService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
@@ -50,13 +52,13 @@ func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
// "override_currency": c.Query("override_currency", ""),
// }
id_lang, ok := c.Locals("langID").(uint)
if !ok {
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(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)))

View File

@@ -1,6 +1,8 @@
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"
@@ -30,13 +32,13 @@ func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
}
func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
id_lang, ok := c.Locals("langID").(uint)
if !ok {
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(id_lang)
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)))
@@ -47,13 +49,13 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
}
func (h *MeiliSearchHandler) Test(c fiber.Ctx) error {
id_lang, ok := c.Locals("langID").(uint)
if !ok {
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(id_lang)
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)))

View File

@@ -1,6 +1,8 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/service/menuService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
@@ -29,13 +31,13 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router {
}
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
id_lang, ok := c.Locals("langID").(uint)
if !ok {
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(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)))

View File

@@ -82,27 +82,18 @@ type ProductFilters struct {
}
type ScannedCategory struct {
CategoryID uint `gorm:"column:ID;primaryKey"`
Name string `gorm:"column:name"`
Active uint `gorm:"column:active"`
Position uint `gorm:"column:position"`
ParentID uint `gorm:"column:id_parent"`
IsRoot uint `gorm:"column:is_root_category"`
LinkRewrite string `gorm:"column:link_rewrite"`
IsoCode string `gorm:"column:iso_code"`
CategoryID uint `gorm:"column:ID;primaryKey"`
Name string `gorm:"column:name"`
Active uint `gorm:"column:active"`
Position uint `gorm:"column:position"`
ParentID uint `gorm:"column:id_parent"`
IsRoot uint `gorm:"column:is_root_category"`
}
type Category struct {
CategoryID uint `json:"category_id" form:"category_id"`
Label string `json:"label" form:"label"`
// Active bool `json:"active" form:"active"`
Params CategpryParams `json:"params" form:"params"`
Children []Category `json:"children" form:"children"`
}
type CategpryParams struct {
CategoryID uint `json:"category_id" form:"category_id"`
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
Locale string `json:"locale" form:"locale"`
CategoryID uint `json:"category_id" form:"category_id"`
Name string `json:"name" form:"name"`
Active uint `json:"active" form:"active"`
Subcategories []Category `json:"subcategories" form:"subcategories"`
}
type FeatVal = map[uint][]uint

View File

@@ -19,26 +19,10 @@ type ProductDescription struct {
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
}
type ProductRow struct {
IDProduct int `gorm:"column:id_product"`
IDShop int `gorm:"column:id_shop"`
Name string `gorm:"column:name"`
Active uint8 `gorm:"column:active"`
Reference string `gorm:"column:reference"`
}
type MeiliSearchProduct struct {
ProductID uint `gorm:"column:id_product"`
Name string `gorm:"column:name"`
Active uint8 `gorm:"column:active"`
Price float64 `gorm:"column:price"`
Description string `gorm:"column:description"`
DescriptionShort string `gorm:"column:description_short"`
Usage string `gorm:"column:usage"`
EAN13 string `gorm:"column:ean13"`
Reference string `gorm:"column:reference"`
Width float64 `gorm:"column:width"`
Height float64 `gorm:"column:height"`
Depth float64 `gorm:"column:depth"`
Weight float64 `gorm:"column:weight"`
ProductID uint
Name string
Description string
DescriptionShort string
Usage string
}

View File

@@ -26,14 +26,15 @@ func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCateg
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
ps_category.is_root_category AS is_root_category
FROM ps_category
LEFT JOIN ps_category_lang ON ps_category_lang.id_category = ps_category.id_category AND ps_category_lang.id_shop = ? AND ps_category_lang.id_lang = ?
LEFT JOIN ps_category_shop ON ps_category_shop.id_category = ps_category.id_category AND ps_category_shop.id_shop = ?
JOIN ps_lang ON ps_lang.id_lang = ps_category_lang.id_lang
`,
LEFT JOIN ps_category_lang
ON ps_category_lang.id_category = ps_category.id_category
AND ps_category_lang.id_shop = ?
AND ps_category_lang.id_lang = ?
LEFT JOIN ps_category_shop
ON ps_category_shop.id_category = ps_category.id_category
AND ps_category_shop.id_shop = ?`,
constdata.SHOP_ID, id_lang, constdata.SHOP_ID).
Scan(&allCategories).Error

View File

@@ -12,7 +12,6 @@ type UIProductDescriptionRepo interface {
GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error)
CreateIfDoesNotExist(productID uint, productLangID uint) error
UpdateFields(productID uint, productLangID uint, updates map[string]string) error
GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error)
}
type ProductDescriptionRepo struct{}
@@ -74,35 +73,3 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint
return nil
}
// We assume that any user has access to all product descriptions
func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) {
var products []model.MeiliSearchProduct
err := db.DB.
Select("pl.`usage` AS `usage`").
Select(`
ps.id_product AS id_product,
pl.name AS name,
ps.active AS active,
ps.price AS price,
pl.description AS description,
pl.description_short AS description_short,
p.ean13 AS ean13,
p.reference AS reference,
p.width AS width,
p.height AS height,
p.depth AS depth,
p.weight AS weight
`).
Table("ps_product_shop AS ps").
Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product").
Where("ps.id_shop = ?", constdata.SHOP_ID).
Scan(&products).Error
if err != nil {
return products, fmt.Errorf("database error: %w", err)
}
return products, nil
}

View File

@@ -1,8 +0,0 @@
{
"products-openai": {
"source": "openAi",
"model": "text-embedding-3-small",
"apiKey": "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",
"documentTemplate": "{{doc.Name}} is equipment used for {{doc.Description | truncatewords: 20}}"
}
}

View File

@@ -9,14 +9,14 @@ import (
"strings"
"time"
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo"
"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 {
productDescriptionRepo productDescriptionRepo.UIProductDescriptionRepo
meiliClient meilisearch.ServiceManager
meiliClient meilisearch.ServiceManager
}
func New() *MeiliService {
@@ -29,52 +29,43 @@ func New() *MeiliService {
)
return &MeiliService{
meiliClient: client,
productDescriptionRepo: productDescriptionRepo.New(),
meiliClient: client,
}
}
// ==================================== FOR SUPERADMIN ONLY ====================================
func (s *MeiliService) CreateIndex(id_lang uint) error {
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang)
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++ {
products[i].Description, err = cleanHTML(products[i].Description)
if err != nil {
fmt.Printf("products[i].Description: %v\n", products[i].Description)
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
fmt.Println("failed at description")
fmt.Printf("err: %v\n", err)
return err
}
var nextMeiliProduct model.MeiliSearchProduct
products[i].DescriptionShort, err = cleanHTML(products[i].DescriptionShort)
if err != nil {
fmt.Printf("products[i].DescriptionShort: %v\n", products[i].DescriptionShort)
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
fmt.Println("failed at description short")
fmt.Printf("err: %v\n", err)
return err
}
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)
products[i].Usage, err = cleanHTML(products[i].Usage)
if err != nil {
fmt.Printf("products[i].Usage: %v\n", products[i].Usage)
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
fmt.Println("failed at usage")
fmt.Printf("err: %v\n", err)
return err
}
meiliProducts = append(meiliProducts, nextMeiliProduct)
}
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
primaryKey := "ProductID"
primaryKey := "product_id"
docOptions := &meilisearch.DocumentOptions{
PrimaryKey: &primaryKey,
SkipCreation: false,
}
task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions)
task, err := s.meiliClient.Index(indexName).AddDocuments(meiliProducts, docOptions)
if err != nil {
return fmt.Errorf("meili AddDocuments error: %w", err)
}
@@ -132,7 +123,7 @@ func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) {
}
// remove all tags from HTML text
func cleanHTML(s string) (string, error) {
func cleanHTML(s string) string {
r := strings.NewReader(s)
d := xml.NewDecoder(r)
@@ -146,25 +137,19 @@ func cleanHTML(s string) (string, error) {
token, err := d.Token()
if err == io.EOF {
break
} else if err != nil {
return text, err
}
switch v := token.(type) {
case xml.StartElement:
if len(text) > 0 && text[len(text)-1] != '\n' {
text += " \n "
}
text += "\n"
case xml.EndElement:
case xml.CharData:
if strings.TrimSpace(string(v)) != "" {
text += string(v)
}
text += string(v)
case xml.Comment:
case xml.ProcInst:
case xml.Directive:
}
}
return text, nil
return text
}

View File

@@ -64,7 +64,7 @@ func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCate
node := s.scannedToNormalCategory((*all_categories)[index])
for i := 0; i < len((*children_indices)[index]); i++ {
node.Children = append(node.Children, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices))
node.Subcategories = append(node.Subcategories, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices))
}
return node
@@ -72,12 +72,10 @@ func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCate
func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category {
var normal model.Category
// normal.Active = scanned.Active
normal.Active = scanned.Active
normal.CategoryID = scanned.CategoryID
normal.Label = scanned.Name
// normal.Active = scanned.Active == 1
normal.Params = model.CategpryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode}
normal.Children = []model.Category{}
normal.Name = scanned.Name
normal.Subcategories = []model.Category{}
return normal
}

View File

@@ -70,7 +70,7 @@ func New() *ProductDescriptionService {
log.Fatalf("productDescriptionService: cannot create Translation client: %v", err)
}
openAIClient := openai.NewClient(option.WithAPIKey(os.Getenv("OPENAI_KEY")),
openAIClient := openai.NewClient(option.WithAPIKey("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"),
option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout
return &ProductDescriptionService{

3
bo/components.d.ts vendored
View File

@@ -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']
@@ -41,7 +43,6 @@ declare module 'vue' {
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default']
UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
UNavigationMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.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']
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']

View File

@@ -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 />

View File

@@ -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">

View File

@@ -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" />

View 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>

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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'

View 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>

View File

@@ -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"/>

View File

@@ -1,56 +1,12 @@
<script setup lang="ts">
import TopBar from '@/components/TopBar.vue';
import { getMenu } from '@/router/menu'
import type { NavigationMenuItem } from '@nuxt/ui';
import { on } from 'events';
import { ref } from 'vue';
let menu = await getMenu() as NavigationMenuItem[]
function adaptMenu(menu) {
for (const item of menu) {
if (item.children && item.children.length > 0) {
adaptMenu(item.children);
// item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label ,to: { name: 'category', params: item.params }})
// item.type = 'trigger'
// item.icon = 'i-lucide-book-open'
// item.onSelect(e){ console.log(e, 'adasdasdas'); }
} else {
// console.log(item.params);
item.to = { name: 'category', params: item.params };
item.icon = 'i-lucide-file-text'
}
}
return menu;
}
menu = adaptMenu(menu.children)
console.log(menu);
const items = ref<NavigationMenuItem[][]>([
[
...menu
],
])
</script>
<template>
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
<main class="p-10">
<div class="mt-24 bg-accented w-1/5 rounded-2xl">
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4">
</UNavigationMenu>
</div>
<!-- <template #item>{{ item }}</template> -->
<TopBar/>
<slot></slot>
</main>
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
<main class="p-10">
<TopBar/>
<router-view />
</main>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<main :key="i18n.locale.value">
<TopBarLogin />
<slot></slot>
<router-view />
</main>
</template>
<script setup lang="ts">

View File

@@ -1,20 +1,15 @@
import { createRouter, createWebHistory } from 'vue-router'
import Empty from '@/layouts/empty.vue'
import { currentLang, langs } from './langs'
import { getSettings } from './settings'
import { useAuthStore } from '@/stores/auth'
import Default from '@/layouts/default.vue'
import { getMenu } from './menu'
function isAuthenticated(): boolean {
if (typeof document === 'undefined') return false
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
}
await getSettings()
// await getMenu()
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_BASE_URL),
routes: [
@@ -25,7 +20,6 @@ const router = createRouter({
{
path: '/:locale',
children: [
{ path: 'category/:category_id-:link_rewrite', component: () => import('../views/CategoryView.vue'), name: 'category' },
{
path: '',
component: Default,
@@ -36,90 +30,51 @@ 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' },
],
},
{ path: 'login', component: () => import('@/views/LoginView.vue'), name: 'login', meta: { guest: true, } },
{ path: 'register', component: () => import('@/views/RegisterView.vue'), name: 'register', meta: { guest: true } },
{ path: 'password-recovery', component: () => import('@/views/PasswordRecoveryView.vue'), name: 'password-recovery', meta: { guest: true } },
{ path: 'reset-password', component: () => import('@/views/ResetPasswordForm.vue'), name: 'reset-password', meta: { guest: true } },
{ path: 'verify-email', component: () => import('@/views/VerifyEmailView.vue'), name: 'verify-email', meta: { guest: true } },
// {
// path: '',
// component: Empty,
// children: [
// ],
// },
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFoundView.vue'),
name: 'not-found',
path: '',
component: Empty,
children: [
{ path: 'login', component: () => import('@/views/LoginView.vue'), name: 'login', meta: { guest: true } },
{ path: 'register', component: () => import('@/views/RegisterView.vue'), name: 'register', meta: { guest: true } },
{ path: 'password-recovery', component: () => import('@/views/PasswordRecoveryView.vue'), name: 'password-recovery', meta: { guest: true } },
{ path: 'reset-password', component: () => import('@/views/ResetPasswordForm.vue'), name: 'reset-password', meta: { guest: true } },
{ path: 'verify-email', component: () => import('@/views/VerifyEmailView.vue'), name: 'verify-email', meta: { guest: true } },
],
},
],
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFoundView.vue'),
name: 'not-found',
},
],
})
// 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)
// // if()
// const validLocale = langs.find((l) => l.lang_code === locale)
// if (validLocale) {
// currentLang.value = localeLang
// if (!to.meta?.guest && !isAuthenticated()) {
// return next({ name: 'login', params: { locale } })
// }
// // return next()
// return next()
// } else if (locale) {
// return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
// }
// }
// if (!locale && to.path !== '/') {
// return next(`/${currentLang.value?.iso_code}${to.path}`)
// }
// next()
// })
router.beforeEach((to, from) => {
router.beforeEach((to, from, next) => {
const locale = to.params.locale as string
const localeLang = langs.find((x) => x.iso_code === locale)
const localeLang = langs.find((x) => x.iso_code == locale)
if (locale && langs.length > 0) {
const authStore = useAuthStore()
console.log(authStore.isAuthenticated, to, from)
// if()
const validLocale = langs.find((l) => l.lang_code === locale)
if (validLocale) {
currentLang.value = localeLang
if (!to.meta?.guest && !authStore.isAuthenticated) {
return { name: 'login', params: { locale } }
if (!to.meta?.guest && !isAuthenticated()) {
return next({ name: 'login', params: { locale } })
}
return true
return next()
} else if (locale) {
return `/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`
return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
}
}
if (!locale && to.path !== '/') {
return `/${currentLang.value?.iso_code}${to.path}`
return next(`/${currentLang.value?.iso_code}${to.path}`)
}
return true
next()
})
export default router

View File

@@ -1,8 +0,0 @@
import { useFetchJson } from "@/composable/useFetchJson";
export const getMenu = async () => {
const resp = await useFetchJson('/api/v1/restricted/menu/get-menu');
return resp.items
}

View File

@@ -95,7 +95,6 @@ export const useAddressStore = defineStore('address', () => {
id: existing.id,
...normalize(formData)
}
return true
}
function deleteAddress(id: number): boolean {

View File

@@ -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) {

View File

@@ -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,
}
})

View File

@@ -1,21 +0,0 @@
<template>
<component :is="Default || 'div'">
<div class="container mt-24">
<div class="row">
<div class="col-12">
<h1>Category</h1>
{{ $route.params }}
</div>
</div>
</div>
</component>
</template>
<script setup lang="ts">
// import { useRoute } from 'vue-router';
import Default from '@/layouts/default.vue';
// const route = useRoute()
// console.log(route);
</script>

View File

@@ -4,8 +4,8 @@ import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui'
import { useI18n } from 'vue-i18n'
import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter()
const route = useRoute()
@@ -52,11 +52,9 @@ const PrivacyComponent = computed(() =>
import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')),
),
)
</script>
<template>
<component :is="Empty || 'div'">
<UDrawer v-model:open="showTherms" :overlay="false">
<template #body>
<component :is="TermsComponent" />
@@ -65,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" />
@@ -78,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">
@@ -96,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>
@@ -116,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">
@@ -165,5 +159,4 @@ const PrivacyComponent = computed(() =>
</p>
</div>
</div>
</component>
</template>

View File

@@ -1,7 +0,0 @@
<template>
<div class="flex flex-col items-center justify-center h-screen">
<h1 class="text-4xl font-bold text-gray-800">404</h1>
<p class="mt-4 text-lg text-gray-600">Page not found</p>
<router-link to="/" class="mt-6 text-blue-500 hover:underline">Go back home</router-link>
</div>
</template>

View File

@@ -5,7 +5,6 @@ import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui'
import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter()
const authStore = useAuthStore()
@@ -37,74 +36,70 @@ function validate(): FormError[] {
</script>
<template>
<component :is="Empty || 'div'">
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<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" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
<div class="w-full max-w-md flex flex-col gap-4">
<template v-if="submitted">
<div class="text-center flex flex-col gap-4">
<UIcon name="i-heroicons-envelope" class="w-12 h-12 mx-auto text-primary-500" />
<h2 class="text-xl font-semibold dark:text-white text-black">{{ $t('general.check_your_email') }}</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.password_reset_link_sent_notice') }}
</p>
<UButton color="neutral" variant="outline" block @click="goToLogin"
class="dark:text-white text-black cursor-pointer">
{{ $t('general.back_to_sign_in') }}
</UButton>
</div>
</template>
<template v-else>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.enter_email_for_password_reset') }}
</p>
</div>
<UForm :validate="validate" @submit="handleRecover" class="flex flex-col gap-3">
<UAlert v-if="authStore.error" color="error" variant="subtle" icon="i-heroicons-exclamation-triangle"
:title="authStore.error" :close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
@close="authStore.clearError" />
<UFormField :label="$t('general.email_address')" name="email" required
class="w-full dark:text-white text-black">
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
</UFormField>
<UButton type="submit" block :loading="authStore.loading"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
{{ $t('general.send_password_reset_link') }}
</UButton>
</UForm>
<div
class="text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4">
<button color="neutral" variant="outline" :loading="authStore.loading"
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
@click="goToLogin">
<UIcon name="mingcute:arrow-left-line"
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" />
{{ $t('general.back_to_sign_in') }}
</button>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.dont_have_an_account') }}
<button variant="link" size="sm" @click="goToRegister"
class=" text-[15px] text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">{{
$t('general.create_account_now') }}
</button>
</p>
</div>
</template>
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<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="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
</div>
</component>
<div class="w-full max-w-md flex flex-col gap-4">
<template v-if="submitted">
<div class="text-center flex flex-col gap-4">
<UIcon name="i-heroicons-envelope" class="w-12 h-12 mx-auto text-primary-500" />
<h2 class="text-xl font-semibold dark:text-white text-black">{{ $t('general.check_your_email') }}</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.password_reset_link_sent_notice') }}
</p>
<UButton color="neutral" variant="outline" block @click="goToLogin"
class="dark:text-white text-black cursor-pointer">
{{ $t('general.back_to_sign_in') }}
</UButton>
</div>
</template>
<template v-else>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.enter_email_for_password_reset') }}
</p>
</div>
<UForm :validate="validate" @submit="handleRecover" class="flex flex-col gap-3">
<UAlert v-if="authStore.error" color="error" variant="subtle" icon="i-heroicons-exclamation-triangle"
:title="authStore.error" :close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
@close="authStore.clearError" />
<UFormField :label="$t('general.email_address')" name="email" required
class="w-full dark:text-white text-black">
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
</UFormField>
<UButton type="submit" block :loading="authStore.loading"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
{{ $t('general.send_password_reset_link') }}
</UButton>
</UForm>
<div class="text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4">
<button color="neutral" variant="outline" :loading="authStore.loading"
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
@click="goToLogin">
<UIcon name="mingcute:arrow-left-line" class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" />
{{ $t('general.back_to_sign_in') }}
</button>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.dont_have_an_account') }}
<button variant="link" size="sm" @click="goToRegister"
class=" text-[15px] text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">{{
$t('general.create_account_now') }}
</button>
</p>
</div>
</template>
</div>
</div>
</template>

View File

@@ -1,5 +1,4 @@
<template>
<component :is="Empty || 'div'">
<UDrawer v-model:open="showTherms" :overlay="false">
<template #body>
<component :is="TermsComponent" />
@@ -22,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">
@@ -111,7 +110,6 @@
</UForm>
</div>
</div>
</component>
</template>
<script setup lang="ts">
@@ -122,8 +120,6 @@ import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui'
import { useI18n } from 'vue-i18n'
import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const { locale } = useI18n()
const router = useRouter()

View File

@@ -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>

View File

@@ -5,7 +5,6 @@ import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui'
import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter()
const route = useRoute()
@@ -50,84 +49,80 @@ function validate(): FormError[] {
</script>
<template>
<component :is="Empty || 'div'">
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<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" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
<div class="w-full max-w-md flex flex-col gap-4">
<template v-if="submitted">
<div class="text-center flex flex-col gap-4">
<UIcon name="i-heroicons-check-circle" class="w-12 h-12 mx-auto text-green-800" />
<h2 class="text-xl font-semibold dark:text-white text-black">
{{ $t('general.password_updated') }}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.password_updated_description') }}
</p>
<UButton block @click="goToLogin" class="dark:text-white text-black">
{{ $t('general.back_to_sign_in') }}
</UButton>
</div>
</template>
<template v-else>
<UForm :validate="validate" @submit="handleReset" class="flex flex-col gap-3">
<UAlert v-if="authStore.error" color="error" variant="subtle"
icon="i-heroicons-exclamation-triangle" :title="authStore.error"
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
@close="authStore.clearError" />
<UFormField :label="$t('general.new_password')" name="new_password" required
class="w-full dark:text-white text-black">
<UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'"
:placeholder="$t('general.enter_your_new_password')" :disabled="authStore.loading"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)"
:ui="{ trailing: 'pe-1' }">
<template #trailing>
<UIcon color="neutral" variant="link" size="sm"
:name="showNewPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:aria-label="showNewPassword ? 'Hide password' : 'Show password'"
:aria-pressed="showNewPassword" aria-controls="new_password"
@click="showNewPassword = !showNewPassword" class="mr-2" />
</template>
</UInput>
</UFormField>
<UFormField :label="$t('general.confirm_password')" name="confirm_new_password" required
class="w-full dark:text-white text-black">
<UInput v-model="confirm_new_password" :type="showConfirmPassword ? 'text' : 'password'"
:placeholder="$t('general.confirm_your_new_password')" :disabled="authStore.loading"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)"
:ui="{ trailing: 'pe-1' }">
<template #trailing>
<UIcon color="neutral" variant="ghost" size="sm"
:name="showConfirmPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
@click="showConfirmPassword = !showConfirmPassword" class="mr-2" />
</template>
</UInput>
</UFormField>
<UButton type="submit" block :loading="authStore.loading"
class="text-white! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
{{ $t('general.reset_password') }}
</UButton>
<div class="text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4">
<button color="neutral" variant="ghost" @click="goToLogin"
class="text-[15px] flex items-center gap-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">
<UIcon name="mingcute:arrow-left-line" />
{{ $t('general.back_to_sign_in') }}
</button>
</div>
</UForm>
</template>
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<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="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
</div>
</component>
<div class="w-full max-w-md flex flex-col gap-4">
<template v-if="submitted">
<div class="text-center flex flex-col gap-4">
<UIcon name="i-heroicons-check-circle" class="w-12 h-12 mx-auto text-green-800" />
<h2 class="text-xl font-semibold dark:text-white text-black">
{{ $t('general.password_updated') }}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.password_updated_description') }}
</p>
<UButton block @click="goToLogin" class="dark:text-white text-black">
{{ $t('general.back_to_sign_in') }}
</UButton>
</div>
</template>
<template v-else>
<UForm :validate="validate" @submit="handleReset" class="flex flex-col gap-3">
<UAlert v-if="authStore.error" color="error" variant="subtle"
icon="i-heroicons-exclamation-triangle" :title="authStore.error"
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
@close="authStore.clearError" />
<UFormField :label="$t('general.new_password')" name="new_password" required
class="w-full dark:text-white text-black">
<UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'"
:placeholder="$t('general.enter_your_new_password')" :disabled="authStore.loading"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
<template #trailing>
<UIcon color="neutral" variant="link" size="sm"
:name="showNewPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:aria-label="showNewPassword ? 'Hide password' : 'Show password'"
:aria-pressed="showNewPassword" aria-controls="new_password"
@click="showNewPassword = !showNewPassword" class="mr-2"/>
</template>
</UInput>
</UFormField>
<UFormField :label="$t('general.confirm_password')" name="confirm_new_password" required
class="w-full dark:text-white text-black">
<UInput v-model="confirm_new_password" :type="showConfirmPassword ? 'text' : 'password'"
:placeholder="$t('general.confirm_your_new_password')" :disabled="authStore.loading"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
<template #trailing>
<UIcon color="neutral" variant="ghost" size="sm"
:name="showConfirmPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
@click="showConfirmPassword = !showConfirmPassword" class="mr-2"/>
</template>
</UInput>
</UFormField>
<UButton type="submit" block :loading="authStore.loading"
class="text-white! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
{{ $t('general.reset_password') }}
</UButton>
<div class="text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4">
<button color="neutral" variant="ghost" @click="goToLogin"
class="text-[15px] flex items-center gap-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">
<UIcon name="mingcute:arrow-left-line" />
{{ $t('general.back_to_sign_in') }}
</button>
</div>
</UForm>
</template>
</div>
</div>
</template>

View File

@@ -4,7 +4,6 @@ import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useFetchJson } from '@/composable/useFetchJson'
import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const { t, te } = useI18n()
const router = useRouter()
@@ -67,84 +66,81 @@ function goToLogin() {
</script>
<template>
<component :is="Empty || 'div'">
<div
class="min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
<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" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
<div
class="min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
<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="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
</div>
<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">
<template #header>
<div class="text-center">
<div v-if="verificationInProgress && loading">
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-primary-500 mx-auto mb-4" />
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.verifying') }}
</h2>
</div>
<div v-else-if="success">
<div
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 text-green-600 mb-4">
<UIcon name="i-heroicons-check-circle" class="w-6 h-6" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.success_title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ $t('verify_email.success_message') }}
</p>
</div>
<div v-else-if="error">
<div
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 text-red-600 mb-4">
<UIcon name="i-heroicons-exclamation-circle" class="w-6 h-6" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.error_title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ $t('verify_email.error_message') }}
</p>
</div>
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
<template #header>
<div class="text-center">
<div v-if="verificationInProgress && loading">
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-primary-500 mx-auto mb-4" />
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.verifying') }}
</h2>
</div>
</template>
<div v-if="success" class="text-center py-4">
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ $t('verify_email.redirect_message') }}</p>
<UButton color="primary" @click="goToLogin">{{ $t('verify_email.go_to_login') }}</UButton>
</div>
<div v-else-if="error" class="text-center py-4">
<UAlert :color="'error'" variant="subtle" icon="i-heroicons-exclamation-triangle" :title="error"
class="mb-4" />
<UButton color="primary" @click="goToLogin" class="cursor-pointer">{{ $t('verify_email.go_to_login') }}
</UButton>
</div>
<div v-else-if="verificationInProgress && loading" class="text-center py-4">
<p class="text-gray-500 dark:text-gray-400">{{ $t('verify_email.please_wait') }}</p>
</div>
<template #footer>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('verify_email.already_registered') }}
<button variant="link" size="sm" @click="goToLogin"
class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{
$t('general.sign_in')
}}
</button>
<div v-else-if="success">
<div
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 text-green-600 mb-4">
<UIcon name="i-heroicons-check-circle" class="w-6 h-6" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.success_title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ $t('verify_email.success_message') }}
</p>
</div>
</template>
</UCard>
</div>
<div v-else-if="error">
<div
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 text-red-600 mb-4">
<UIcon name="i-heroicons-exclamation-circle" class="w-6 h-6" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.error_title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ $t('verify_email.error_message') }}
</p>
</div>
</div>
</template>
<div v-if="success" class="text-center py-4">
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ $t('verify_email.redirect_message') }}</p>
<UButton color="primary" @click="goToLogin">{{ $t('verify_email.go_to_login') }}</UButton>
</div>
<div v-else-if="error" class="text-center py-4">
<UAlert :color="'error'" variant="subtle" icon="i-heroicons-exclamation-triangle" :title="error"
class="mb-4" />
<UButton color="primary" @click="goToLogin" class="cursor-pointer">{{ $t('verify_email.go_to_login') }}
</UButton>
</div>
<div v-else-if="verificationInProgress && loading" class="text-center py-4">
<p class="text-gray-500 dark:text-gray-400">{{ $t('verify_email.please_wait') }}</p>
</div>
<template #footer>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('verify_email.already_registered') }}
<button variant="link" size="sm" @click="goToLogin"
class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{ $t('general.sign_in')
}}
</button>
</p>
</div>
</template>
</UCard>
</div>
</div>
</component>
</div>
</template>

2
go.mod
View File

@@ -11,7 +11,6 @@ require (
github.com/gofiber/fiber/v3 v3.1.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.1
github.com/meilisearch/meilisearch-go v0.36.1
github.com/openai/openai-go/v3 v3.28.0
github.com/samber/lo v1.53.0
golang.org/x/crypto v0.48.0
@@ -30,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