mirror of
https://github.com/Litlyx/litlyx
synced 2026-02-04 06:32:20 +01:00
refactoring
This commit is contained in:
34
dashboard/pages/accept_invite.vue
Normal file
34
dashboard/pages/accept_invite.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const project_id = route.query.project_id;
|
||||
if (!project_id) throw Error('project_id is required');
|
||||
const res = await $fetch('/api/project/members/accept', {
|
||||
headers: useComputedHeaders({
|
||||
custom: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).value,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ project_id })
|
||||
});
|
||||
router.push('/');
|
||||
} catch (ex) {
|
||||
console.error('ERROR');
|
||||
console.error(ex);
|
||||
alert('An error occurred');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
You will be redirected soon.
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,165 +5,8 @@ import type { CItem } from '~/components/CustomTab.vue';
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
|
||||
const filterPremium = ref<boolean>(false);
|
||||
const filterAppsumo = ref<boolean>(false);
|
||||
|
||||
|
||||
|
||||
const timeRange = ref<number>(9);
|
||||
|
||||
function setTimeRange(n: number) {
|
||||
timeRange.value = n;
|
||||
}
|
||||
|
||||
const timeRangeTimestamp = computed(() => {
|
||||
if (timeRange.value == 1) return Date.now() - 1000 * 60 * 60 * 24;
|
||||
if (timeRange.value == 2) return Date.now() - 1000 * 60 * 60 * 24 * 7;
|
||||
if (timeRange.value == 3) return Date.now() - 1000 * 60 * 60 * 24 * 30;
|
||||
return 0;
|
||||
})
|
||||
|
||||
|
||||
// const { data: projectsAggregatedResponseData } = await useFetch<AdminProjectsList[]>('/api/admin/projects', signHeaders());
|
||||
// const { data: counts } = await useFetch(() => `/api/admin/counts?from=${timeRangeTimestamp.value}`, signHeaders());
|
||||
|
||||
|
||||
|
||||
// function onHideClicked() {
|
||||
// isAdminHidden.value = true;
|
||||
// }
|
||||
|
||||
|
||||
// function isAppsumoType(type: number) {
|
||||
// return type > 6000 && type < 6004
|
||||
// }
|
||||
|
||||
// const projectsAggregated = computed(() => {
|
||||
|
||||
// let pool = projectsAggregatedResponseData.value ? [...projectsAggregatedResponseData.value] : [];
|
||||
|
||||
// let shownPool: AdminProjectsList[] = [];
|
||||
|
||||
|
||||
// for (const element of pool) {
|
||||
|
||||
// shownPool.push({ ...element, projects: [...element.projects] });
|
||||
|
||||
// if (filterAppsumo.value === true) {
|
||||
// shownPool.forEach(e => {
|
||||
// e.projects = e.projects.filter(project => {
|
||||
// return isAppsumoType(project.premium_type)
|
||||
// })
|
||||
// })
|
||||
|
||||
// shownPool = shownPool.filter(e => {
|
||||
// return e.projects.length > 0;
|
||||
// })
|
||||
|
||||
// } else if (filterPremium.value === true) {
|
||||
// shownPool.forEach(e => {
|
||||
// e.projects = e.projects.filter(project => {
|
||||
// return project.premium === true;
|
||||
// })
|
||||
// })
|
||||
|
||||
// shownPool = shownPool.filter(e => {
|
||||
// return e.projects.length > 0;
|
||||
// })
|
||||
|
||||
// } else {
|
||||
// console.log('NO DATA')
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
// return shownPool.sort((a, b) => {
|
||||
// const sumVisitsA = a.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0) + (pe.counts?.events || 0), 0);
|
||||
// const sumVisitsB = b.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0) + (pe.counts?.events || 0), 0);
|
||||
// return sumVisitsB - sumVisitsA;
|
||||
// }).filter(e => {
|
||||
// return new Date(e.created_at).getTime() >= timeRangeTimestamp.value
|
||||
// });
|
||||
// })
|
||||
|
||||
// const premiumCount = computed(() => {
|
||||
// let premiums = 0;
|
||||
// projectsAggregated.value?.forEach(e => {
|
||||
// e.projects.forEach(p => {
|
||||
// if (p.premium) premiums++;
|
||||
// });
|
||||
|
||||
// })
|
||||
// return premiums;
|
||||
// })
|
||||
|
||||
|
||||
// const activeProjects = computed(() => {
|
||||
// let actives = 0;
|
||||
|
||||
// projectsAggregated.value?.forEach(e => {
|
||||
// e.projects.forEach(p => {
|
||||
// if (!p.counts) return;
|
||||
// if (!p.counts.updated_at) return;
|
||||
// const updated_at = new Date(p.counts.updated_at).getTime();
|
||||
// if (updated_at < Date.now() - 1000 * 60 * 60 * 24) return;
|
||||
// actives++;
|
||||
// });
|
||||
// })
|
||||
// return actives;
|
||||
// });
|
||||
|
||||
|
||||
|
||||
// const totalVisits = computed(() => {
|
||||
// return projectsAggregated.value?.reduce((a, e) => {
|
||||
// return a + e.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0), 0);
|
||||
// }, 0) || 0;
|
||||
// });
|
||||
|
||||
// const totalEvents = computed(() => {
|
||||
// return projectsAggregated.value?.reduce((a, e) => {
|
||||
// return a + e.projects.reduce((pa, pe) => pa + (pe.counts?.events || 0), 0);
|
||||
// }, 0) || 0;
|
||||
// });
|
||||
|
||||
|
||||
|
||||
const details = ref<any>();
|
||||
const showDetails = ref<boolean>(false);
|
||||
async function getProjectDetails(project_id: string) {
|
||||
details.value = await $fetch(`/api/admin/details?project_id=${project_id}`, signHeaders());
|
||||
showDetails.value = true;
|
||||
}
|
||||
|
||||
async function resetCount(project_id: string) {
|
||||
await $fetch(`/api/admin/reset_count?project_id=${project_id}`, signHeaders());
|
||||
}
|
||||
|
||||
|
||||
function dateDiffDays(a: string) {
|
||||
return (Date.now() - new Date(a).getTime()) / (1000 * 60 * 60 * 24)
|
||||
}
|
||||
|
||||
function getLogBg(last_logged_at?: string) {
|
||||
|
||||
const day = 1000 * 60 * 60 * 24;
|
||||
const week = 1000 * 60 * 60 * 24 * 7;
|
||||
|
||||
const lastLoggedAtDate = new Date(last_logged_at || 0);
|
||||
|
||||
if (lastLoggedAtDate.getTime() > Date.now() - day) {
|
||||
return 'bg-green-500'
|
||||
} else if (lastLoggedAtDate.getTime() > Date.now() - week) {
|
||||
return 'bg-yellow-500'
|
||||
} else {
|
||||
return 'bg-red-500'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const tabs: CItem[] = [
|
||||
{ label: 'Overview', slot: 'overview' },
|
||||
|
||||
@@ -6,6 +6,8 @@ definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const selfhosted = useSelfhosted();
|
||||
|
||||
const { permission, canSeeAi } = usePermission();
|
||||
|
||||
const debugModeAi = ref<boolean>(false);
|
||||
|
||||
const { userRoles } = useLoggedUser();
|
||||
@@ -253,7 +255,12 @@ async function clearAllChats() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full overflow-y-hidden">
|
||||
|
||||
<div v-if="!canSeeAi" class="h-full w-full flex mt-[20vh] justify-center">
|
||||
<div> You need AI permission to view this page </div>
|
||||
</div>
|
||||
|
||||
<div v-if="canSeeAi" class="w-full h-full overflow-y-hidden">
|
||||
|
||||
<div class="flex flex-row h-full overflow-y-hidden">
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import DateService, { type Slice } from '@services/DateService';
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const { permission, canSeeEvents } = usePermission();
|
||||
|
||||
const { snapshotDuration } = useSnapshot();
|
||||
|
||||
@@ -30,7 +31,12 @@ const eventsData = await useFetch(`/api/data/count`, {
|
||||
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
|
||||
|
||||
<div v-if="!canSeeEvents" class="h-full w-full flex mt-[20vh] justify-center">
|
||||
<div> You need events permission to view this page </div>
|
||||
</div>
|
||||
|
||||
<div v-if="canSeeEvents" class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
|
||||
|
||||
|
||||
<LyxUiCard class="w-full flex justify-between items-center lg:flex-row flex-col gap-6 lg:gap-0">
|
||||
|
||||
@@ -11,6 +11,9 @@ const jwtLogin = computed(() => route.query.jwt_login as string);
|
||||
|
||||
const { token, setToken } = useAccessToken();
|
||||
|
||||
const { refreshingDomains } = useDomain();
|
||||
const { permission, canSeeWeb, canSeeEvents } = usePermission();
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
if (jwtLogin.value) {
|
||||
@@ -36,27 +39,39 @@ const selfhosted = useSelfhosted();
|
||||
|
||||
<template>
|
||||
|
||||
<div class="dashboard w-full h-full overflow-y-auto overflow-x-hidden pb-[7rem] md:pt-4 lg:pt-0">
|
||||
<div v-if="!canSeeWeb" class="h-full w-full flex mt-[20vh] justify-center">
|
||||
<div> You need webAnalytics permission to view this page </div>
|
||||
</div>
|
||||
|
||||
<div v-if="canSeeWeb && refreshingDomains">
|
||||
<div class="w-full flex justify-center items-center mt-[20vh]">
|
||||
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canSeeWeb && !refreshingDomains" class="dashboard w-full h-full overflow-y-auto overflow-x-hidden pb-[7rem] md:pt-4 lg:pt-0">
|
||||
|
||||
<div v-if="showDashboard">
|
||||
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||
|
||||
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
|
||||
<!-- <BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer> -->
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DashboardTopSection :key="refreshKey"></DashboardTopSection>
|
||||
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
||||
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
||||
</div>
|
||||
<DashboardActionableChart v-if="canSeeWeb && canSeeEvents" :key="refreshKey"></DashboardActionableChart>
|
||||
<LyxUiCard v-else class="flex justify-center w-full py-4">
|
||||
You need events permission to view this widget
|
||||
</LyxUiCard>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex w-full justify-center mt-6 px-6">
|
||||
<div class="flex w-full justify-center mt-6 px-6">
|
||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||
<div class="flex-1">
|
||||
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
|
||||
@@ -66,7 +81,7 @@ const selfhosted = useSelfhosted();
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex w-full justify-center mt-6 px-6">
|
||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||
<div class="flex-1">
|
||||
@@ -76,9 +91,9 @@ const selfhosted = useSelfhosted();
|
||||
<BarCardDevices :key="refreshKey"></BarCardDevices>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="flex w-full justify-center mt-6 px-6">
|
||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||
<div class="flex-1">
|
||||
@@ -88,11 +103,10 @@ const selfhosted = useSelfhosted();
|
||||
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<FirstInteraction v-if="!justLogged" :refresh-interaction="firstInteraction.refresh"
|
||||
:first-interaction="(firstInteraction.data.value || false)"></FirstInteraction>
|
||||
|
||||
|
||||
186
dashboard/pages/members.vue
Normal file
186
dashboard/pages/members.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogPermissionManager } from '#components';
|
||||
import type { TPermission } from '~/shared/schema/TeamMemberSchema';
|
||||
|
||||
const { projectId, isGuest } = useProject();
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const columns = [
|
||||
{ key: 'me', label: '' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'permission', label: 'Permission' },
|
||||
{ key: 'pending', label: 'Status' },
|
||||
{ key: 'action', label: 'Actions' },
|
||||
]
|
||||
|
||||
const { data: members, refresh: refreshMembers } = useFetch('/api/project/members/list', {
|
||||
headers: useComputedHeaders({ useSnapshotDates: false })
|
||||
});
|
||||
|
||||
const showAddMember = ref<boolean>(false);
|
||||
|
||||
const addMemberEmail = ref<string>("");
|
||||
|
||||
async function kickMember(email: string) {
|
||||
const sure = confirm('Are you sure to kick ' + email + ' ?');
|
||||
if (!sure) return;
|
||||
try {
|
||||
await $fetch('/api/project/members/kick', {
|
||||
method: 'POST',
|
||||
...signHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
'x-pid': projectId.value ?? ''
|
||||
}),
|
||||
body: JSON.stringify({ email }),
|
||||
onResponseError({ request, response, options }) {
|
||||
alert(response.statusText);
|
||||
}
|
||||
});
|
||||
|
||||
refreshMembers();
|
||||
} catch (ex: any) { }
|
||||
}
|
||||
|
||||
async function addMember() {
|
||||
|
||||
if (addMemberEmail.value.length === 0) return;
|
||||
|
||||
try {
|
||||
|
||||
showAddMember.value = false;
|
||||
|
||||
await $fetch('/api/project/members/add', {
|
||||
method: 'POST',
|
||||
...signHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
'x-pid': projectId.value ?? ''
|
||||
}),
|
||||
body: JSON.stringify({ email: addMemberEmail.value }),
|
||||
onResponseError({ request, response, options }) {
|
||||
alert(response.statusText);
|
||||
}
|
||||
});
|
||||
|
||||
addMemberEmail.value = '';
|
||||
|
||||
refreshMembers();
|
||||
|
||||
} catch (ex: any) { }
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
const modal = useModal();
|
||||
|
||||
function openPermissionManagerDialog(member_id: string) {
|
||||
modal.open(DialogPermissionManager, {
|
||||
preventClose: true,
|
||||
member_id,
|
||||
onSuccess: () => {
|
||||
modal.close();
|
||||
refreshMembers();
|
||||
},
|
||||
onCancel: () => {
|
||||
modal.close();
|
||||
refreshMembers();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function permissionToString(permission: TPermission) {
|
||||
const result: string[] = [];
|
||||
if (permission.webAnalytics) result.push('w');
|
||||
if (permission.events) result.push('e');
|
||||
if (permission.ai) result.push('a');
|
||||
if (permission.domains.includes('All domains')) {
|
||||
result.push('+');
|
||||
} else {
|
||||
result.push(permission.domains.length.toString());
|
||||
}
|
||||
return result.join('');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 pt-10">
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="flex gap-4 items-center">
|
||||
<LyxUiInput class="px-4 py-1 w-full" placeholder="Add a new member" v-model="addMemberEmail">
|
||||
</LyxUiInput>
|
||||
<LyxUiButton @click="addMember" type="secondary"> Add </LyxUiButton>
|
||||
</div>
|
||||
<div class="poppins text-[.8rem] mt-2 text-lyx-text-darker">
|
||||
User should have been registered to Litlyx
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<UTable :rows="members || []" :columns="columns">
|
||||
|
||||
<template #me-data="e">
|
||||
<i v-if="e.row.me" class="far fa-user text-lyx-lightmode-text dark:text-lyx-text"></i>
|
||||
<i v-if="!e.row.me"></i>
|
||||
</template>
|
||||
|
||||
<template #email-data="e">
|
||||
<div class="text-lyx-lightmode-text dark:text-lyx-text">
|
||||
{{ e.row.email }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #pending-data="e">
|
||||
<div class="text-lyx-lightmode-text dark:text-lyx-text">
|
||||
{{ e.row.pending ? 'Pending' : 'Ok' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #permission-data="e">
|
||||
<div class="text-lyx-lightmode-text dark:text-lyx-text flex gap-2">
|
||||
<div v-if="e.row.role !== 'OWNER' && !isGuest">
|
||||
<LyxUiButton class="!px-2" type="secondary"
|
||||
@click="openPermissionManagerDialog(e.row.id.toString())">
|
||||
<i class="far fa-gear"></i>
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<UBadge variant="outline" size="sm" v-if="e.row.permission.webAnalytics"
|
||||
label="Analytics"> </UBadge>
|
||||
<UBadge variant="outline" size="sm" v-if="e.row.permission.events" label="Events">
|
||||
</UBadge>
|
||||
<UBadge variant="outline" size="sm" v-if="e.row.permission.ai" label="AI"> </UBadge>
|
||||
<UBadge variant="outline" color="blue" size="sm"
|
||||
v-if="e.row.permission.domains.includes('All domains')" label="All domains">
|
||||
</UBadge>
|
||||
|
||||
<UBadge variant="outline" size="sm" color="blue"
|
||||
v-if="!e.row.permission.domains.includes('All domains')"
|
||||
v-for="domain of e.row.permission.domains" :label="domain"> </UBadge>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<template #action-data="e" v-if="!isGuest">
|
||||
<div @click="kickMember(e.row.email)" v-if="e.row.role != 'OWNER'"
|
||||
class="text-red-500 hover:bg-black/20 cursor-pointer outline outline-[1px] outline-red-500 px-3 py-1 rounded-lg text-center">
|
||||
Kick
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</UTable>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,7 +7,6 @@ const selfhosted = useSelfhosted();
|
||||
const items = [
|
||||
{ label: 'General', slot: 'general', tab: 'general' },
|
||||
{ label: 'Domains', slot: 'domains', tab: 'domains' },
|
||||
{ label: 'Members', slot: 'members', tab: 'members' },
|
||||
{ label: 'Billing', slot: 'billing', tab: 'billing' },
|
||||
{ label: 'Codes', slot: 'codes', tab: 'codes' },
|
||||
{ label: 'Account', slot: 'account', tab: 'account' }
|
||||
@@ -27,9 +26,6 @@ const items = [
|
||||
<template #domains>
|
||||
<SettingsData :key="refreshKey"></SettingsData>
|
||||
</template>
|
||||
<template #members>
|
||||
<SettingsMembers :key="refreshKey"></SettingsMembers>
|
||||
</template>
|
||||
<template #billing>
|
||||
<SettingsBilling v-if="!selfhosted" :key="refreshKey"></SettingsBilling>
|
||||
<div class="flex popping text-[1.2rem] font-semibold justify-center mt-[20vh] text-lyx-lightmode-text dark:text-lyx-text"
|
||||
|
||||
15
dashboard/pages/test_links.vue
Normal file
15
dashboard/pages/test_links.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { data: links } = useFetch('/api/project/links/list', {
|
||||
headers: useComputedHeaders()
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="link of links">
|
||||
{{ link }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user