4 Commits

Author SHA1 Message Date
Daniel Goc
bfc488bad8 new fields in meili indexing 2026-03-23 16:43:51 +01:00
Daniel Goc
f5f23f8a27 new fields in meili indexing 2026-03-23 16:42:54 +01:00
cffe4c2f83 Merge pull request 'routing' (#16) from routing into main
Reviewed-on: #16
2026-03-23 13:09:23 +00:00
15e8626280 routing 2026-03-23 14:08:53 +01:00
24 changed files with 494 additions and 303 deletions

4
.env
View File

@@ -25,6 +25,9 @@ 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
@@ -33,6 +36,7 @@ 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

View File

@@ -1,8 +1,6 @@
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"
@@ -52,13 +50,13 @@ func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
// "override_currency": c.Query("override_currency", ""),
// }
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
if err != nil {
id_lang, 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)))
}
listing, err := h.listProductsService.GetListing(uint(id_lang), paging, filters)
listing, err := h.listProductsService.GetListing(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,8 +1,6 @@
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"
@@ -32,13 +30,13 @@ func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
}
func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
if err != nil {
id_lang, 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)))
}
err = h.meiliService.CreateIndex(uint(id_lang))
err := h.meiliService.CreateIndex(id_lang)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
@@ -49,13 +47,13 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
}
func (h *MeiliSearchHandler) Test(c fiber.Ctx) error {
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
if err != nil {
id_lang, 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)))
}
test, err := h.meiliService.Test(uint(id_lang))
test, err := h.meiliService.Test(id_lang)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -1,8 +1,6 @@
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"
@@ -31,13 +29,13 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router {
}
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
if err != nil {
id_lang, 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.GetMenu(uint(id_lang))
menu, err := h.menuService.GetMenu(id_lang)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -88,12 +88,21 @@ type ScannedCategory struct {
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"`
}
type Category struct {
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"`
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"`
}
type FeatVal = map[uint][]uint

View File

@@ -19,10 +19,26 @@ type ProductDescription struct {
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
}
type MeiliSearchProduct struct {
ProductID uint
Name string
Description string
DescriptionShort string
Usage string
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"`
}

View File

@@ -26,15 +26,14 @@ 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.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
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 = ?`,
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
`,
constdata.SHOP_ID, id_lang, constdata.SHOP_ID).
Scan(&allCategories).Error

View File

@@ -12,6 +12,7 @@ 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{}
@@ -73,3 +74,35 @@ 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

@@ -0,0 +1,8 @@
{
"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,13 +9,13 @@ import (
"strings"
"time"
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo"
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
}
@@ -30,56 +30,41 @@ func New() *MeiliService {
return &MeiliService{
meiliClient: client,
productDescriptionRepo: productDescriptionRepo.New(),
}
}
// ==================================== FOR SUPERADMIN ONLY ====================================
func (s *MeiliService) CreateIndex(id_lang uint) error {
var products []model.ProductDescription
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang)
err := db.DB.
Table("ps_product_lang").
Where("id_shop = ? AND id_lang = ?", constdata.SHOP_ID, id_lang).
Scan(&products).Error
if err != nil {
return fmt.Errorf("database error: %w", err)
}
var meiliProducts []model.MeiliSearchProduct
for i := 0; i < len(products); i++ {
var nextMeiliProduct model.MeiliSearchProduct
nextMeiliProduct.ProductID = products[i].ProductID
nextMeiliProduct.Name = products[i].Name
nextMeiliProduct.Description, err = cleanHTML(products[i].Description)
products[i].Description, err = cleanHTML(products[i].Description)
if err != nil {
fmt.Printf("nextMeiliProduct.Description: %v\n", nextMeiliProduct.Description)
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
}
nextMeiliProduct.DescriptionShort, err = cleanHTML(products[i].DescriptionShort)
products[i].DescriptionShort, err = cleanHTML(products[i].DescriptionShort)
if err != nil {
fmt.Printf("nextMeiliProduct.DescriptionShort: %v\n", nextMeiliProduct.DescriptionShort)
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.Usage, err = cleanHTML(products[i].Usage)
products[i].Usage, err = cleanHTML(products[i].Usage)
if err != nil {
fmt.Printf("nextMeiliProduct.Usage: %v\n", nextMeiliProduct.Usage)
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)
@@ -89,7 +74,7 @@ func (s *MeiliService) CreateIndex(id_lang uint) error {
SkipCreation: false,
}
task, err := s.meiliClient.Index(indexName).AddDocuments(meiliProducts, docOptions)
task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions)
if err != nil {
return fmt.Errorf("meili AddDocuments error: %w", err)
}

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.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
@@ -72,10 +72,12 @@ 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.Name = scanned.Name
normal.Subcategories = []model.Category{}
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{}
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("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"),
openAIClient := openai.NewClient(option.WithAPIKey(os.Getenv("OPENAI_KEY")),
option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout
return &ProductDescriptionService{

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

@@ -1,14 +1,56 @@
<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]">
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
<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/>
<router-view />
<slot></slot>
</main>
</div>
</template>

View File

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

View File

@@ -1,9 +1,9 @@
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
@@ -12,6 +12,7 @@ function isAuthenticated(): boolean {
await getSettings()
// await getMenu()
const router = createRouter({
@@ -24,6 +25,7 @@ const router = createRouter({
{
path: '/:locale',
children: [
{ path: 'category/:category_id-:link_rewrite', component: () => import('../views/CategoryView.vue'), name: 'category' },
{
path: '',
component: Default,
@@ -36,49 +38,88 @@ const router = createRouter({
{ path: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' },
],
},
{
path: '',
component: Empty,
children: [
{ path: 'login', component: () => import('@/views/LoginView.vue'), name: 'login', meta: { guest: true } },
{ 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: '/: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 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 && !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) {
return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
return `/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`
}
}
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

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

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

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 { 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,9 +52,11 @@ 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" />
@@ -163,4 +165,5 @@ const PrivacyComponent = computed(() =>
</p>
</div>
</div>
</component>
</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 type { FormError } from '@nuxt/ui'
import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter()
const authStore = useAuthStore()
@@ -36,6 +37,7 @@ 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
@@ -84,11 +86,13 @@ function validate(): FormError[] {
</UButton>
</UForm>
<div class="text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4">
<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]" />
<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">
@@ -102,4 +106,5 @@ function validate(): FormError[] {
</template>
</div>
</div>
</component>
</template>

View File

@@ -1,4 +1,5 @@
<template>
<component :is="Empty || 'div'">
<UDrawer v-model:open="showTherms" :overlay="false">
<template #body>
<component :is="TermsComponent" />
@@ -110,6 +111,7 @@
</UForm>
</div>
</div>
</component>
</template>
<script setup lang="ts">
@@ -120,6 +122,8 @@ 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

@@ -5,6 +5,7 @@ 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()
@@ -49,6 +50,7 @@ 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
@@ -85,7 +87,8 @@ function validate(): FormError[] {
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' }">
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'"
@@ -100,7 +103,8 @@ function validate(): FormError[] {
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' }">
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'"
@@ -125,4 +129,5 @@ function validate(): FormError[] {
</template>
</div>
</div>
</component>
</template>

View File

@@ -4,6 +4,7 @@ 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()
@@ -66,6 +67,7 @@ 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">
@@ -133,7 +135,8 @@ function goToLogin() {
<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')
class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{
$t('general.sign_in')
}}
</button>
</p>
@@ -143,4 +146,5 @@ function goToLogin() {
</div>
</div>
</div>
</component>
</template>