Finalize Typescript restructure

This commit is contained in:
Darko Gjorgjijoski
2026-04-06 17:59:15 +02:00
parent cab785172e
commit 74b4b2df4e
209 changed files with 12419 additions and 1745 deletions

View File

@@ -0,0 +1,84 @@
<template>
<div
class="
flex min-h-screen items-center justify-center bg-surface-tertiary px-4 py-12
sm:px-6 lg:px-8
"
>
<NotificationRoot />
<div class="w-full max-w-md">
<div class="mb-10 flex justify-center">
<MainLogo
v-if="!customerLogo"
class="block h-auto w-44 max-w-full text-primary-500"
/>
<img
v-else
:src="customerLogo"
class="block h-auto w-44 max-w-full"
/>
</div>
<div class="rounded-2xl border border-line-default bg-surface px-6 py-8 shadow-sm sm:px-8">
<div class="mb-8 text-left">
<h1 class="text-2xl font-semibold tracking-tight text-heading">
{{ pageTitle }}
</h1>
<p class="mt-2 text-sm leading-6 text-muted">
{{ pageDescription }}
</p>
</div>
<router-view />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import NotificationRoot from '@v2/components/notifications/NotificationRoot.vue'
import MainLogo from '@v2/components/icons/MainLogo.vue'
declare global {
interface Window {
customer_logo?: string
customer_page_title?: string
}
}
const route = useRoute()
const { t } = useI18n()
const customerLogo = computed<string | false>(() => {
return window.customer_logo || false
})
const pageTitle = computed<string>(() => {
if (route.name === 'customer-portal.forgot-password') {
return t('login.forgot_password')
}
if (route.name === 'customer-portal.reset-password') {
return t('login.reset_password')
}
return window.customer_page_title || t('customers.portal_access')
})
const pageDescription = computed<string>(() => {
if (route.name === 'customer-portal.forgot-password') {
return t('login.enter_email')
}
if (route.name === 'customer-portal.reset-password') {
return t('customers.portal_access_text')
}
return t('customers.portal_access_text')
})
</script>

View File

@@ -0,0 +1,166 @@
<template>
<header
class="
fixed top-0 left-0 z-20 flex w-full items-center justify-between border-b
border-line-default bg-surface px-4 py-3 shadow-xs md:px-8
"
>
<div class="flex min-w-0 items-center gap-6">
<router-link
:to="dashboardPath"
class="shrink-0"
>
<MainLogo
v-if="!customerLogo"
class="h-6 w-auto text-primary-500"
/>
<img
v-else
:src="customerLogo"
class="h-6 w-auto"
/>
</router-link>
<nav class="hidden items-center gap-5 md:flex">
<router-link
v-for="item in store.mainMenu"
:key="item.link"
:to="menuLink(item.link)"
:class="[
isActiveLink(item.link)
? 'text-primary-500'
: 'text-muted hover:text-heading',
'text-sm font-medium transition-colors',
]"
>
{{ $t(item.title) }}
</router-link>
</nav>
</div>
<div class="flex items-center gap-3">
<div class="hidden text-right sm:block">
<p class="text-sm font-medium text-heading">
{{ store.currentUser?.name ?? '' }}
</p>
<p class="text-xs text-muted">
{{ store.currentUser?.email ?? '' }}
</p>
</div>
<BaseDropdown width-class="w-56">
<template #activator>
<button
class="flex items-center gap-2 rounded-full p-1 transition-colors hover:bg-surface-tertiary"
type="button"
>
<img
:src="previewAvatar"
class="h-9 w-9 rounded-full object-cover"
/>
<BaseIcon
class="hidden h-4 w-4 text-muted md:block"
name="ChevronDownIcon"
/>
</button>
</template>
<div class="px-2 pb-2 md:hidden">
<router-link
v-for="item in store.mainMenu"
:key="`${item.link}-mobile`"
:to="menuLink(item.link)"
>
<BaseDropdownItem>
{{ $t(item.title) }}
</BaseDropdownItem>
</router-link>
</div>
<router-link :to="settingsPath">
<BaseDropdownItem>
<BaseIcon
class="mr-3 h-5 w-5 text-subtle group-hover:text-muted"
name="CogIcon"
/>
{{ $t('navigation.settings') }}
</BaseDropdownItem>
</router-link>
<BaseDropdownItem @click="logout">
<BaseIcon
class="mr-3 h-5 w-5 text-subtle group-hover:text-muted"
name="ArrowRightOnRectangleIcon"
/>
{{ $t('navigation.logout') }}
</BaseDropdownItem>
</BaseDropdown>
</div>
</header>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useCustomerPortalStore } from '../store'
import { buildCustomerPortalPath, prefixCustomerPortalMenuLink } from '../utils/routes'
import MainLogo from '@v2/components/icons/MainLogo.vue'
declare global {
interface Window {
customer_logo?: string
}
}
const store = useCustomerPortalStore()
const route = useRoute()
const router = useRouter()
const customerLogo = computed<string | false>(() => {
return window.customer_logo || false
})
const dashboardPath = computed<string>(() => {
return buildCustomerPortalPath(store.companySlug, 'dashboard')
})
const settingsPath = computed<string>(() => {
return buildCustomerPortalPath(store.companySlug, 'settings')
})
const previewAvatar = computed<string>(() => {
if (typeof store.currentUser?.avatar === 'string' && store.currentUser.avatar) {
return store.currentUser.avatar
}
return getDefaultAvatar()
})
function getDefaultAvatar(): string {
const imageUrl = new URL('$images/default-avatar.jpg', import.meta.url)
return imageUrl.href
}
function menuLink(link: string): string {
return prefixCustomerPortalMenuLink(store.companySlug, link)
}
function isActiveLink(link: string): boolean {
const resolvedLink = menuLink(link)
if (resolvedLink.endsWith('/dashboard')) {
return route.path === resolvedLink
}
return route.path.startsWith(resolvedLink)
}
async function logout(): Promise<void> {
const companySlug = store.companySlug
await store.logout()
await router.push({
name: 'customer-portal.login',
params: { company: companySlug },
})
}
</script>

View File

@@ -1,6 +1,7 @@
<template>
<div v-if="isAppLoaded" class="h-full">
<slot name="header" />
<NotificationRoot />
<CustomerPortalHeader />
<main class="mt-16 pb-16 h-screen overflow-y-auto min-h-0">
<router-view />
</main>
@@ -8,19 +9,41 @@
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useCustomerPortalStore } from '../store'
import { resolveCompanySlug } from '../utils/routes'
import NotificationRoot from '@v2/components/notifications/NotificationRoot.vue'
import CustomerPortalHeader from './CustomerPortalHeader.vue'
const store = useCustomerPortalStore()
const route = useRoute()
const router = useRouter()
const isAppLoaded = computed<boolean>(() => store.isAppLoaded)
onMounted(async () => {
const companySlug = route.params.company as string
if (companySlug && !store.isAppLoaded) {
await store.bootstrap(companySlug)
}
})
watch(
() => route.params.company,
async (companyParam) => {
const companySlug = resolveCompanySlug(companyParam)
if (!companySlug) {
return
}
if (store.isAppLoaded && store.companySlug === companySlug) {
return
}
try {
await store.bootstrap(companySlug)
} catch {
await router.push({
name: 'customer-portal.login',
params: { company: companySlug },
})
}
},
{ immediate: true }
)
</script>