timetracker update
This commit is contained in:
@@ -11,14 +11,6 @@ export const uiOptions: NuxtUIOptions = {
|
||||
root: '',
|
||||
}
|
||||
},
|
||||
// selectMenu: {
|
||||
// variants: {
|
||||
// size: {
|
||||
// xxl: {
|
||||
// group: 'mt-20!'
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
button: {
|
||||
slots: {
|
||||
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
@@ -27,115 +19,29 @@ export const uiOptions: NuxtUIOptions = {
|
||||
input: {
|
||||
slots: {
|
||||
base: 'text-(--black) dark:text-white border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
error: 'text-red-600!'
|
||||
},
|
||||
},
|
||||
// variants: {
|
||||
// size: {
|
||||
// xxl: {
|
||||
// base: 'h-8 sm:h-[38px] px-[10px] py-[10px] border! border-(--border-light)! dark:border-(--border-dark)!',
|
||||
// trailingIcon: 'px-6 !text-base',
|
||||
// root: 'w-full',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// defaultVariants: {
|
||||
// size: 'xxl',
|
||||
// },
|
||||
// },
|
||||
// textarea: {
|
||||
// slots: {
|
||||
// base: 'disabled:!opacity-100 text-(--black) dark:text-white disabled:text-(--gray) !text-base placeholder:text-(--gray)/50! dark:placeholder:text-(--gray)!',
|
||||
// trailingIcon: 'shrink-0 pr-4 !text-base',
|
||||
// error: 'text-sm! !sm:text-[15px] leading-none! mt-1!',
|
||||
// },
|
||||
// variants: {
|
||||
// size: {
|
||||
// xxl: {
|
||||
// base: 'px-[25px] py-[15px]',
|
||||
// trailingIcon: 'px-6 !text-base',
|
||||
// root: 'w-full',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// defaultVariants: {
|
||||
// size: 'xxl',
|
||||
// },
|
||||
// },
|
||||
// formField: {
|
||||
// slots: {
|
||||
// base: 'flex !flex-col border! border-(--border-light)! dark:border-(--border-dark)!',
|
||||
// label: 'text-[15px] text-(--gray)! dark:text-(--gray-dark)! pl-6! leading-none! font-normal! mb-1 sm:mb-1',
|
||||
// error: 'text-sm! !sm:text-[15px] leading-none! mt-1!',
|
||||
// },
|
||||
// variants: {
|
||||
// size: {
|
||||
// xxl: 'w-full',
|
||||
// label: '!label !mb-1',
|
||||
// },
|
||||
// },
|
||||
// defaultVariants: {
|
||||
// size: 'xxl',
|
||||
// },
|
||||
// },
|
||||
|
||||
// defaultVariants: {
|
||||
// size: 'xxl',
|
||||
// },
|
||||
// },
|
||||
select: {
|
||||
slots: {
|
||||
base: 'w-full! cursor-pointer border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
itemLabel: 'text-black! dark:text-white!',
|
||||
itemTrailingIcon: 'text-black! dark:text-white!'
|
||||
},
|
||||
// variants: {
|
||||
// size: {
|
||||
// xxl: {
|
||||
// base: ' h-12 sm:h-[54px] px-[25px]',
|
||||
// item: 'py-2 px-2',
|
||||
// trailingIcon: 'px-6 !text-base',
|
||||
// leading: '!px-[25px]',
|
||||
// itemLabel: 'text-black dark:text-white',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// defaultVariants: {
|
||||
// size: 'xxl',
|
||||
// },
|
||||
// },
|
||||
// inputDate: {
|
||||
// slots: {
|
||||
// leadingIcon: 'border-none! outline-0! ring-0!',
|
||||
// },
|
||||
// defaultVariants: {
|
||||
// size: 'xxl',
|
||||
// },
|
||||
// },
|
||||
// checkbox: {
|
||||
// slots: {
|
||||
// label: 'block !font-normal',
|
||||
// indicator: '!bg-(--accent-brown)',
|
||||
// },
|
||||
// },
|
||||
// radioGroup: {
|
||||
// slots: {
|
||||
// label: 'block !font-normal text-base font-normal leading-none text-(--black) dark:text-(--second-light)',
|
||||
// indicator: '!bg-(--accent-brown)',
|
||||
// size: 'xxl',
|
||||
// },
|
||||
},
|
||||
formField: {
|
||||
slots: {
|
||||
error: 'mt-1! text-[14px] text-error text-red-600! dark:text-red-400!',
|
||||
label: 'text-[16px]'
|
||||
},
|
||||
},
|
||||
selectMenu: {
|
||||
slots: {
|
||||
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! z-80 text-(--black)! dark:text-white!',
|
||||
itemLeadingIcon: 'text-(--black)! dark:text-white!'
|
||||
}
|
||||
|
||||
// },
|
||||
// modal: {
|
||||
// slots: {
|
||||
// overlay: 'dark:bg-(--main-dark)/90',
|
||||
// },
|
||||
// },
|
||||
// tooltip: {
|
||||
// slots: {
|
||||
// content: 'max-w-60 sm:max-w-100 bg-(--main-light)! dark:bg-(--black)! w-full h-full',
|
||||
// text: 'whitespace-normal',
|
||||
// },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,14 +25,6 @@ body {
|
||||
--gray: #6B6B6B;
|
||||
--gray-dark: #A3A3A3;
|
||||
|
||||
--accent-green: #004F3D;
|
||||
--accent-green-dark: #00A882;
|
||||
--accent-brown: #9A7F62;
|
||||
--accent-red: #B72D2D;
|
||||
--dark-red: #F94040;
|
||||
--accent-orange: #E68D2B;
|
||||
--accent-blue: #002B4F;
|
||||
|
||||
/* borders */
|
||||
--border-light: #E8E7E0;
|
||||
--border-dark: #3F3E3D;
|
||||
@@ -44,16 +36,9 @@ body {
|
||||
--placeholder: #8C8C8A;
|
||||
|
||||
--ui-bg: var(--main-light);
|
||||
--ui-primary: var(--color-gray-300);
|
||||
--ui-secondary: var(--accent-green);
|
||||
--ui-border-accented: var(--border-light);
|
||||
--ui-text-dimmed: var(--gray);
|
||||
--ui-bg-elevated: var(--color-gray-300);
|
||||
--ui-border: var(--border-light);
|
||||
--ui-color-neutral-700: var(--black);
|
||||
--ui-error: var(--accent-red);
|
||||
--border: var(--border-light);
|
||||
--tw-border-style: var(--border-light);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
36
bo/src/components/TopBar.vue
Normal file
36
bo/src/components/TopBar.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import LangSwitch from './inner/langSwitch.vue'
|
||||
import ThemeSwitch from './inner/themeSwitch.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
|
||||
<div class="container px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-14">
|
||||
<!-- 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" />
|
||||
</div>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
|
||||
</RouterLink>
|
||||
<!-- Right Side Actions -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Language Switcher -->
|
||||
<LangSwitch />
|
||||
<!-- Theme Switcher -->
|
||||
<ThemeSwitch />
|
||||
<!-- Logout Button (only when authenticated) -->
|
||||
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
||||
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark)">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import HomeView from '@/views/HomeView.vue';
|
||||
import LangSwitch from './inner/langSwitch.vue'
|
||||
import ThemeSwitch from './inner/themeSwitch.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -20,17 +19,11 @@ const authStore = useAuthStore()
|
||||
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
|
||||
</RouterLink>
|
||||
<!-- Right Side Actions -->
|
||||
<HomeView />
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Language Switcher -->
|
||||
<LangSwitch />
|
||||
<!-- Theme Switcher -->
|
||||
<ThemeSwitch />
|
||||
<!-- Logout Button (only when authenticated) -->
|
||||
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
||||
class="px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 hover:text-primary dark:hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<button :type="type" :disabled="disabled"
|
||||
:class="['px-[25px] h-[43px] leading-none rounded-md text-[15px] sm:text-[16px] dark:text-white text-black',
|
||||
fillType === 'border' ? 'border border-(--border-light) dark:border-(--border-dark)' : false,]">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{ type?: 'button' | 'submit', fillType?: 'border', disabled?: boolean }>(), {
|
||||
type: 'button',
|
||||
fillType: 'border',
|
||||
disabled: false,
|
||||
})
|
||||
</script>
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<USelectMenu v-model="locale" :items="langs"
|
||||
class="w-40 bg-white dark:bg-(--black) rounded-md shadow-sm hover:none!"
|
||||
<USelectMenu v-model="locale" :items="langs" class="w-40 bg-white dark:bg-(--black) rounded-md shadow-sm hover:none!"
|
||||
valueKey="iso_code" :searchInput="false">
|
||||
<template #default="{ modelValue }">
|
||||
<div class="flex items-center gap-1">
|
||||
@@ -22,7 +21,7 @@ import { langs, currentLang } from '@/router/langs'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useCookie } from '@/composable/useCookie'
|
||||
import { computed, watch } from 'vue'
|
||||
import { i18n } from '@/plugins/i18n'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -36,28 +35,23 @@ const locale = computed({
|
||||
i18n.locale.value = value
|
||||
currentLang.value = langs.find((x) => x.iso_code == value)
|
||||
|
||||
// Update URL to reflect language change
|
||||
const currentPath = route.path
|
||||
const pathParts = currentPath.split('/').filter(Boolean)
|
||||
|
||||
cookie.setCookie('lang_id', `${langs.find((x) => x.iso_code == value)?.id}`, { days: 60, secure: true, sameSite: 'Lax' })
|
||||
|
||||
if (pathParts.length > 0) {
|
||||
// Check if first part is a locale
|
||||
const isLocale = langs.some((l) => l.lang_code === pathParts[0])
|
||||
if (isLocale) {
|
||||
// Replace existing locale
|
||||
pathParts[0] = value
|
||||
router.replace({ path: '/' + pathParts.join('/'), query: route.query })
|
||||
} else {
|
||||
// Add locale to path
|
||||
router.replace({ path: '/' + value + currentPath, query: route.query })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Sync i18n locale with router locale on initial load
|
||||
watch(
|
||||
() => route.params.locale,
|
||||
(newLocale) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useFetchJson } from './useFetchJson'
|
||||
import type { Resp } from '@/types/response'
|
||||
|
||||
const API_PREFIX = '/api/v1/repo'
|
||||
const API_PREFIX = '/api/v1/restricted/repo'
|
||||
|
||||
export interface QuarterData {
|
||||
quarter: string
|
||||
@@ -35,7 +34,6 @@ export async function getRepos(): Promise<any> {
|
||||
// export async function getYears(repoID: number): Promise<any> {
|
||||
// return useFetchJson<number[]>(`${API_PREFIX}/get-years?repoID=${repoID}`)
|
||||
// }
|
||||
// console.log(getYears(), 'leraaaaaa')
|
||||
|
||||
export async function getYears(repoID: number): Promise<any> {
|
||||
return useFetchJson<number[]>(`${API_PREFIX}/get-years?repoID=${repoID}`);
|
||||
@@ -61,7 +59,6 @@ export async function getQuarters(repoID: number, year: number): Promise<any> {
|
||||
// }
|
||||
// async function logYears() {
|
||||
// const years = await getIssues(7); // pass a repoID
|
||||
// console.log(years, 'leraaaaaa');
|
||||
// }
|
||||
export async function getIssues(
|
||||
repoID: number,
|
||||
@@ -74,14 +71,3 @@ export async function getIssues(
|
||||
`${API_PREFIX}/get-issues?repoID=${repoID}&year=${year}&quarter=${quarter}&page_number=${page}&elements_per_page=${pageSize}`
|
||||
);
|
||||
}
|
||||
|
||||
// Correct logging function
|
||||
async function logIssues() {
|
||||
const repoID = 7;
|
||||
const year = 2026; // example year
|
||||
const quarter = 1; // example quarter
|
||||
const issues = await getIssues(repoID, year, quarter);
|
||||
console.log(issues, 'leraaaaaa');
|
||||
}
|
||||
|
||||
logIssues();
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { FormError } from '@nuxt/ui'
|
||||
import { settings } from '@/router/settings'
|
||||
import { i18n } from '@/plugins/i18n'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
|
||||
export const useValidation = () => {
|
||||
const errors = [] as FormError[]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import TopBarLogin from '@/components/TopBarLogin.vue'
|
||||
import TopBar from '@/components/TopBar.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -7,6 +8,7 @@ import TopBarLogin from '@/components/TopBarLogin.vue'
|
||||
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
|
||||
<UContainer>
|
||||
<main class="p-10">
|
||||
<TopBar/>
|
||||
<router-view />
|
||||
</main>
|
||||
</UContainer>
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import TopBarLogin from '@/components/TopBarLogin.vue'
|
||||
import { i18n } from '@/plugins/i18n'
|
||||
import HomeView from '@/views/HomeView.vue';
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import './assets/main.css'
|
||||
import { i18ninstall } from '@/plugins/i18n'
|
||||
|
||||
import { i18ninstall } from '@/plugins/02_i18n'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from '@/App.vue'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ui)
|
||||
app.use(i18ninstall)
|
||||
app.mount('#app')
|
||||
app.mount('#app')
|
||||
@@ -1,12 +1,11 @@
|
||||
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import { langs } from '@/router/langs'
|
||||
import type { Resp } from '@/types'
|
||||
import { getLangs } from '@/utils/fake'
|
||||
import { initLangs, langs } from '@/router/langs'
|
||||
import { watch } from 'vue'
|
||||
import { createI18n, type LocaleMessageValue, type PathValue, type VueMessageType } from 'vue-i18n'
|
||||
import { createI18n, type PathValue } from 'vue-i18n'
|
||||
|
||||
// const x =
|
||||
|
||||
await initLangs()
|
||||
export const i18ninstall = createI18n({
|
||||
legacy: false, // you must set `false`, to use Composition API
|
||||
locale: 'en',
|
||||
@@ -30,18 +29,22 @@ export const i18n = i18ninstall.global
|
||||
|
||||
let downloadedLangs = [] as string[]
|
||||
|
||||
const getLangs = async (l: string) => {
|
||||
|
||||
if (!downloadedLangs.includes(l)) {
|
||||
const lang = langs.find((x) => x.iso_code == l)
|
||||
if (!lang) return
|
||||
downloadedLangs.push(l)
|
||||
const res = await useFetchJson<any>(`/api/v1/translations?lang_id=${lang?.id}&scope=backoffice`)
|
||||
i18n.setLocaleMessage(l, res.items[lang.id]['backoffice'])
|
||||
}
|
||||
}
|
||||
|
||||
getLangs(i18n.locale.value)
|
||||
|
||||
watch(
|
||||
i18n.locale,
|
||||
async (l) => {
|
||||
if (!downloadedLangs.includes(l)) {
|
||||
const lang = langs.find((x) => x.iso_code == l)
|
||||
if (!lang) return
|
||||
downloadedLangs.push(l)
|
||||
const res = await useFetchJson<any>(`/api/v1/translations?lang_id=${lang?.id}&scope=backoffice`)
|
||||
// console.log(res.items[lang.id as number]['backoffice'])
|
||||
|
||||
i18n.setLocaleMessage(l, res.items[lang.id]['backoffice'])
|
||||
}
|
||||
},
|
||||
{},
|
||||
await getLangs(l)
|
||||
}
|
||||
)
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import Default from '@/layouts/default.vue'
|
||||
import Empty from '@/layouts/empty.vue'
|
||||
import { currentLang, initLangs, langs } from './langs'
|
||||
import { currentLang, langs } from './langs'
|
||||
import { getSettings } from './settings'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import Default from '@/layouts/default.vue'
|
||||
|
||||
// Helper: read the non-HTTPOnly is_authenticated cookie set by the backend.
|
||||
// The backend sets it to "1" on login and removes it on logout.
|
||||
@@ -13,7 +13,6 @@ function isAuthenticated(): boolean {
|
||||
}
|
||||
|
||||
|
||||
await initLangs()
|
||||
await getSettings()
|
||||
|
||||
|
||||
@@ -27,18 +26,17 @@ const router = createRouter({
|
||||
{
|
||||
path: '/:locale',
|
||||
children: [
|
||||
// {
|
||||
// path: '',
|
||||
// component: Default,
|
||||
// children: [
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
path: '',
|
||||
component: Default,
|
||||
children: [
|
||||
{ path: '', component: () => import('../views/RepoChartView.vue'), name: 'home' },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: Empty,
|
||||
children: [
|
||||
{ path: '', component: () => import('../views/HomeView.vue'), name: 'home' },
|
||||
{ path: 'chart', component: () => import('../views/RepoChartView.vue'), name: 'chart' },
|
||||
{ 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 } },
|
||||
@@ -58,6 +56,9 @@ router.beforeEach((to, from, next) => {
|
||||
|
||||
// Check if the locale is valid
|
||||
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) {
|
||||
|
||||
@@ -46,7 +46,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const data = await useFetchJson<AuthResponse>('/api/v1/auth/login', {
|
||||
const data = await useFetchJson<AuthResponse>('/api/v1/public/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
@@ -81,7 +81,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await useFetchJson('/api/v1/auth/register', {
|
||||
await useFetchJson('/api/v1/public/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ first_name, last_name, email, password, confirm_password, lang: lang || 'en' }),
|
||||
@@ -101,7 +101,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
await useFetchJson('/api/v1/auth/forgot-password', {
|
||||
await useFetchJson('/api/v1/public/auth/forgot-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
@@ -121,7 +121,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
await useFetchJson('/api/v1/auth/reset-password', {
|
||||
await useFetchJson('/api/v1/public/auth/reset-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, password }),
|
||||
@@ -137,7 +137,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
function loginWithGoogle() {
|
||||
window.location.href = '/api/v1/auth/google'
|
||||
window.location.href = '/api/v1/public/auth/google'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +146,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
*/
|
||||
async function logout() {
|
||||
try {
|
||||
await useFetchJson('/api/v1/auth/logout', {
|
||||
await useFetchJson('/api/v1/public/auth/logout', {
|
||||
method: 'POST',
|
||||
})
|
||||
} catch {
|
||||
@@ -167,7 +167,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
*/
|
||||
async function refreshAccessToken(): Promise<boolean> {
|
||||
try {
|
||||
await useFetchJson('/api/v1/auth/refresh', {
|
||||
await useFetchJson('/api/v1/public/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
// No body needed — the backend reads the refresh_token from the HTTPOnly cookie
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main class="flex gap-4">
|
||||
<RouterLink class="bg-(--color-blue-600) dark:bg-(--color-blue-500) px-2 py-1 rounded text-white flex items-center shadow-md"
|
||||
:to="{ name: 'login' }">Login
|
||||
</RouterLink>
|
||||
<RouterLink class="bg-(--color-blue-600) dark:bg-(--color-blue-500) px-2 py-1 rounded text-white flex items-center shadow-md"
|
||||
:to="{ name: 'register' }">
|
||||
Register</RouterLink>
|
||||
<RouterLink class="bg-(--color-blue-600) dark:bg-(--color-blue-500) px-2 py-1 rounded text-white flex items-center shadow-md"
|
||||
:to="{ name: 'chart' }">Chart
|
||||
</RouterLink>
|
||||
</main>
|
||||
</template>
|
||||
@@ -5,9 +5,8 @@ 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/i18n'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
@@ -20,7 +19,7 @@ async function handleLogin() {
|
||||
const success = await authStore.login(email.value, password.value)
|
||||
if (success) {
|
||||
const redirectTo = route.query.redirect as string
|
||||
router.push(redirectTo || { name: 'chart' })
|
||||
router.push(redirectTo || { name: 'home' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +74,14 @@ const PrivacyComponent = computed(() =>
|
||||
<UButton @click="showPrivacy = false" class="mx-auto px-12">close</UButton>
|
||||
</template>
|
||||
</UDrawer>
|
||||
<div class="h-[100vh] flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||
<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">
|
||||
<UForm :validate="validate" @submit="handleLogin" class="space-y-5">
|
||||
<UAlert v-if="authStore.error" color="error" variant="subtle" :title="authStore.error"
|
||||
@@ -85,28 +91,28 @@ const PrivacyComponent = computed(() =>
|
||||
<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" />
|
||||
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
|
||||
</UFormField>
|
||||
|
||||
<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" :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'"
|
||||
<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" />
|
||||
aria-controls="password" @click="showPassword = !showPassword" class="mr-2"/>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
|
||||
<div class="flex items-center justify-between w-full dark:text-white text-black">
|
||||
<button variant="link" size="sm" @click="goToPasswordRecovery" class="text-[15px] w-full flex justify-end text-(--color-blue-600) dark:text-(--color-blue-500)">
|
||||
{{$t('general.forgot_password')}}?
|
||||
<button variant="link" size="sm" @click="goToPasswordRecovery"
|
||||
class="text-[15px] w-full flex justify-end text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">
|
||||
{{ $t('general.forgot_password') }}?
|
||||
</button>
|
||||
</div>
|
||||
<UButton type="submit" :loading="authStore.loading"
|
||||
class="w-full flex justify-center text-white bg-(--color-blue-600) dark:bg-(--color-blue-500)">
|
||||
class="w-full flex justify-center text-white bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
|
||||
{{ $t('general.sign_in') }}
|
||||
</UButton>
|
||||
</UForm>
|
||||
@@ -120,7 +126,8 @@ const PrivacyComponent = computed(() =>
|
||||
|
||||
<!-- 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">
|
||||
@click="authStore.loginWithGoogle()"
|
||||
class="flex items-center justify-center gap-2 dark:text-white text-black cursor-pointer">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
@@ -140,18 +147,20 @@ const PrivacyComponent = computed(() =>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="dark:text-white text-black">
|
||||
{{$t('general.dont_have_an_account')}}?
|
||||
<button variant="link" size="sm" class="text-[15px] text-(--color-blue-600) dark:text-(--color-blue-500)" @click="goToRegister">{{ $t('general.create_account_now') }}</button>
|
||||
{{ $t('general.dont_have_an_account') }}?
|
||||
<button variant="link" size="sm"
|
||||
class="text-[15px] text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer"
|
||||
@click="goToRegister">{{ $t('general.create_account_now') }}</button>
|
||||
</p>
|
||||
</div>
|
||||
<p class="mt-8 text-center text-xs dark:text-white text-black">
|
||||
{{ $t('general.by_signing_in_you_agree_to_our') }}
|
||||
<span @click="showTherms = !showTherms"
|
||||
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{
|
||||
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">{{
|
||||
$t('general.terms_of_service') }}</span>
|
||||
{{ $t('general.and') }}
|
||||
<span @click="showPrivacy = !showPrivacy"
|
||||
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{
|
||||
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">{{
|
||||
$t('general.privacy_policy') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,8 @@ import { useRouter } 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/i18n'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const validation = useValidation()
|
||||
@@ -30,20 +28,24 @@ function goToRegister() {
|
||||
router.push({ name: 'register' })
|
||||
}
|
||||
|
||||
|
||||
function validate(): FormError[] {
|
||||
validation.reset()
|
||||
validation.validateEmail(email, 'email', i18n.t('validate_error.email_required'))
|
||||
return validation.errors
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[100vh] flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||
<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">
|
||||
|
||||
<!-- Success State -->
|
||||
<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" />
|
||||
@@ -51,13 +53,13 @@ function validate(): FormError[] {
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<!-- Form State -->
|
||||
<template v-else>
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
@@ -73,29 +75,31 @@ function validate(): FormError[] {
|
||||
<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" />
|
||||
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" block :loading="authStore.loading"
|
||||
class="text-white bg-(--color-blue-600) dark:bg-(--color-blue-500)">
|
||||
class="text-white bg-(--color-blue-600) dark:bg-(--color-blue-500) 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">
|
||||
<UButton color="neutral" variant="outline" :loading="authStore.loading"
|
||||
class="w-full flex justify-center dark:text-white text-black" @click="goToLogin">
|
||||
<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-(--color-blue-600) dark:text-(--color-blue-500) text-[16px]" />
|
||||
{{ $t('general.back_to_sign_in') }}
|
||||
</UButton>
|
||||
</button>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ $t('general.dont_have_an_account') }}
|
||||
<UButton variant="link" size="sm" @click="goToRegister"
|
||||
class="text-(--color-blue-600) dark:text-(--color-blue-500)">{{ $t('general.create_account_now') }}
|
||||
</UButton>
|
||||
<button variant="link" size="sm" @click="goToRegister"
|
||||
class=" text-[15px] text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">{{
|
||||
$t('general.create_account_now') }}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,50 +17,62 @@
|
||||
</template>
|
||||
</UDrawer>
|
||||
|
||||
<div class="h-[100vh] flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||
<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">
|
||||
<UForm :validate="validate" @submit="handleRegister" 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.first_name')" name="first_name" required class="w-full dark:text-white text-black ">
|
||||
<UInput class="w-full" v-model="first_name" type="text" :placeholder="$t('general.first_name')"
|
||||
<UFormField :label="$t('general.first_name')" name="first_name" required
|
||||
class="w-full dark:text-white text-black ">
|
||||
<UInput class="w-full placeholder:text-(--placeholder)" v-model="first_name" type="text" :placeholder="$t('general.first_name')"
|
||||
:disabled="authStore.loading">
|
||||
</UInput>
|
||||
</UFormField>
|
||||
|
||||
<UFormField :label="$t('general.last_name')" name="last_name" required class="w-full dark:text-white text-black">
|
||||
<UInput class="w-full dark:text-white text-black" v-model="last_name" type="text" :placeholder="$t('general.last_name')"
|
||||
:disabled="authStore.loading">
|
||||
<UFormField :label="$t('general.last_name')" name="last_name" required
|
||||
class="w-full dark:text-white text-black">
|
||||
<UInput class="w-full dark:text-white text-black placeholder:text-(--placeholder)" v-model="last_name" type="text"
|
||||
:placeholder="$t('general.last_name')" :disabled="authStore.loading">
|
||||
</UInput>
|
||||
</UFormField>
|
||||
|
||||
<UFormField :label="$t('general.email_address')" name="email" required class="w-full dark:text-white text-black">
|
||||
<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" />
|
||||
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
|
||||
</UFormField>
|
||||
|
||||
<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')" :disabled="authStore.loading"
|
||||
class="w-full dark:text-white text-black" :type="showPassword ? 'text' : 'password'" :ui="{ trailing: 'pe-1' }">
|
||||
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :type="showPassword ? 'text' : 'password'"
|
||||
:ui="{ trailing: 'pe-1' }">
|
||||
<template #trailing>
|
||||
<UIcon color="neutral" variant="link" size="sm"
|
||||
:name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
||||
<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" />
|
||||
aria-controls="password" @click="showPassword = !showPassword" class="mr-2"/>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
|
||||
<UFormField :label="$t('general.confirm_password')" name="confirm_password" required class="w-full dark:text-white text-black">
|
||||
<UInput v-model="confirm_password_ref" :type="showConfirmPassword ? 'text' : 'password'" class="w-full dark:text-white text-black"
|
||||
:placeholder="$t('general.confirm_your_password')" :disabled="authStore.loading" :ui="{ trailing: 'pe-1' }">
|
||||
<UFormField :label="$t('general.confirm_password')" name="confirm_password" required
|
||||
class="w-full dark:text-white text-black">
|
||||
<UInput v-model="confirm_password_ref" :type="showConfirmPassword ? 'text' : 'password'"
|
||||
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :placeholder="$t('general.confirm_your_password')"
|
||||
:disabled="authStore.loading" :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" />
|
||||
@click="showConfirmPassword = !showConfirmPassword" class="mr-2"/>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
@@ -69,16 +81,21 @@
|
||||
<template #label>
|
||||
<span class="dark:text-white text-black">
|
||||
{{ $t('general.i_agree_to_the') }}
|
||||
<span @click="showTherms = !showTherms" class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{ $t('general.terms_of_service')
|
||||
}}</span>
|
||||
<span @click="showTherms = !showTherms"
|
||||
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{
|
||||
$t('general.terms_of_service')
|
||||
}}</span>
|
||||
{{ $t('general.and') }}
|
||||
<span @click="showPrivacy = !showPrivacy" class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{ $t('general.privacy_policy')
|
||||
}}</span>
|
||||
<span @click="showPrivacy = !showPrivacy"
|
||||
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{
|
||||
$t('general.privacy_policy')
|
||||
}}</span>
|
||||
</span>
|
||||
</template>
|
||||
</UCheckbox>
|
||||
|
||||
<UButton type="submit" block :loading="authStore.loading" :disabled="!acceptTerms" class="text-white bg-(--color-blue-600) dark:bg-(--color-blue-500)">
|
||||
<UButton type="submit" block :loading="authStore.loading" :disabled="!acceptTerms"
|
||||
class="text-white bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
|
||||
{{ $t('general.create_account') }}
|
||||
</UButton>
|
||||
|
||||
@@ -86,7 +103,8 @@
|
||||
<p class="dark:text-white text-black">
|
||||
{{ $t('general.already_have_an_account') }}
|
||||
</p>
|
||||
<UButton color="neutral" variant="outline" :loading="authStore.loading" class="w-full flex justify-center dark:text-white hover:text-white hover:bg-(--color-blue-600) dark:hover:bg-(--color-blue-500) border border-(--border-light)! dark:border-(--border-dark)!"
|
||||
<UButton color="neutral" variant="outline" :loading="authStore.loading"
|
||||
class="w-full flex justify-center dark:text-white hover:text-white cursor-pointer hover:bg-(--color-blue-600) dark:hover:bg-(--color-blue-500) border border-(--border-light)! dark:border-(--border-dark)!"
|
||||
@click="goToLogin">{{ $t('general.sign_in') }}</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
@@ -101,7 +119,7 @@ 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/i18n'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
|
||||
const { locale } = useI18n()
|
||||
const router = useRouter()
|
||||
@@ -149,7 +167,5 @@ const PrivacyComponent = computed(() =>
|
||||
defineAsyncComponent(() =>
|
||||
import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')),
|
||||
),
|
||||
|
||||
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
} from 'chart.js'
|
||||
import { getRepos, getYears, getQuarters, getIssues, type QuarterData, type IssueTimeSummary } from '@/composable/useRepoApi'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { i18n } from '@/plugins/i18n'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||
|
||||
@@ -86,7 +87,7 @@ async function loadIssues(repoID: number, year: number, quarterStr: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await getIssues(repoID, year, parseInt(quarterPart), page.value, 50)
|
||||
const response = await getIssues(repoID, year, parseInt(quarterPart), page.value, 10)
|
||||
issues.value = response.items || []
|
||||
totalItems.value = response.items_count || 0
|
||||
} catch (e: any) {
|
||||
@@ -136,23 +137,64 @@ const quarterItems = computed(() => [
|
||||
label: `${q.quarter} (${q.time.toFixed(1)}h)`
|
||||
}))
|
||||
])
|
||||
|
||||
const columns: TableColumn<IssueTimeSummary>[] = [
|
||||
{
|
||||
accessorKey: 'IssueID',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'IssueName',
|
||||
header: i18n.t('repo_chart.issue_name'),
|
||||
},
|
||||
// {
|
||||
// accessorKey: 'Initials',
|
||||
// header: i18n.t('repo_chart.user_initials'),
|
||||
// },
|
||||
{
|
||||
accessorKey: 'CreatedDate',
|
||||
header: i18n.t('repo_chart.created_on'),
|
||||
cell: ({ row }) => {
|
||||
const date = new Date(row.getValue('CreatedDate'))
|
||||
return date.toLocaleDateString(i18n.locale.value)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'CreatedDate',
|
||||
header: i18n.t('repo_chart.created_on'),
|
||||
cell: ({ row }) => {
|
||||
const date = new Date(row.getValue('CreatedDate'))
|
||||
return date.toLocaleTimeString(i18n.locale.value)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'TotalHoursSpent',
|
||||
header: i18n.t('repo_chart.hours_spent'),
|
||||
meta: {
|
||||
class: {
|
||||
th: 'text-right',
|
||||
td: 'text-right font-medium'
|
||||
}
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const hours = row.getValue('TotalHoursSpent')
|
||||
return `${hours}h`
|
||||
}
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
|
||||
<h1 class="text-2xl font-bold mb-6 text-black">{{ $t('repo_chart.repository_work_chart') }}</h1>
|
||||
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }}
|
||||
</h1>
|
||||
<div v-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">{{ error }}</div>
|
||||
|
||||
<div v-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
|
||||
{{ $t('repo_chart.loading') }}...
|
||||
<div v-if="loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">{{ $t('repo_chart.loading') }}...
|
||||
</div>
|
||||
|
||||
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
|
||||
<!-- Please log in to view repository work charts. -->
|
||||
{{ $t('repo_chart.login_to_view_charts') }}
|
||||
</div>
|
||||
|
||||
@@ -161,24 +203,22 @@ const quarterItems = computed(() => [
|
||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.repository')
|
||||
}}</label>
|
||||
<USelect v-model="selectedRepo" :items="items" :disabled="loading"
|
||||
:placeholder="$t('repo_chart.select_a_repository')" class="dark:text-white text-black "/>
|
||||
:placeholder="$t('repo_chart.select_a_repository')" class="dark:text-white text-black " />
|
||||
<!-- Select a repository -->
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col min-w-[160px]">
|
||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.year')
|
||||
}}</label>
|
||||
<USelect v-model="selectedYear" :items="yearItems"
|
||||
:disabled="loading || !selectedRepo || years.length === 0"
|
||||
:placeholder="$t('repo_chart.select_a_year')" class="dark:text-white text-black "/>
|
||||
:placeholder="$t('repo_chart.select_a_year')" class="dark:text-white text-black placeholder:text-(--placeholder)" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col min-w-[192px]">
|
||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.quarter')
|
||||
}}</label>
|
||||
<USelect v-model="selectedQuarter" :items="quarterItems"
|
||||
:disabled="loading || !selectedYear || quarters.length === 0"
|
||||
:placeholder="$t('repo_chart.all_quarters')" class="dark:text-white text-black "/>
|
||||
:placeholder="$t('repo_chart.all_quarters')" class="dark:text-white text-black placeholder:text-(--placeholder)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -196,50 +236,19 @@ const quarterItems = computed(() => [
|
||||
<h2 class="text-xl font-medium mb-4 text-black dark:text-white">{{ $t('repo_chart.issues_for') }} {{
|
||||
selectedQuarter }}
|
||||
</h2>
|
||||
|
||||
<table class="w-full border border-(--border-light) dark:border-(--border-dark)">
|
||||
<thead>
|
||||
<tr class="bg-gray-100 dark:bg-(--black)">
|
||||
<th
|
||||
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
|
||||
ID</th>
|
||||
<th
|
||||
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ $t('repo_chart.issue_name') }}</th>
|
||||
<th
|
||||
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ $t('repo_chart.user_initials') }}</th>
|
||||
<th
|
||||
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ $t('repo_chart.created_on') }}</th>
|
||||
<th
|
||||
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ $t('repo_chart.hours_spent') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="issue in issues" :key="issue.IssueID"
|
||||
class=" border-b border-(--border-light) dark:border-(--border-dark)">
|
||||
<td class="p-3 text-black dark:text-white">{{ issue.IssueID }}</td>
|
||||
<td class="p-3 text-black dark:text-white">{{ issue.IssueName }}</td>
|
||||
<td class="p-3 text-black dark:text-white">{{ issue.Initials }}</td>
|
||||
<td class="p-3 text-black dark:text-white">{{ issue.CreatedDate }}</td>
|
||||
<td class="p-3 text-black dark:text-white">{{ issue.TotalHoursSpent }}h</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pt-4 flex justify-center items-center dark:text-white!">
|
||||
<UTable :data="issues" :columns="columns" class="flex-1 dark:text-white! text-dark" />
|
||||
<div class="pt-4 flex justify-center items-center dark:text-white! text-dark">
|
||||
<UPagination v-model:page="page" :total="totalItems" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="selectedQuarter && !loading && authStore.isAuthenticated && !hasIssues"
|
||||
class="mt-4 p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) dark:text-white text-black rounded">
|
||||
class="mt-4 p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) dark:text-white! text-black rounded">
|
||||
{{ $t('validate_error.no_issues_for_quarter') }}.
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loading && authStore.isAuthenticated" class="p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) rounded">
|
||||
<div v-else-if="!loading && authStore.isAuthenticated"
|
||||
class="p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) rounded dark:text-white! text-black">
|
||||
<span v-if="!selectedRepo">{{ $t('repo_chart.select_repo_to_view_data') }}</span>
|
||||
<span v-else-if="!selectedYear">{{ $t('repo_chart.select_year_to_view_data') }}</span>
|
||||
<span v-else-if="!selectedQuarter">{{ $t('repo_chart.select_quarter_to_view_issues') }}</span>
|
||||
|
||||
@@ -4,15 +4,12 @@ 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/i18n'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const validation = useValidation()
|
||||
|
||||
const new_password = ref('')
|
||||
const confirm_new_password = ref('')
|
||||
const showNewPassword = ref(false)
|
||||
@@ -52,13 +49,19 @@ function validate(): FormError[] {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[100vh] flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||
<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">
|
||||
|
||||
<!-- Success State -->
|
||||
<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-500" />
|
||||
<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>
|
||||
@@ -71,7 +74,6 @@ function validate(): FormError[] {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Form State -->
|
||||
<template v-else>
|
||||
<UForm :validate="validate" @submit="handleReset" class="flex flex-col gap-3">
|
||||
<UAlert v-if="authStore.error" color="error" variant="subtle"
|
||||
@@ -79,16 +81,17 @@ function validate(): FormError[] {
|
||||
: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">
|
||||
<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" :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'"
|
||||
:aria-label="showNewPassword ? 'Hide password' : 'Show password'"
|
||||
:aria-pressed="showNewPassword" aria-controls="new_password"
|
||||
@click="showNewPassword = !showNewPassword" />
|
||||
@click="showNewPassword = !showNewPassword" class="mr-2"/>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
@@ -97,27 +100,29 @@ 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" :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'"
|
||||
@click="showConfirmPassword = !showConfirmPassword" />
|
||||
@click="showConfirmPassword = !showConfirmPassword" class="mr-2"/>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" block :loading="authStore.loading" class="text-white! bg-(--color-blue-600) dark:bg-(--color-blue-500)">
|
||||
<UButton type="submit" block :loading="authStore.loading"
|
||||
class="text-white! bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
|
||||
{{ $t('general.reset_password') }}
|
||||
</UButton>
|
||||
|
||||
<div class="text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4">
|
||||
<UButton color="neutral" variant="ghost" @click="goToLogin" class="dark:text-white text-black">
|
||||
<button color="neutral" variant="ghost" @click="goToLogin"
|
||||
class="text-[15px] flex items-center gap-2 text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">
|
||||
<UIcon name="mingcute:arrow-left-line" />
|
||||
{{ $t('general.back_to_sign_in') }}
|
||||
</UButton>
|
||||
</button>
|
||||
</div>
|
||||
</UForm>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,12 +3,12 @@ import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
|
||||
const { t, te } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// Helper function to get translation with fallback
|
||||
function tt(key: string, fallback: string): string {
|
||||
return te(key) ? t(key) : fallback
|
||||
}
|
||||
@@ -20,22 +20,19 @@ const success = ref(false)
|
||||
const verificationInProgress = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
// Get token from URL query params
|
||||
token.value = (route.query.token as string) || ''
|
||||
|
||||
if (!token.value) {
|
||||
error.value = tt('verify_email.invalid_token', 'Invalid or missing verification token')
|
||||
error.value = i18n.t('verify_email.invalid_token')
|
||||
verificationInProgress.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// Automatically verify email on page load
|
||||
handleVerifyEmail()
|
||||
})
|
||||
|
||||
async function handleVerifyEmail() {
|
||||
if (!token.value) {
|
||||
error.value = tt('verify_email.invalid_token', 'Invalid or missing verification token')
|
||||
error.value = i18n.t('verify_email.invalid_token')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -43,7 +40,7 @@ async function handleVerifyEmail() {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
await useFetchJson('/api/v1/auth/complete-registration', {
|
||||
await useFetchJson('/api/v1/public/auth/complete-registration', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: token.value }),
|
||||
@@ -52,12 +49,11 @@ async function handleVerifyEmail() {
|
||||
success.value = true
|
||||
verificationInProgress.value = false
|
||||
|
||||
// Redirect to login after 3 seconds
|
||||
setTimeout(() => {
|
||||
router.push({ name: 'login' })
|
||||
}, 3000)
|
||||
} catch (e: any) {
|
||||
error.value = e?.message ?? tt('verify_email.verification_failed', 'Email verification failed')
|
||||
error.value = e?.message ?? i18n.t('verify_email.verification_failed')
|
||||
verificationInProgress.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -74,81 +70,72 @@ function goToLogin() {
|
||||
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">
|
||||
<!-- Logo/Brand Section -->
|
||||
<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-envelope-check" class="w-8 h-8" />
|
||||
<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>
|
||||
|
||||
<!-- Verify Email Card -->
|
||||
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
|
||||
<template #header>
|
||||
<div class="text-center">
|
||||
<!-- Loading State -->
|
||||
<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">
|
||||
{{ tt('verify_email.verifying', 'Verifying your email...') }}
|
||||
{{ $t('verify_email.verifying') }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Success State -->
|
||||
<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">
|
||||
{{ tt('verify_email.success_title', 'Email Verified!') }}
|
||||
{{ $t('verify_email.success_title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ tt('verify_email.success_message', 'Your email has been verified successfully.') }}
|
||||
{{ $t('verify_email.success_message') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<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">
|
||||
{{ tt('verify_email.error_title', 'Verification Failed') }}
|
||||
{{ $t('verify_email.error_title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ tt('verify_email.error_message', 'We could not verify your email.') }}
|
||||
{{ $t('verify_email.error_message') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Success State Content -->
|
||||
<div v-if="success" class="text-center py-4">
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ tt('verify_email.redirect_message', 'You will be redirected to login page...') }}</p>
|
||||
<UButton color="primary" @click="goToLogin">{{ tt('verify_email.go_to_login', 'Go to Login') }}</UButton>
|
||||
<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>
|
||||
|
||||
<!-- Error State Content -->
|
||||
<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">{{ tt('verify_email.go_to_login', 'Go to Login') }}</UButton>
|
||||
<UButton color="primary" @click="goToLogin" class="cursor-pointer">{{ $t('verify_email.go_to_login') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Loading State Content -->
|
||||
<div v-else-if="verificationInProgress && loading" class="text-center py-4">
|
||||
<p class="text-gray-500 dark:text-gray-400">{{ tt('verify_email.please_wait', 'Please wait while we verify your email address.') }}</p>
|
||||
<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">
|
||||
{{ tt('verify_email.already_registered', 'Already have an account?') }}
|
||||
<UButton variant="link" size="sm" @click="goToLogin"> {{ tt('verify_email.sign_in', 'Sign in') }}
|
||||
</UButton>
|
||||
{{ $t('verify_email.already_registered') }}
|
||||
<button variant="link" size="sm" @click="goToLogin"
|
||||
class="cursor-pointer text-(--color-blue-600) dark:text-(--color-blue-500)"> {{ $t('general.sign_in')
|
||||
}}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user