add dashboard

This commit is contained in:
Litlyx
2024-06-01 15:27:40 +02:00
parent 75f0787c3b
commit df4faf366f
201 changed files with 91267 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
<script setup lang="ts">
import type { AdminProjectsList } from '~/server/api/admin/projects';
definePageMeta({ layout: 'dashboard' });
const { data: projects } = await useFetch<AdminProjectsList[]>('/api/admin/projects', signHeaders());
type TProjectsGrouped = {
user: {
name: string,
email: string,
given_name: string,
picture: string,
created_at: Date
},
projects: {
premium: boolean,
created_at: Date,
project_name: string,
total_visits: number,
total_events: number,
}[]
}
const projectsGrouped = computed(() => {
if (!projects.value) return [];
const result: TProjectsGrouped[] = [];
for (const project of projects.value) {
if (!project.user) continue;
const target = result.find(e => e.user.email == project.user.email);
if (target) {
target.projects.push({
created_at: project.created_at,
premium: project.premium,
project_name: project.project_name,
total_events: project.total_events,
total_visits: project.total_visits
});
} else {
const item: TProjectsGrouped = {
user: project.user,
projects: [{
created_at: project.created_at,
premium: project.premium,
project_name: project.project_name,
total_events: project.total_events,
total_visits: project.total_visits
}]
}
result.push(item);
}
}
return result;
});
function onHideClicked() {
isAdminHidden.value = true;
}
</script>
<template>
<div class="bg-bg overflow-y-auto w-full h-dvh p-6 gap-6 flex flex-col">
<div @click="onHideClicked()" v-if="!isAdminHidden"
class="bg-menu hover:bg-menu/70 cursor-pointer flex gap-2 rounded-lg w-fit px-6 py-4 text-text-sub">
<div class="text-text-sub/90"> <i class="far fa-eye"></i> </div>
<div> Nascondi dalla barra </div>
</div>
<div v-for="item of projectsGrouped" class="bg-menu p-4 rounded-xl flex flex-col gap-2 w-full relative">
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-1">
<div> {{ item.user.email }} </div>
<div> {{ item.user.name }} </div>
</div>
<div class="flex justify-evenly flex-col lg:flex-row gap-2 lg:gap-0">
<div v-for="project of item.projects"
class="lg:w-[30%] flex flex-col items-center bg-bg p-6 rounded-xl">
<div class="flex gap-4">
<div class="font-bold"> {{ project.premium ? 'PREMIUM' : 'FREE' }} </div>
<div class="text-text-sub/90">
{{ new Date(project.created_at).toLocaleDateString('it-IT') }}
</div>
</div>
<div class="text-ellipsis line-clamp-1"> {{ project.project_name }} </div>
<div class="flex gap-2">
<div> Visits: </div>
<div> {{ project.total_visits }} </div>
<div> Events: </div>
<div> {{ project.total_events }} </div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>

205
dashboard/pages/analyst.vue Normal file
View File

