2 Commits

Author SHA1 Message Date
0f494d3abd routing 2026-03-23 16:57:59 +01:00
Daniel Goc
528f12b065 fixed broken HTMLs in ps_product_lang 2026-03-23 12:56:11 +01:00
24 changed files with 3549 additions and 273 deletions

2995
ADD_THIS_TO_SQL.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,6 @@
package restricted package restricted
import ( import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/service/menuService" "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/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
@@ -26,18 +24,33 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMenuHandler() handler := NewMenuHandler()
r.Get("/get-menu", handler.GetMenu) r.Get("/get-menu", handler.GetMenu)
r.Get("/get-routes", handler.GetRouting)
return r return r
} }
func (h *MenuHandler) GetMenu(c fiber.Ctx) error { func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2")) lang_id, ok := c.Locals("langID").(uint)
if err != nil { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
menu, err := h.menuService.GetMenu(lang_id)
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)))
}
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
}
func (h *MenuHandler) GetRouting(c fiber.Ctx) error {
lang_id, ok := c.Locals("langID").(uint)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
menu, err := h.menuService.GetRoutes(lang_id)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -110,6 +110,10 @@ func (s *Server) Setup() error {
meiliSearch := s.restricted.Group("/meili-search") meiliSearch := s.restricted.Group("/meili-search")
restricted.MeiliSearchHandlerRoutes(meiliSearch) restricted.MeiliSearchHandlerRoutes(meiliSearch)
s.api.All("*", func(c fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotFound)
})
// // Restricted routes example // // Restricted routes example
// restricted := s.api.Group("/restricted") // restricted := s.api.Group("/restricted")
// restricted.Use(middleware.AuthMiddleware()) // restricted.Use(middleware.AuthMiddleware())

View File

@@ -82,18 +82,27 @@ type ProductFilters struct {
} }
type ScannedCategory struct { type ScannedCategory struct {
CategoryID uint `gorm:"column:ID;primaryKey"` CategoryID uint `gorm:"column:ID;primaryKey"`
Name string `gorm:"column:name"` Name string `gorm:"column:name"`
Active uint `gorm:"column:active"` Active uint `gorm:"column:active"`
Position uint `gorm:"column:position"` Position uint `gorm:"column:position"`
ParentID uint `gorm:"column:id_parent"` ParentID uint `gorm:"column:id_parent"`
IsRoot uint `gorm:"column:is_root_category"` IsRoot uint `gorm:"column:is_root_category"`
LinkRewrite string `gorm:"column:link_rewrite"`
IsoCode string `gorm:"column:iso_code"`
} }
type Category struct { type Category struct {
CategoryID uint `json:"category_id" form:"category_id"` CategoryID uint `json:"category_id" form:"category_id"`
Name string `json:"name" form:"name"` Label string `json:"label" form:"label"`
Active uint `json:"active" form:"active"` // Active bool `json:"active" form:"active"`
Subcategories []Category `json:"subcategories" form:"subcategories"` 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"`
} }
type FeatVal = map[uint][]uint type FeatVal = map[uint][]uint

21
app/model/routing.go Normal file
View File

@@ -0,0 +1,21 @@
package model
type Route struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(255);not null;unique"`
Path *string `gorm:"type:varchar(255);default:null"`
Component string `gorm:"type:varchar(255);not null;comment:path to component file"`
Layout *string `gorm:"type:varchar(50);default:'default';comment:'default | empty'"`
Meta *string `gorm:"type:longtext;default:'{}'"`
IsActive *bool `gorm:"type:tinyint;default:1"`
SortOrder *int `gorm:"type:int;default:0"`
ParentID *uint `gorm:"index"`
Parent *Route `gorm:"constraint:OnUpdate:RESTRICT,OnDelete:SET NULL;foreignKey:ParentID"`
Children []Route `gorm:"foreignKey:ParentID"`
}
func (Route) TableName() string {
return "b2b_routes"
}

View File

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

View File

@@ -0,0 +1,25 @@
package routesrepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
)
type UIRoutesRepo interface {
GetRoutes(langId uint) ([]model.Route, error)
}
type RoutesRepo struct{}
func New() UIRoutesRepo {
return &RoutesRepo{}
}
func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) {
routes := []model.Route{}
err := db.DB.Find(&routes).Error
if err != nil {
return nil, err
}
return routes, nil
}

View File