@@ -0,0 +1,205 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject();
const { data: chatsList, refresh: reloadChatsList } = useFetch(`/api/ai/${activeProject.value?._id}/chats_list`, signHeaders());
const { data: chatsRemaining, refresh: reloadChatsRemaining } = useFetch(`/api/ai/${activeProject.value?._id}/chats_remaining`, signHeaders());
const currentText = ref<string>("");
const loading = ref<boolean>(false);
const currentChatId = ref<string>("");
const currentChatMessages = ref<any[]>([]);
const scroller = ref<HTMLDivElement | null>(null);
async function sendMessage() {
if (loading.value) return;
if (!activeProject.value) return;
loading.value = true;
const body: any = { text: currentText.value }
if (currentChatId.value) body.chat_id = currentChatId.value
currentChatMessages.value.push({ role: 'user', content: currentText.value });
setTimeout(() => scrollToBottom(), 1);
currentText.value = '';
try {
const res = await $fetch(`/api/ai/${activeProject.value._id.toString()}/send_message`, {
method: 'POST',
body: JSON.stringify(body),
...signHeaders({ 'Content-Type': 'application/json' })
});
currentChatMessages.value.push({ role: 'assistant', content: res });
await reloadChatsRemaining();
await reloadChatsList();
currentChatId.value = chatsList.value?.at(-1)?._id.toString() || '';
} catch (ex: any) {
if (ex.message.includes('CHAT_LIMIT_REACHED')) {
currentChatMessages.value.push({ role: 'assistant', content: 'You have reached your free tier chat limit.\n Upgrade to an higher tier.' });
}
}
setTimeout(() => scrollToBottom(), 1);
loading.value = false;
}
async function openChat(chat_id?: string) {
if (!activeProject.value) return;
if (!chat_id) {
currentChatMessages.value = [];
currentChatId.value = '';
return;
}
currentChatId.value = chat_id;
const messages = await $fetch(`/api/ai/${activeProject.value._id}/${chat_id}/get_messages`, signHeaders());
if (!messages) return;
currentChatMessages.value = messages;
setTimeout(() => scrollToBottom(), 1);
}
function scrollToBottom() {
if (!scroller.value) return;
scroller.value.scrollTo({ behavior: 'smooth', top: 999999 })
}
function parseMessageContent(content: string) {
return content.replace(/\*\*(.*?)\*\*/g, '<b class="text-text">$1</b>');
}
</script>
<template>
<div class="w-full h-full">
<div class="flex flex-row-reverse h-full">
<div class="flex-[2] bg-[#303030] p-6 flex flex-col gap-4">
<div class="gap-2 flex flex-col">
<div class="poppins font-semibold text-[1.5rem]">
Lit, your AI Analyst is here!
</div>
<div class="poppins text-text/75">
Ask anything you want on your analytics,
and understand more Trends and Key Points to take Strategic moves!
</div>
</div>
<div class="flex gap-2 items-center py-3">
<div class="bg-accent w-5 h-5 rounded-full animate-pulse">
</div>
<div class="manrope font-semibold"> {{ chatsRemaining }} remaining messages </div>
</div>
<div>
<div class="poppins font-semibold text-[1.1rem]"> History: </div>
<div class="flex flex-col w-full mt-4 gap-2">
<div @click="openChat()"
class="bg-menu px-4 py-3 cursor-pointer hover:bg-menu/80 poppins rounded-lg mb-8 flex gap-2 items-center">
<div> <i class="fas fa-plus"></i> </div>
<div> New chat </div>
</div>
<div @click="openChat(chat._id.toString())" v-for="chat of chatsList"
class="bg-menu px-4 py-3 cursor-pointer hover:bg-menu/80 poppins rounded-lg"
:class="{ '!bg-accent/60': chat._id.toString() === currentChatId }">
{{ chat.title }}
</div>
</div>
</div>
</div>
<div class="flex-[5] py-8 flex flex-col items-center relative">
<div class="flex flex-col items-center mt-[20vh] px-28" v-if="currentChatMessages.length == 0">
<div class="w-[10rem]">
<img :src="'analyst.png'" class="w-full h-full">
</div>
<div class="poppins text-[1.2rem]">
How can i help you today?
</div>
<div class="grid grid-cols-2 gap-4 mt-6">
<div class="bg-[#2f2f2f] p-4 rounded-lg poppins">
How many visits i got last week ?
</div>
<div class="bg-[#2f2f2f] p-4 rounded-lg poppins">
How many visits i got last week ?
</div>
<div class="bg-[#2f2f2f] p-4 rounded-lg poppins">
How many visits i got last week ?
</div>
<div class="bg-[#2f2f2f] p-4 rounded-lg poppins">
How many visits i got last week ?
</div>
</div>
</div>
<div ref="scroller" class="flex flex-col w-full gap-6 px-28 overflow-y-auto pb-20">
<div class="flex w-full" v-for="message of currentChatMessages">
<div class="flex justify-end w-full poppins text-[1.1rem]" v-if="message.role === 'user'">
<div class="bg-[#303030] px-5 py-3 rounded-lg">
{{ message.content }}
</div>
</div>
<div class="flex items-center gap-3 justify-start w-full poppins text-[1.1rem]"
v-if="message.role === 'assistant'">
<div class="flex items-center justify-center shrink-0">
<img class="h-[3.5rem] w-auto" :src="'analyst.png'">
</div>
<div v-html="parseMessageContent(message.content)"
class="max-w-[70%] text-text/90 whitespace-pre-wrap">
</div>
</div>
</div>
<div v-if="loading"
class="flex items-center mt-10 gap-3 justify-center w-full poppins text-[1.1rem]">
<div class="flex items-center justify-center">
<img class="animate-bounce h-[3.5rem] w-auto" :src="'analyst.png'">
</div>
<div class="poppins "> Loading </div>
</div>
</div>
<div class="flex gap-2 items-center absolute bottom-8 left-0 w-full px-28">
<input v-model="currentText" class="bg-[#303030] w-full focus:outline-none px-4 py-2 rounded-lg"
type="text">
<div @click="sendMessage()"
class="bg-[#303030] hover:bg-[#464646] cursor-pointer px-4 py-2 rounded-full">
<i class="far fa-arrow-up"></i>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,29 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
const iframecal = ref<any | undefined>(null);
const loaded = ref<boolean>(false);
onMounted(() => {
loaded.value = false;
if (!iframecal.value) return;
iframecal.value.onload = () => {
loaded.value = true;
}
});
</script>
<template>
<div class="w-full h-full bg-[#101010]">
<iframe v-show="loaded" ref="iframecal" class="w-full h-full" src="https://cal.com/litlyx/30min">
</iframe>
<div class="w-full h-full flex gap-2 justify-center items-center poppins font-bold text-[1.6rem]" v-if="!loaded">
<div class="animate-pulse"> Loading... </div>
</div>
</div>
</template>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import type { MetricsCounts } from '~/server/api/metrics/[project_id]/counts';
definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject();
const metricsInfo = ref<number>(0);
const columns = [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'metadata', label: 'Metadata' },
{ key: 'created_at', label: 'Creation', sortable: true }
]
const sort = ref({
column: 'created_at',
direction: 'desc'
})
const selectedColumns = ref([...columns]);
const page = ref<number>(1);
const itemsPerPage = 50;
const totalItems = computed(() => metricsInfo.value);
const { data: tableData, pending: loadingData } = await useLazyFetch<any[]>(() =>
`/api/metrics/${activeProject.value?._id}/query?type=1&orderBy=${sort.value.column}&order=${sort.value.direction}&page=${page.value}&limit=${itemsPerPage}`, {
...signHeaders(),
})
onMounted(async () => {
const counts = await $fetch<MetricsCounts>(`/api/metrics/${activeProject.value?._id}/counts`, signHeaders());
metricsInfo.value = counts.eventsCount;
});
</script>
<template>
<div class="w-full h-dvh flex flex-col">
<div class="flex justify-end px-12 py-3">
<div
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
Download CSV
</div>
</div>
<UTable v-if="tableData" class="utable px-8" :ui="{
wrapper: 'overflow-auto w-full h-full',
thead: 'sticky top-0 bg-menu',
td: {
color: 'text-[#ffffffb3]',
base: 'border-r border-l border-gray-300/20'
},
th: { color: 'text-text-sub' },
tbody: 'divide-y divide-gray-300/20',
divide: '',
}" v-model:sort="sort" :columns="selectedColumns" :rows="tableData" :loading="loadingData" sort-mode="manual">
<template #metadata-data="{ row }">
<div v-if="row.metadata" class="flex flex-col gap-1">
<div v-for="(value, key) in row.metadata">
<span class="font-bold">{{ key }}</span>: {{ value }}
</div>
</div>
</template>
<template #created_at-data="{ row }">
<span> {{ new Date(row.created_at).toLocaleString() }} </span>
</template>
</UTable>
<div class="flex justify-end px-3 py-3 border-t border-gray-300/30 dark:border-gray-700">
<UPagination v-model="page" :page-count="itemsPerPage" :total="totalItems" />
</div>
</div>
</template>
<style scoped lang="scss">
.card {
@apply border text-2xl text-text bg-transparent border-gray-500 border-[1] w-fit px-8 h-[7rem] flex flex-col items-center justify-center gap-1 rounded-xl;
}
</style>

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject();
const router = useRouter();
const newProjectName = ref<string>(activeProject.value?.name || "");
async function deleteProject(projectId: string, projectName: string) {
const sure = confirm(`Are you sure to delete the project ${projectName} ?`);
if (!sure) return;
try {
await $fetch('/api/project/delete', {
method: 'DELETE',
...signHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ project_id: projectId })
});
// await refresh();
// setActiveProject(0);
router.push('/')
} catch (ex: any) {
alert(ex.message);
}
}
onMounted(() => {
});
</script>
<template>
<div class="settings w-full">
<div class="flex flex-col justify-center mt-16 gap-10 px-10">
<div class="text-text font-bold text-[1.5rem]"> Settings </div>
<div class="flex gap-4 items-center text-[1.2rem] text-text-sub">
<div class="font-semibold"> Name: </div>
<div>
<input v-model="newProjectName" type="text" class="px-4 py-1 rounded-lg">
</div>
</div>
<div class="flex mt-10">
<div @click="deleteProject(((activeProject?._id || '') as string), (activeProject?.name || ''))"
class="bg-[#bd4747] hover:bg-[#c94b4b] rounded-lg px-6 py-2 text-white text-[.9rem] font-semibold inter cursor-pointer">
Delete
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
input:focus {
outline: none;
}
</style>

View File

@@ -0,0 +1,107 @@
<script setup lang="ts">
import type { MetricsCounts } from '~/server/api/metrics/[project_id]/counts';
definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject();
const metricsInfo = ref<number>(0);
const columns = [
{ key: 'website', label: 'Website', sortable: true },
{ key: 'page', label: 'Page', sortable: true },
{ key: 'referrer', label: 'Referrer', sortable: true },
{ key: 'session', label: 'Session', sortable: true },
{ key: 'browser', label: 'Browser', sortable: true },
{ key: 'os', label: 'OS', sortable: true },
{ key: 'screen', label: 'Screen', sortable: true },
{ key: 'created_at', label: 'Date', sortable: true }
]
const sort = ref({
column: 'created_at',
direction: 'desc'
})
const selectedColumns = ref([...columns]);
const page = ref<number>(1);
const itemsPerPage = 50;
const totalItems = computed(() => metricsInfo.value);
const { data: tableData, pending: loadingData } = await useLazyFetch<any[]>(() =>
`/api/metrics/${activeProject.value?._id}/query?type=0&orderBy=${sort.value.column}&order=${sort.value.direction}&page=${page.value}&limit=${itemsPerPage}`, {
...signHeaders(),
})
onMounted(async () => {
const counts = await $fetch<MetricsCounts>(`/api/metrics/${activeProject.value?._id}/counts`, signHeaders());
metricsInfo.value = counts.visitsCount;
});
</script>
<template>
<div class="w-full h-dvh flex flex-col">
<div class="flex justify-end px-12 py-3">
<div
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
Download CSV
</div>
</div>
<UTable v-if="tableData" class="utable px-8" :ui="{
wrapper: 'overflow-auto w-full h-full',
thead: 'sticky top-0 bg-menu',
td: {
color: 'text-[#ffffffb3]',
base: 'border-r border-l border-gray-300/20'
},
th: { color: 'text-text-sub' },
tbody: 'divide-y divide-gray-300/20',
divide: '',
}" v-model:sort="sort" :columns="selectedColumns" :rows="tableData" :loading="loadingData" sort-mode="manual"
:sortButton="{ color: '#000000' }">
<template #metadata-data="{ row }">
<div v-if="row.metadata" class="flex flex-col gap-1">
<div v-for="(value, key) in JSON.parse(row.metadata)">
<span class="font-bold">{{ key }}</span>: {{ value }}
</div>
</div>
</template>
<template #screen-data="{ row }">
<span> {{ row.screenWidth }}x{{ row.screenHeight }} </span>
</template>
<template #userAgent-data="{ row }">
<span> {{ row.userAgent }} </span>
</template>
<template #created_at-data="{ row }">
<span> {{ new Date(row.created_at).toLocaleString() }} </span>
</template>
</UTable>
<div class="flex justify-end px-3 py-3 border-t border-gray-300/30 dark:border-gray-700">
<UPagination v-model="page" :page-count="itemsPerPage" :total="totalItems" />
</div>
</div>
</template>
<style scoped lang="scss">
.card {
@apply border text-2xl text-text bg-transparent border-gray-500 border-[1] w-fit px-8 h-[7rem] flex flex-col items-center justify-center gap-1 rounded-xl;
}
</style>

102
dashboard/pages/events.vue Normal file
View File

@@ -0,0 +1,102 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
const selectLabels = [
{ label: 'Day', value: 'day' },
{ label: 'Month', value: 'month' },
];
const activeProject = useActiveProject();
// const { data: names } = useFetch(`/api/metrics/${activeProject.value?._id.toString()}/events/names`, signHeaders());
const eventsStackedSelectIndex = ref<number>(0);
const text = ref<string>("");
const response = ref<string>("");
const loading = ref<boolean>(false);
async function ask() {
if (loading.value) return;
if (!activeProject.value) return;
loading.value = true;
response.value = '';
const res = await $fetch(`/api/ai/${activeProject.value._id.toString()}/ask`, {
method: 'POST',
body: JSON.stringify({ text: text.value }),
...signHeaders({ 'Content-Type': 'application/json' })
});
text.value = '';
loading.value = false;
response.value = res || 'NO_RESPONSE';
}
const { isAdmin } = useUserRoles();
</script>
<template>
<div class="w-full h-full p-6">
<CardTitled class="p-4 flex-1" title="Events" sub="Events stacked bar chart.">
<template #header>
<SelectButton @changeIndex="eventsStackedSelectIndex = $event" :currentIndex="eventsStackedSelectIndex"
:options="selectLabels">
</SelectButton>
</template>
<div>
<EventsStackedBarChart :slice="(selectLabels[eventsStackedSelectIndex].value as any)">
</EventsStackedBarChart>
</div>
</CardTitled>
<div class="p-4 text-[1.3rem] flex flex-col gap-4" v-if="isAdmin">
<div class="flex gap-8">
<input class="w-full p-4 px-8 poppins rounded-full" type="text" v-model="text">
<div class="bg-menu py-2 px-10 flex items-center rounded-lg cursor-pointer hover:bg-menu/80"
@click="ask()">
{{ loading ? 'Loading' : 'Send' }}
</div>
</div>
<div v-if="response">
{{ response }}
</div>
</div>
<!--
<div>
<br>
<br>
<div> Event names:</div>
<br>
<div v-for="name of names">
{{ name }}
</div>
</div> -->
<!-- <div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1">
<div class="bg-menu p-6 rounded-xl flex flex-col gap-6">
<div class="poppins font-semibold text-[1.1rem]">
Manage your events
</div>
<div class="w-full">
<DashboardEventsColorManager></DashboardEventsColorManager>
</div>
</div>
</div>
<div class="flex-1">
<DashboardEventsBarCard></DashboardEventsBarCard>
</div>
</div> -->
</div>
</template>

236
dashboard/pages/index.vue Normal file
View File