@@ -51,15 +51,39 @@ func (s *MeiliService) CreateIndex(id_lang uint) error {
nextMeiliProduct.ProductID = products[i].ProductID nextMeiliProduct.ProductID = products[i].ProductID
nextMeiliProduct.Name = products[i].Name nextMeiliProduct.Name = products[i].Name
nextMeiliProduct.Description = cleanHTML(products[i].Description)
nextMeiliProduct.DescriptionShort = cleanHTML(products[i].DescriptionShort) nextMeiliProduct.Description, err = cleanHTML(products[i].Description)
nextMeiliProduct.Usage = cleanHTML(products[i].Usage) if err != nil {
fmt.Printf("nextMeiliProduct.Description: %v\n", nextMeiliProduct.Description)
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
fmt.Println("failed at description")
fmt.Printf("err: %v\n", err)
return err
}
nextMeiliProduct.DescriptionShort, err = cleanHTML(products[i].DescriptionShort)
if err != nil {
fmt.Printf("nextMeiliProduct.DescriptionShort: %v\n", nextMeiliProduct.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.Usage, err = cleanHTML(products[i].Usage)
if err != nil {
fmt.Printf("nextMeiliProduct.Usage: %v\n", nextMeiliProduct.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) meiliProducts = append(meiliProducts, nextMeiliProduct)
} }
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
primaryKey := "product_id" primaryKey := "ProductID"
docOptions := &meilisearch.DocumentOptions{ docOptions := &meilisearch.DocumentOptions{
PrimaryKey: &primaryKey, PrimaryKey: &primaryKey,
SkipCreation: false, SkipCreation: false,
@@ -123,7 +147,7 @@ func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) {
} }
// remove all tags from HTML text // remove all tags from HTML text
func cleanHTML(s string) string { func cleanHTML(s string) (string, error) {
r := strings.NewReader(s) r := strings.NewReader(s)
d := xml.NewDecoder(r) d := xml.NewDecoder(r)
@@ -137,19 +161,25 @@ func cleanHTML(s string) string {
token, err := d.Token() token, err := d.Token()
if err == io.EOF { if err == io.EOF {
break break
} else if err != nil {
return text, err
} }
switch v := token.(type) { switch v := token.(type) {
case xml.StartElement: case xml.StartElement:
text += "\n" if len(text) > 0 && text[len(text)-1] != '\n' {
text += " \n "
}
case xml.EndElement: case xml.EndElement:
case xml.CharData: case xml.CharData:
text += string(v) if strings.TrimSpace(string(v)) != "" {
text += string(v)
}
case xml.Comment: case xml.Comment:
case xml.ProcInst: case xml.ProcInst:
case xml.Directive: case xml.Directive:
} }
} }
return text return text, nil
} }

View File

@@ -5,23 +5,26 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo" "git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo"
routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
) )
type MenuService struct { type MenuService struct {
categoriesRepo categoriesRepo.UICategoriesRepo categoriesRepo categoriesRepo.UICategoriesRepo
routesRepo routesRepo.UIRoutesRepo
} }
func New() *MenuService { func New() *MenuService {
return &MenuService{ return &MenuService{
categoriesRepo: categoriesRepo.New(), categoriesRepo: categoriesRepo.New(),
routesRepo: routesRepo.New(),
} }
} }
func (s *MenuService) GetMenu(id_lang uint) (model.Category, error) { func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang) all_categories, err := s.categoriesRepo.GetAllCategories(id_lang)
if err != nil { if err != nil {
return model.Category{}, err return &model.Category{}, err
} }
// find the root // find the root
@@ -35,7 +38,7 @@ func (s *MenuService) GetMenu(id_lang uint) (model.Category, error) {
} }
} }
if !root_found { if !root_found {
return model.Category{}, responseErrors.ErrNoRootFound return &model.Category{}, responseErrors.ErrNoRootFound
} }
// now create the children and reorder them according to position // now create the children and reorder them according to position
@@ -57,25 +60,31 @@ func (s *MenuService) GetMenu(id_lang uint) (model.Category, error) {
// finally, create the tree // finally, create the tree
tree := s.createTree(root_index, &all_categories, &children_indices) tree := s.createTree(root_index, &all_categories, &children_indices)
return tree, nil return &tree, nil
} }
func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) model.Category { func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) model.Category {
node := s.scannedToNormalCategory((*all_categories)[index]) node := s.scannedToNormalCategory((*all_categories)[index])
for i := 0; i < len((*children_indices)[index]); i++ { for i := 0; i < len((*children_indices)[index]); i++ {
node.Subcategories = append(node.Subcategories, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices)) node.Children = append(node.Children, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices))
} }
return node return node
} }
func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) {
return s.routesRepo.GetRoutes(id_lang)
}
func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category { func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category {
var normal model.Category var normal model.Category
normal.Active = scanned.Active // normal.Active = scanned.Active
normal.CategoryID = scanned.CategoryID normal.CategoryID = scanned.CategoryID
normal.Name = scanned.Name normal.Label = scanned.Name
normal.Subcategories = []model.Category{} // normal.Active = scanned.Active == 1
normal.Params = model.CategpryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode}
normal.Children = []model.Category{}
return normal return normal
} }