@@ -0,0 +1,236 @@
<script setup lang="ts">
definePageMeta({ layout: 'dashboard' });
const { data: projects } = useProjectsList();
const activeProject = useActiveProject();
const mainChartSelectIndex = ref<number>(1);
const sessionsChartSelectIndex = ref<number>(1);
const eventsStackedSelectIndex = ref<number>(0);
const route = useRoute();
onMounted(async () => {
if (route.query.just_logged) {
return location.href = '/';
}
});
function copyProjectId() {
if (!navigator.clipboard) alert('NON PUOI COPIARE IN HTTP');
navigator.clipboard.writeText(activeProject.value?._id?.toString() || '');
alert('Copiato !');
}
function copyScript() {
if (!navigator.clipboard) alert('NON PUOI COPIARE IN HTTP');
const createScriptText = () => {
return [
'<script defer ',
`data-project="${activeProject.value?._id}" `,
'src="https://cdn.jsdelivr.net/npm/litlyx/browser/litlyx.js"></',
'script>'
].join('')
}
navigator.clipboard.writeText(createScriptText());
alert('Copiato !');
}
const { data: firstInteraction, pending, refresh } = useFirstInteractionData();
watch(pending, () => {
if (pending.value === true) return;
if (firstInteraction.value === false) {
setTimeout(() => { refresh(); }, 2000);
}
})
const selectLabels = [
{ label: 'Hour', value: 'hour' },
{ label: 'Day', value: 'day' }
];
const selectLabelsEvents = [
{ label: 'Day', value: 'day' },
{ label: 'Month', value: 'month' },
];
</script>
<template>
<div class="dashboard w-full h-full overflow-y-auto pb-20 md:pt-4 lg:pt-0">
<div :key="'home-' + isLiveDemo()" v-if="projects && activeProject && firstInteraction">
<DashboardTopSection></DashboardTopSection>
<DashboardTopCards></DashboardTopCards>
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row">
<CardTitled class="p-4 flex-1" title="Visits trends" sub="Shows trends in page visits.">
<template #header>
<SelectButton @changeIndex="mainChartSelectIndex = $event" :currentIndex="mainChartSelectIndex"
:options="selectLabels">
</SelectButton>
</template>
<div>
<DashboardVisitsLineChart :slice="(selectLabels[mainChartSelectIndex].value as any)">
</DashboardVisitsLineChart>
</div>
</CardTitled>
<CardTitled class="p-4 flex-1" title="Sessions" sub="Shows trends in sessions.">
<template #header>
<SelectButton @changeIndex="sessionsChartSelectIndex = $event"
:currentIndex="sessionsChartSelectIndex" :options="selectLabels">
</SelectButton>
</template>
<div>
<DashboardSessionsLineChart :slice="(selectLabels[sessionsChartSelectIndex].value as any)">
</DashboardSessionsLineChart>
</div>
</CardTitled>
</div>
<div class="flex flex-row p-6 gap-6">
<CardTitled class="p-4 flex-[4]" title="Events" sub="Events stacked bar chart.">
<template #header>
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
:currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents">
</SelectButton>
</template>
<div>
<EventsStackedBarChart :slice="(selectLabelsEvents[eventsStackedSelectIndex].value as any)">
</EventsStackedBarChart>
</div>
</CardTitled>
<div class="bg-menu p-4 rounded-xl flex-[2] flex flex-col gap-10 h-full">
<div class="flex flex-col gap-1">
<div class="poppins font-semibold text-[1.4rem] text-text">
Top events
</div>
<div class="poppins text-[1rem] text-text-sub/90">
Displays key events.
</div>
</div>
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
</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">
<DashboardWebsitesBarCard></DashboardWebsitesBarCard>
</div>
<div class="flex-1">
<DashboardEventsBarCard></DashboardEventsBarCard>
</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">
<DashboardReferrersBarCard></DashboardReferrersBarCard>
</div>
<div class="flex-1">
<DashboardBrowsersBarCard></DashboardBrowsersBarCard>
</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">
<DashboardOssBarCard></DashboardOssBarCard>
</div>
<div class="flex-1">
<DashboardGeolocationBarCard></DashboardGeolocationBarCard>
</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">
<DashboardDevicesBarCard></DashboardDevicesBarCard>
</div>
<div class="flex-1">
</div>
</div>
</div>
</div>
<div v-if="!firstInteraction && activeProject" class="mt-[36vh] flex flex-col gap-6">
<div class="flex gap-4 items-center justify-center">
<div class="animate-pulse w-[1.5rem] h-[1.5rem] bg-accent rounded-full"> </div>
<div class="text-text/90 poppins text-[1.4rem] font-bold">
Waiting for your first Visit or Event
</div>
</div>
<div class="flex justify-center gap-10 flex-col lg:flex-row items-center lg:items-stretch px-10">
<div class="bg-menu p-6 rounded-xl flex flex-col gap-2 w-full">
<div class="poppins font-semibold"> Copy your project_id: </div>
<div class="flex items-center gap-2">
<div> <i @click="copyProjectId()" class="cursor-pointer hover:text-text far fa-copy"></i> </div>
<div class="text-[.9rem] text-[#acacac]"> {{ activeProject?._id }} </div>
</div>
</div>
<div class="bg-menu p-6 rounded-xl flex flex-col gap-2 w-full lg:max-w-[40vw]">
<div class="poppins font-semibold">
Start logging visits in 1 click | Plug anywhere !
</div>
<div class="flex items-center gap-4">
<div> <i @click="copyScript()" class="cursor-pointer hover:text-text far fa-copy"></i> </div>
<div class="text-[.9rem] text-[#acacac] lg:w-min">
{{ `
<script defer data-project="${activeProject?._id}"
src="https://cdn.jsdelivr.net/npm/litlyx/browser/litlyx.js"></script>` }}
</div>
</div>
</div>
</div>
<div></div>
</div>
<div class="text-text/85 mt-8 ml-8 poppis text-[1.2rem]" v-if="projects && projects.length == 0">
Create your first project...
</div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,215 @@
<script setup lang="ts">
definePageMeta({ layout: 'none' });
const { data: project } = useLiveDemo();
let interval: any;
onMounted(async () => {
await getOnlineUsers();
interval = setInterval(async () => {
await getOnlineUsers();
}, 5000);
})
onUnmounted(() => {
try {
if (interval) clearInterval(interval);
} catch (ex) {
}
})
async function getOnlineUsers() {
if (!project.value) return;
const online = await $fetch<number>(`/api/metrics/${project.value._id}/live_users`, signHeaders());
onlineUsers.value = online;
}
const onlineUsers = ref<number | undefined>();
const mainChartSelectIndex = ref<number>(1);
const sessionsChartSelectIndex = ref<number>(1);
const eventsStackedSelectIndex = ref<number>(0);
const selectLabels = [
{ label: 'Hour', value: 'hour' },
{ label: 'Day', value: 'day' }
];
const selectLabelsEvents = [
{ label: 'Day', value: 'day' },
{ label: 'Month', value: 'month' },
];
</script>
<template>
<div class="dashboard w-full h-full overflow-y-auto pb-20">
<div v-if="project">
<div
class="bg-bg w-full px-6 py-6 text-text/90 flex flex-collg:flex-row text-lg lg:text-2xl gap-2 lg:gap-12">
<div class="flex items-center w-full">
<div class="flex flex-col gap-2">
<div class="poppins font-semibold">
Litlyx open metrics
</div>
<div v-if="project" class="flex gap-2 items-center text-text/90">
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
<div> {{ onlineUsers }} Online users</div>
</div>
</div>
<div class="grow"></div>
<div class="flex gap-2">
<NuxtLink target="_blank" to="https://cal.com/litlyx/30min"
class="bg-white hover:bg-white/90 px-4 py-3 text-black poppins font-semibold text-[.9rem] lg:text-[1.2rem] rounded-lg">
Book a demo
</NuxtLink>
<NuxtLink to="/"
class="bg-accent hover:bg-accent/90 px-4 py-3 poppins font-semibold text-[.9rem] lg:text-[1.2rem] rounded-lg">
Go to dashboard
</NuxtLink>
</div>
</div>
</div>
<div>
<DashboardTopCards></DashboardTopCards>
</div>
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row">
<CardTitled class="p-4 flex-1" title="Visits trends" sub="Shows trends in page visits.">
<template #header>
<SelectButton @changeIndex="mainChartSelectIndex = $event" :currentIndex="mainChartSelectIndex"
:options="selectLabels">
</SelectButton>
</template>
<div>
<DashboardVisitsLineChart :slice="(selectLabels[mainChartSelectIndex].value as any)">
</DashboardVisitsLineChart>
</div>
</CardTitled>
<CardTitled class="p-4 flex-1" title="Sessions" sub="Shows trends in sessions.">
<template #header>
<SelectButton @changeIndex="sessionsChartSelectIndex = $event"
:currentIndex="sessionsChartSelectIndex" :options="selectLabels">
</SelectButton>
</template>
<div>
<DashboardSessionsLineChart :slice="(selectLabels[sessionsChartSelectIndex].value as any)">
</DashboardSessionsLineChart>
</div>
</CardTitled>
</div>
<div class="p-6">
<CardTitled class="p-4 flex-1" title="Events" sub="Events stacked bar chart.">
<template #header>
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
:currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents">
</SelectButton>
</template>
<div>
<EventsStackedBarChart :slice="(selectLabelsEvents[eventsStackedSelectIndex].value as any)">
</EventsStackedBarChart>
</div>
</CardTitled>
</div>
<div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col lg:flex-row">
<div class="flex-1">
<DashboardWebsitesBarCard></DashboardWebsitesBarCard>
</div>
<div class="flex-1">
<DashboardEventsBarCard></DashboardEventsBarCard>
</div>
</div>
</div>
<div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col lg:flex-row">
<div class="flex-1">
<DashboardReferrersBarCard></DashboardReferrersBarCard>
</div>
<div class="flex-1">
<DashboardBrowsersBarCard></DashboardBrowsersBarCard>
</div>
</div>
</div>
<div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col lg:flex-row">
<div class="flex-1">
<DashboardOssBarCard></DashboardOssBarCard>
</div>
<div class="flex-1">
<DashboardGeolocationBarCard></DashboardGeolocationBarCard>
</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">
<DashboardDevicesBarCard></DashboardDevicesBarCard>
</div>
<div class="flex-1">
<!-- <DashboardGeolocationBarCard></DashboardGeolocationBarCard> -->
</div>
</div>
</div>
<div class="justify-center mt-14 lg:mt-40 flex flex-col items-center gap-14 lg:gap-20">
<div class="poppins text-[1.3rem]">
Made with in Italy
</div>
<div class="flex flex-col lg:flex-row justify-between w-full items-center gap-10 lg:gap-0">
<div class="text-[1.9rem] lg:text-[2.2rem] text-center lg:text-left px-2 lg:px-0">
<div class="poppins font-semibold text-accent">
Do you want this KPIs for your website ?
</div>
<div class="poppins font-semibold text-text-sub">
Start now ! It's free.
</div>
</div>
<div class="flex gap-2">
<NuxtLink to="/"
class="bg-accent hover:bg-accent/90 px-14 py-4 poppins font-semibold text-[1.1rem] lg:text-[1.6rem] rounded-lg">
Get started
</NuxtLink>
<NuxtLink target="_blank" to="https://cal.com/litlyx/30min"
class="bg-white hover:bg-white/90 text-black px-14 py-4 poppins font-semibold text-[1.1rem] lg:text-[1.6rem] rounded-lg">
Book a demo
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss"></style>

160
dashboard/pages/login.vue Normal file
View File

@@ -0,0 +1,160 @@
<script setup lang="ts">
definePageMeta({ layout: 'none' });
const { isReady, login } = useCodeClient({ onSuccess: handleOnSuccess, onError: handleOnError, });
const router = useRouter();
const { token, setToken } = useAccessToken();
async function handleOnSuccess(response: any) {
try {
const result = await $fetch('/api/auth/google_login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: response.code })
})
if (result.error) return alert('Error during login, please try again');
setToken(result.access_token);
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
const loggedUser = useLoggedUser();
loggedUser.value = user;
console.log('LOGIN DONE - USER', loggedUser.value);
const { refresh } = useProjectsList();
const isFirstTime = await $fetch<boolean>('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } })
await refresh();
if (isFirstTime === true) {
router.push('/project_creation');
} else {
router.push('/?just_logged=true');
}
} catch (ex) {
alert('Google sta avendo problemi con il login, ci scusiamo per il problema ma non dipende da noi.');
}
};
function handleOnError(errorResponse: any) {
alert('Error' + errorResponse);
};
</script>
<template>
<div class="home w-full h-full">
<div class="flex h-full">
<div class="flex-1 flex flex-col items-center pt-20 lg:pt-[22vh]">
<div class="rotating-thing absolute top-0"></div>
<div class="mb-8 bg-black rounded-xl">
<img class="w-[5rem]" :src="'logo.png'">
</div>
<div class="text-text text-[2.2rem] font-bold poppins">
Sign in with
</div>
<div class="text-text/80 text-[1.2rem] text-center w-[70%] poppins mt-2">
Real-time analytics for 15+ JS/TS frameworks
<br>
with one-line code setup.
<br>
<div class="font-bold poppins mt-4">
Start for Free now! Up to 3k visits/events monthly.
</div>
</div>
<div class="mt-12">
<div @click="login"
class="hover:bg-accent cursor-pointer flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]">
<div class="flex items-center">
<i class="fab fa-google"></i>
</div>
Continue with Google
</div>
</div>
<div class="text-[.9rem] poppins mt-12 text-text-sub text-center relative z-[2]">
By continuing you are indicating that you accept
<br>
our
<a class="underline" href="https://litlyx.com/terms" target="_blank">Terms of Service</a> and
<a class="underline" href="https://litlyx.com/privacy" target="_blank">Privacy Policy</a>.
</div>
</div>
<div class="grow flex-1 items-center justify-center hidden lg:flex">
<!-- <GlobeSvg></GlobeSvg> -->
<img :src="'image-bg.png'" class="h-full py-6">
</div>
</div>
<!-- <div class="flex flex-col items-center justify-center mt-40 gap-20">
<div class="google-login text-gray-700" :class="{ disabled: !isReady }" @click="login">
<div class="icon">
<i class="fab fa-google"></i>
</div>
<div> Continua con Google </div>
</div>
</div> -->
</div>
</template>
<style scoped lang="scss">
.rotating-thing {
height: 100%;
aspect-ratio: 1 / 1;
opacity: 0.15;
background: radial-gradient(51.24% 31.29% at 50% 50%, rgb(51, 58, 232) 0%, rgba(51, 58, 232, 0) 100%);
animation: 12s linear 0s infinite normal none running spin;
}
.google-login {
cursor: pointer;
font-weight: bold;
background-color: #fcefed;
padding: 1rem 2rem;
border-radius: 1rem;
display: flex;
gap: 1rem;
align-items: center;
&.disabled {
filter: brightness(50%);
}
i {
font-size: 1.5rem;
}
}
</style>

152
dashboard/pages/plans.vue Normal file
View File