1
bo/components.d.ts vendored
View File

@@ -41,6 +41,7 @@ declare module 'vue' {
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default'] 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'] 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'] 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'] UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default'] USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default'] USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']

View File

@@ -1,14 +1,51 @@
<script setup lang="ts"> <script setup lang="ts">
import TopBar from '@/components/TopBar.vue'; import TopBar from '@/components/TopBar.vue';
import { getMenu } from '@/router/menu'
import type { NavigationMenuItem } from '@nuxt/ui';
import { ref } from 'vue';
let menu = await getMenu() as NavigationMenuItem[]
const openAll = ref(false)
function adaptMenu(menu: NavigationMenuItem[]) {
for (const item of menu) {
if(item.children && item.children.length > 0){
console.log(item);
adaptMenu(item.children);
item.open = openAll.value
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label ,to: { name: 'category', params: item.params }})
} else {
item.to = { name: 'category', params: item.params };
item.icon = 'i-lucide-file-text'
}
}
return menu;
}
menu = adaptMenu(menu)
const items = ref<NavigationMenuItem[][]>([
[
...menu as NavigationMenuItem[]
],
])
</script> </script>
<template> <template>
<div class="h-screen grid grid-rows-[auto_1fr_auto]"> <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> --> <main class="p-10">
<main class="p-10"> <TopBar/>
<TopBar/> <div class="mt-24 w-1/4 bg-accented rounded-2xl">
<router-view /> <button @click="openAll = !openAll">open all</button>
</main> <UNavigationMenu arrow orientation="vertical" :items="items" class="p-4" :key="openAll">
</UNavigationMenu>
</div>
<slot></slot>
</main>
</div> </div>
</template> </template>

View File

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

View File