@@ -0,0 +1,152 @@
<script lang="ts" setup>
import dayjs from 'dayjs';
const activeProject = useActiveProject();
definePageMeta({ layout: 'dashboard' });
const { data: planData } = useFetch('/api/project/plan', signHeaders());
const percent = computed(() => {
if (!planData.value) return '-';
return (100 / planData.value.limit * planData.value.count).toFixed(2) + '%';
});
const color = computed(() => {
if (!planData.value) return 'blue';
if (planData.value.count >= planData.value.limit) return 'red';
return 'blue';
});
const daysLeft = computed(() => {
if (!planData.value) return '-';
return (-dayjs().diff(planData.value.billing_expire_at, 'days')).toString();
});
const leftPercent = computed(() => {
if (!planData.value) return 0;
const left = dayjs().diff(planData.value.billing_expire_at, 'days');
const total = dayjs(planData.value.billing_start_at).diff(planData.value.billing_expire_at, 'days');
const percent = 100 / total * left;
return percent;
});
const prettyExpireDate = computed(() => {
if (!planData.value) return '';
return dayjs(planData.value.billing_expire_at).format('DD/MM/YYYY');
});
const router = useRouter();
function onPlanUpgradeClick() {
router.push('/book_demo');
}
</script>
<template>
<div class="w-full h-full p-8 overflow-y-auto pb-40 lg:pb-0">
<div class="poppins font-semibold text-[1.8rem]">
Billing
</div>
<div class="poppins text-[1.3rem] text-text-sub">
Manage your billing cycle for the project
<span class="font-bold">
{{ activeProject?.name || '' }}
</span>
</div>
<div class="my-4 mb-10 w-full bg-gray-400/30 h-[1px]">
</div>
<div class="flex flex-wrap justify-start gap-8">
<Card v-if="planData" class="px-0 pt-6 pb-4 w-[35rem] flex flex-col">
<div class="flex flex-col gap-6 px-8 grow">
<div class="flex justify-between flex-col sm:flex-row">
<div class="flex flex-col">
<div class="flex gap-3 items-center">
<div class="poppins font-semibold text-[1.1rem]">
{{ planData.premium ? 'Premium plan' : 'Basic plan' }}
</div>
<div
class="flex lato text-[.7rem] bg-accent/25 border-accent/40 border-[1px] px-[.6rem] rounded-lg">
{{ planData.premium ? 'PREMIUM ' + planData.premium_type : 'FREE' }}
</div>
</div>
<div class="poppins text-text-sub text-[.9rem]">
Our free plan for testing the product.
</div>
</div>
<div class="flex items-center gap-1">
<div class="poppins font-semibold text-[2rem]"> $0 </div>
<div class="poppins text-text-sub mt-2"> per month </div>
</div>
</div>
<div class="flex flex-col">
<div class="poppins"> Billing period:</div>
<div class="flex items-center gap-4">
<div class="grow">
<UProgress color="green" :min="0" :max="100" :value="leftPercent"></UProgress>
</div>
<div class="poppins"> {{ daysLeft }} days left </div>
</div>
</div>
</div>
<div class="my-4 w-full bg-gray-400/30 h-[1px]">
</div>
<div class="flex justify-between px-8 flex-col sm:flex-row">
<div class="flex gap-2 text-text-sub text-[.9rem]">
<div class="poppins"> Expire date:</div>
<div> {{ prettyExpireDate }}</div>
</div>
<div @click="onPlanUpgradeClick()"
class="cursor-pointer flex items-center gap-2 text-[.9rem] text-accent drop-shadow-[0_0_8px_#000000]">
<div class="poppins"> Upgrade plan </div>
<i class="fas fa-arrow-up-right"></i>
</div>
</div>
</Card>
<Card v-if="planData" class="px-0 pt-6 pb-4 w-[35rem] flex flex-col">
<div class="flex flex-col gap-6 px-8">
<div class="flex justify-between">
<div class="flex flex-col">
<div class="poppins font-semibold text-[1.1rem]">
Usage
</div>
<div class="poppins text-text-sub text-[.9rem]">
Check the usage limits of your project.
</div>
</div>
</div>
<div class="flex flex-col">
<div class="poppins"> Usage:</div>
<div class="flex items-center gap-4">
<div class="grow">
<UProgress :color="color" :min="0" :max="planData.limit" :value="planData.count">
</UProgress>
</div>
<div class="poppins"> {{ percent }}</div>
</div>
<div class="flex justify-center">
{{ formatNumberK(planData.count) }} / {{ formatNumberK(planData.limit) }}
</div>
</div>
</div>
<div class="my-4 w-full bg-gray-400/30 h-[1px]">
</div>
<div class="flex justify-end px-8 flex-col sm:flex-row">
<div @click="onPlanUpgradeClick()"
class="cursor-pointer flex items-center gap-2 text-[.9rem] text-accent drop-shadow-[0_0_8px_#000000]">
<div class="poppins"> Upgrade plan </div>
<i class="fas fa-arrow-up-right"></i>
</div>
</div>
</Card>
</div>
<CardTitled title="Invoices" sub="No invoices yet" class="p-4 mt-8 max-w-[72rem]">
</CardTitled>
</div>
</template>

101
dashboard/pages/pricing.vue Normal file
View File

@@ -0,0 +1,101 @@
<script setup lang="ts">
definePageMeta({ layout: 'header' });
</script>
<template>
<div class="home h-full overflow-y-auto relative">
<div class="absolute top-0 left-0 w-full h-full flex flex-col items-center z-0 overflow-hidden">
<HomeBgGrid :size="50" :spacing="18" opacity="0.3" class="w-fit h-fit"></HomeBgGrid>
<HomeBgGrid :size="50" :spacing="18" opacity="0.3" class="w-fit h-fit"></HomeBgGrid>
</div>
<div class="flex w-full justify-center px-20">
<SelectButton class="text-[1.4rem]" :current-index="0" :options="[
{ label: 'Monthly' },
{ label: 'Yearly' },
]"></SelectButton>
</div>
<div class="flex mt-20 gap-10 mx-20">
<PricingCard title="Free plan" icon="far fa-fire" price="€ 54" :list="[
{ text: '3k Page visits / Custom Events (one time)', icon: 'fas fa-check' },
{ text: 'Access to Crm', icon: 'fas fa-check' },
{ text: 'Download CSV Raw data', icon: 'fas fa-check' },
{ text: 'PDF Report For investor', icon: 'fas fa-check' },
{ text: 'Team member', icon: '' },
{ text: 'Only 1 project connected', icon: '' },
]"></PricingCard>
<PricingCard title="Free plan" icon="far fa-fire" price="€ 54" :list="[
{ text: '3k Page visits / Custom Events (one time)', icon: 'fas fa-check' },
{ text: 'Access to Crm', icon: 'fas fa-check' },
{ text: 'Download CSV Raw data', icon: 'fas fa-check' },
{ text: 'PDF Report For investor', icon: 'fas fa-check' },
{ text: 'Team member', icon: '' },
{ text: 'Only 1 project connected', icon: '' },
]"></PricingCard>
<PricingCard title="Free plan" icon="far fa-fire" price="€ 54" :list="[
{ text: '3k Page visits / Custom Events (one time)', icon: 'fas fa-check' },
{ text: 'Access to Crm', icon: 'fas fa-check' },
{ text: 'Download CSV Raw data', icon: 'fas fa-check' },
{ text: 'PDF Report For investor', icon: 'fas fa-check' },
{ text: 'Team member', icon: '' },
{ text: 'Only 1 project connected', icon: '' },
]"></PricingCard>
<PricingCard title="Free plan" icon="far fa-fire" price="€ 54" :list="[
{ text: '3k Page visits / Custom Events (one time)', icon: 'fas fa-check' },
{ text: 'Access to Crm', icon: 'fas fa-check' },
{ text: 'Download CSV Raw data', icon: 'fas fa-check' },
{ text: 'PDF Report For investor', icon: 'fas fa-check' },
{ text: 'Team member', icon: '' },
{ text: 'Only 1 project connected', icon: '' },
]"></PricingCard>
</div>
<div class="flex w-full py-20 my-20 px-20">
<div class="flex justify-between w-full">
<div class="flex flex-col gap-2">
<div class="poppins font-bold text-[2.2rem]">
Ready to ditch Google Analytics ?
</div>
<div class="text-accent poppins font-bold text-[2.2rem]">
Start your free trial today.
</div>
</div>
<div class="flex gap-4 items-center mr-20">
<div class="poppins px-12 py-6 text-[1.2rem] hover:bg-accent/90 text-text cursor-pointer bg-accent rounded-xl font-semibold">
Get started
</div>
<div class="poppins px-12 py-6 text-[1.2rem] hover:bg-text/90 cursor-pointer text-accent bg-text rounded-xl font-semibold">
Live demo
</div>
</div>
</div>
</div>
</div>
</template>

122
dashboard/pages/privacy.vue Normal file
View File

@@ -0,0 +1,122 @@
<script lang="ts" setup>
definePageMeta({ layout: 'header' });
</script>
<template>
<div class="tutto-poppins flex flex-col gap-3 px-96 mt-20 text-[1.2rem] leading-[2rem]">
<div class="font-bold text-[2rem]">
LitLyx Privacy Policy
</div>
<div class="bg-gray-500/90 h-[1px] w-full my-6"></div>
<div>
For our beloved litlyx.com visitors and users.
This document outlines our commitment to privacy for all visitors and users.
</div>
<div>
At LitLyx Analytics, your privacy is really important. We avoid using cookies and never gather personal
information. Should you opt to register an account, only essential details are requested, and these are
shared
exclusively with crucial services required for app functionality.
</div>
<div>
LitLyx Analytics (SaaS) adheres strictly to GDPR, CCPA, PECR, and other relevant privacy standards both on
our site and within our analytics tool. We prioritize the confidentiality of your informationit belongs to
you, not us. This policy details the types of data we gather, the handling process, and your rights over
your data. We are committed to never selling your datathis has always been our stance.
</div>
<div>
For those using the LitLyx Analytics script on their sites, refer to our detailed data policy to understand
what
we collect on your behalf regarding site visitors.
</div>
<div class="font-bold mb-1 mt-4">Visitor privacy on litlyx.com includes:</div>
<ul>
<div class="ml-8"> No collection of personal details</div>
<div class="ml-8"> No browser cookie storage </div>
<div class="ml-8"> No data sharing with third parties or advertisers </div>
<div class="ml-8"> No collection or analysis of personal or behavioral trends </div>
<div class="ml-8"> No monetization of data </div>
</ul>
<div>
We deploy the LitLyx Analytics script solely to accumulate anonymous statistical data, aiming to analyze
general
website traffic trends without tracking individual users. All collected data is aggregated, and no personal
information is captured. Our live demo page showcases the accessible data, including referral sources, top
visited pages, session durations, and device specifics like type, OS, country, and browser.
</div>
<div class="font-bold mb-1 mt-4"> As a LitLyx Analytics subscriber: </div>
<div>
Our core principle is minimal data collectiononly what is essential for delivering the services you
register
for. We select trusted external providers who comply with stringent data security and privacy regulations.
Information shared with them is strictly necessary for their services, and they are contractually obliged to
maintain confidentiality and adhere to our processing directives.
</div>
<div>
Here's a practical overview
</div>
<div class="font-bold mb-1 mt-4"> Collected data and usage:</div>
<ul>
<div class="ml-8">
Email address: Required for account setup to enable login, personalization, and to send you necessary
communications like invoices or updates.
</div>
<div class="ml-8">
Persistent first-party cookie: Facilitates seamless login across sessions, improving usability. You
control cookie settings via your browser, including their deletion.
</div>
<div class="ml-8">
Data security: All collected data is securely encrypted and stored on renewable
energy-powered servers in Falkenstein, Germany, adhering to EU data privacy laws. Your data does not
leave the EU.
</div>
<div class="ml-8">
Payment processing: Handled by PayPal, with detailed privacy information
available on their site.
</div>
<div class="ml-8">
Email communication: Managed by European providers Gmail or Brevo, with disabled tracking features.
Full privacy details are available on their respective sites.
</div>
</ul>
<div class="font-bold mb-1 mt-4"> Data retention: </div>
<div>
Your data remains with us as long as your account is active or as needed to deliver services. It is used in
line
with this policy and retained to fulfill legal obligations, resolve disputes, and protect LitLyxs rights.
You
may delete your account anytime, which results in immediate and permanent data deletion.
</div>
<div class="font-bold mb-1 mt-4"> Updates and inquiries: </div>
<div>
We update this policy as necessary to stay compliant and reflect new practices. Significant changes are
communicated through our blog, social media, and direct emails.
</div>
<div>
Please reach out to us at <a href="mailto:helplitlyx@gmail.com" class=text-blue-400>helplitlyx@gmail.com</a> with any questions or concerns regarding this privacy policy
or
your rights.
</div>
<div class="font-bold mb-1 mt-4"> Last updated: May 1, 2024 </div>
</div>
</template>
<style scoped lang=scss>
.tutto-poppins * {
font-family: "Poppins";
}
</style>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
definePageMeta({ layout: 'dashboard' });
const projectName = ref<string>("");
const creating = ref<boolean>(false);
const router = useRouter();
const { data: projects, refresh: refreshProjects } = useProjectsList();
const isFirstProject = computed(() => { return projects.value?.length == 0; })
import { Lit } from 'litlyx';
onMounted(() => {
if (projects.value?.length == 0) {
setPageLayout('none');
}
})
async function createProject() {
if (projectName.value.length < 2) return;
Lit.event('create_project');
creating.value = true;
try {
await $fetch('/api/project/create', {
method: 'POST',
...signHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ name: projectName.value })
});
await refreshProjects();
const newActiveProjectId = projects.value?.[projects.value?.length - 1]._id.toString();
if (newActiveProjectId) {
await setActiveProject(newActiveProjectId);
}
await refreshProjects();
router.push('/');
} catch (ex: any) {
alert(ex.message);
}
creating.value = false;
}
</script>
<template>
<div class="home relative h-full overflow-y-auto lg:overflow-hidden">
<div class="absolute w-full h-full z-[8] flex justify-center items-center">
<HomeBgGrid :size="120" :spacing="12" opacity="0.2"></HomeBgGrid>
</div>
<div class="flex flex-col items-center justify-center pt-[12rem] gap-12 relative z-[10]">
<div class="text-[3rem] font-semibold text-center">
Create your {{ isFirstProject ? 'first' : '' }} project
</div>
<div v-if="isFirstProject" class="text-[1.5rem]">
Welcome to Litlyx. Setup your project in less than 30 seconds.
</div>
<div class="w-[20rem] flex flex-col gap-2">
<div class="text-lg text-text-sub font-semibold">
{{ isFirstProject ? 'Choose a name' : 'Project name' }}
</div>
<CInput placeholder="ProjectName" :readonly="creating" v-model="projectName"></CInput>
</div>
<div>
<CButton :loading="creating" @click="createProject()" :disabled="projectName.length < 2"
class="rounded-lg w-[10rem] text-md font-semibold" label="Create"></CButton>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,111 @@
<script setup lang="ts">
definePageMeta({ layout: 'dashboard' });
const { data: projects, refresh } = useProjectsList();
const activeProject = useActiveProject();
async function deleteProject(projectId: string, projectName: string) {
const sure = confirm(`Are you sure to delete the project ${projectName} ?`);
if (!sure) return;
try {
await $fetch('/api/project/delete', {
method: 'DELETE',
...signHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ project_id: projectId })
});
if (activeProject.value?._id.toString() == projectId) {
const firstProjectId = projects.value?.[0]?._id.toString();
if (firstProjectId) {
await setActiveProject(firstProjectId);
}
}
await refresh();
} catch (ex: any) {
alert(ex.message);
}
}
</script>
<template>
<div class="h-full w-full pb-40 lg:pb-0 project-selector overflow-y-auto">
<div class="flex flex-col justify-center mt-16 gap-10 px-10" v-if="projects">
<div class="flex gap-4 items-center">
<div class="text-text font-bold text-[1.5rem]"> Projects </div>
<div class="text-text-sub/90 text-[1rem] font-semibold lato">
{{ projects?.length ?? '-' }} / 3
</div>
<NuxtLink v-if="(projects?.length || 0) < 3" to="/project_creation"
class="bg-blue-500/20 hover:bg-blue-500/30 px-4 py-1 flex items-center gap-4 rounded-xl cursor-pointer">
<div class="h-full aspect-[1/1] flex items-center justify-center">
<i class="fas fa-plus text-[1rem] text-text-sub/80"></i>
</div>
<div>
<div class="text-text font-semibold manrope text-[1.3rem]"> Create new project</div>
</div>
</NuxtLink>
</div>
<div class="text-text/85 mt-8 poppis text-[1.2rem]" v-if="projects.length == 0">
Create your first project...
</div>
<div class="flex gap-12 flex-wrap" v-if="activeProject">
<div v-for="e of projects">
<DashboardProjectSelectionCard @click="setActiveProject(e._id.toString())"
:active="activeProject._id == e._id" :title="e.name"
:subtitle="activeProject._id == e._id ? 'ATTIVO' : ''"
:chip="e.premium ? 'PREMIUM PLAN' : 'FREE PLAN'">
</DashboardProjectSelectionCard>
<div @click="deleteProject(e._id.toString(), e.name)"
class="mt-4 rounded-lg bg-[#3a3a3b] hover:bg-[#4f4f50] cursor-pointer hover:text-red-500 flex items-center justify-center py-3">
<i class="far fa-trash"></i>
</div>
</div>
<!--
<div class="bg-blue-500/20 hover:bg-blue-500/30 p-4 w-[20rem] h-[6rem] flex items-center gap-4 rounded-xl cursor-pointer relative"
v-for="e of projects" @click="setActiveProject(e._id.toString())">
<div class="absolute right-2 top-2" v-if="project._id == e._id">
<i class="far fa-circle-check text-green-600"></i>
</div>
<div class="h-full aspect-[1/1]">
<div class="w-full h-full bg-blue-500 rounded-xl"></div>
</div>
<div>
<div class="text-text text-ellipsis line-clamp-1 font-semibold manrope text-[1.1rem]">
{{ e.name }}
</div>
<div class="text-text-sub font-normal lato text-[.9rem]">
{{ e.premium ? 'PREMIUM PLAN' : 'FREE PLAN' }}
</div>
</div>
</div> -->
<!-- <NuxtLink v-if="(projects?.length || 0) < 3" to="/project_creation"
class="bg-blue-500/20 hover:bg-blue-500/30 p-4 w-[20rem] h-[6rem] flex items-center gap-4 rounded-xl cursor-pointer">
<div class="h-full aspect-[1/1] flex items-center justify-center">
<i class="fas fa-plus text-[2rem] text-text-sub/80"></i>
</div>
<div>
<div class="text-text font-semibold manrope text-[1.1rem]"> Create new project</div>
<div class="text-text-sub font-normal lato text-[.9rem]"></div>
</div>
</NuxtLink> -->
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject();
async function generatePDF() {
const res = await $fetch<Blob>('/api/project/generate_pdf', {
...signHeaders(),
responseType: 'blob'
});
const url = URL.createObjectURL(res);
const a = document.createElement('a');
a.href = url;
a.download = `Report.pdf`;
a.click();
URL.revokeObjectURL(url);
}
</script>
<template>
<div class="home w-full h-full px-10 lg:px-0">
<div class="flex flex-col items-center justify-center mt-20 gap-20">
<div class="flex flex-col items-center justify-center gap-10">
<div class="poppins text-[2.4rem] font-bold text-text">
Project Report
</div>
<div class="poppins text-[1.8rem] text-text-sub/90">
One-Click, Comprehensive KPI PDF for Your Investors or Team.
</div>
<div v-if="activeProject" class="flex gap-2">
<div class="poppins text-[1.4rem] font-semibold text-text-sub/90">
Relative to:
</div>
<div class="poppins text-[1.4rem] font-semibold text-text">
{{ activeProject.name }}
<span class="text-[.9rem] text-text-sub/80"> ( {{ activeProject._id }} ) </span>
</div>
</div>
</div>
<div>
<div @click="generatePDF()"
class="flex flex-col rounded-xl overflow-hidden hover:shadow-[0_0_50px_#2969f1] hover:outline hover:outline-[2px] hover:outline-accent cursor-pointer">
<div class="h-[14rem] aspect-[9/7] bg-[#2f2a64] flex relative">
<img class="object-cover" :src="'/report/card_image.png'">
<div
class="absolute px-4 py-1 rounded-lg poppins left-2 flex gap-2 bottom-2 bg-orange-500/80 items-center">
<div class="flex items-center"> <i class="far fa-fire text-[1.1rem]"></i></div>
<div class="poppins text-[1rem] font-semibold"> Popular </div>
</div>
</div>
<div class="bg-[#444444cc] p-4 h-[7rem] relative">
<div class="poppins text-[1.2rem] font-bold text-text">
Generate
</div>
<div class="poppins text-[1rem] text-text-sub/90">
Create your report now
</div>
<div class="absolute right-4 bottom-3">
<i class="fas fa-arrow-right text-[1.2rem]"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

15
dashboard/pages/terms.vue Normal file
View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
</script>
<template>
<div>
TEST
</div>
</template>