@@ -1,9 +1,9 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import Empty from '@/layouts/empty.vue'
import { currentLang, langs } from './langs' import { currentLang, langs } from './langs'
import { getSettings } from './settings' import { getSettings } from './settings'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import Default from '@/layouts/default.vue' import Default from '@/layouts/default.vue'
import { getMenu } from './menu'
function isAuthenticated(): boolean { function isAuthenticated(): boolean {
if (typeof document === 'undefined') return false if (typeof document === 'undefined') return false
@@ -12,6 +12,7 @@ function isAuthenticated(): boolean {
await getSettings() await getSettings()
const routes = await getMenu()
const router = createRouter({ const router = createRouter({
@@ -24,6 +25,7 @@ const router = createRouter({
{ {
path: '/:locale', path: '/:locale',
children: [ children: [
{ path: 'category/:category_id-:link_rewrite', component: () => import('../views/CategoryView.vue'), name: 'category' },
{ {
path: '', path: '',
component: Default, component: Default,
@@ -36,49 +38,88 @@ const router = createRouter({
{ path: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' }, { path: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' },
], ],
}, },
{ 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: '', path: '/:pathMatch(.*)*',
component: Empty, component: () => import('@/views/NotFoundView.vue'),
children: [ name: 'not-found',
{ 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) => { // 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) => {
const locale = to.params.locale as string 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) { if (locale && langs.length > 0) {
const authStore = useAuthStore() const authStore = useAuthStore()
console.log(authStore.isAuthenticated, to, from)
// if()
const validLocale = langs.find((l) => l.lang_code === locale) const validLocale = langs.find((l) => l.lang_code === locale)
if (validLocale) { if (validLocale) {
currentLang.value = localeLang currentLang.value = localeLang
if (!to.meta?.guest && !isAuthenticated()) {
return next({ name: 'login', params: { locale } }) if (!to.meta?.guest && !authStore.isAuthenticated) {
return { name: 'login', params: { locale } }
} }
return next() return true
} else if (locale) { } else if (locale) {
return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`) return `/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`
} }
} }
if (!locale && to.path !== '/') { if (!locale && to.path !== '/') {
return next(`/${currentLang.value?.iso_code}${to.path}`) return `/${currentLang.value?.iso_code}${to.path}`
} }
next() return true
}) })
export default router export default router

17
bo/src/router/menu.ts Normal file
View File

@@ -0,0 +1,17 @@
import { useFetchJson } from "@/composable/useFetchJson";
import type { MenuItem, Route } from "@/types/menu";
export const getMenu = async () => {
const resp = await useFetchJson<MenuItem>('/api/v1/restricted/menu/get-menu');
return resp.items.children
}
export const getRoutes = async () => {
const resp = await useFetchJson<Route[]>('/api/v1/restricted/menu/get-routes');
return resp.items
}

26
bo/src/types/menu.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
export interface MenuItem {
category_id: number
label: string
params: Params
children: MenuItem[]
}
export interface Params {
category_id?: number
link_rewrite?: string
locale?: string
}
export interface Route {
ID: number
Name: string
Path: string
Component: string
Layout: string
Meta: string
IsActive: boolean
SortOrder: number
ParentID: any
Parent: any
Children: any
}

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation' import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui' import type { FormError } from '@nuxt/ui'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -49,80 +50,84 @@ function validate(): FormError[] {
</script> </script>
<template> <template>
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8"> <component :is="Empty || 'div'">
<div class="text-center mb-15"> <div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<div <div class="text-center mb-15">
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"> <div
<UIcon name="i-heroicons-clock" class="w-8 h-8" /> 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">
</div> <UIcon name="i-heroicons-clock" class="w-8 h-8" />
<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> </div>
</template> <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-else> <template v-if="submitted">
<UForm :validate="validate" @submit="handleReset" class="flex flex-col gap-3"> <div class="text-center flex flex-col gap-4">
<UAlert v-if="authStore.error" color="error" variant="subtle" <UIcon name="i-heroicons-check-circle" class="w-12 h-12 mx-auto text-green-800" />
icon="i-heroicons-exclamation-triangle" :title="authStore.error" <h2 class="text-xl font-semibold dark:text-white text-black">
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }" {{ $t('general.password_updated') }}
@close="authStore.clearError" /> </h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
<UFormField :label="$t('general.new_password')" name="new_password" required {{ $t('general.password_updated_description') }}
class="w-full dark:text-white text-black"> </p>
<UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'" <UButton block @click="goToLogin" class="dark:text-white text-black">
: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') }} {{ $t('general.back_to_sign_in') }}
</button> </UButton>
</div> </div>
</UForm> </template>
</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> </div>
</div> </component>
</template> </template>

View File

@@ -4,6 +4,7 @@ import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const { t, te } = useI18n() const { t, te } = useI18n()
const router = useRouter() const router = useRouter()
@@ -66,81 +67,84 @@ function goToLogin() {
</script> </script>
<template> <template>
<div <component :is="Empty || '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
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8"> 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="w-full max-w-md"> <div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="text-center mb-8"> <div class="w-full max-w-md">
<div <div class="text-center mb-8">
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"> <div
<UIcon name="i-heroicons-clock" class="w-8 h-8" /> 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>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
<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>
</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>
<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>
</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>
</div> </div>
</div> </component>
</template> </template>

2
go.mod
View File

@@ -11,6 +11,7 @@ require (
github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/fiber/v3 v3.1.0
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.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/openai/openai-go/v3 v3.28.0
github.com/samber/lo v1.53.0 github.com/samber/lo v1.53.0
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.48.0
@@ -29,7 +30,6 @@ require (
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // 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/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect

View File

@@ -1,6 +1,6 @@
-- +goose Up -- +goose Up
-- create routes table -- create routes table
CREATE TABLE IF NOT EXISTS b2b_tracker_routes ( CREATE TABLE IF NOT EXISTS b2b_routes (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL UNIQUE,
path VARCHAR(255) NULL, path VARCHAR(255) NULL,
@@ -13,11 +13,11 @@ CREATE TABLE IF NOT EXISTS b2b_tracker_routes (
CONSTRAINT fk_parent CONSTRAINT fk_parent
FOREIGN KEY (parent_id) FOREIGN KEY (parent_id)
REFERENCES b2b_tracker_routes(id) REFERENCES b2b_routes(id)
ON DELETE SET NULL ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO b2b_tracker_routes INSERT IGNORE INTO b2b_routes
(name, path, component, layout, meta, is_active, sort_order, parent_id) (name, path, component, layout, meta, is_active, sort_order, parent_id)
VALUES VALUES
('root', '', '', 'default', '{"trans": "route.root"}', 0, 0, 0), ('root', '', '', 'default', '{"trans": "route.root"}', 0, 0, 0),
@@ -30,5 +30,5 @@ VALUES
-- +goose Down -- +goose Down
DROP TABLE IF EXISTS b2b_tracker_routes; DROP TABLE IF EXISTS b2b_routes;