mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-09 23:48:36 +01:00
add dashboard
This commit is contained in:
124
dashboard/pages/admin/index.vue
Normal file
124
dashboard/pages/admin/index.vue
Normal 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
205
dashboard/pages/analyst.vue
Normal 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>
|
||||
29
dashboard/pages/book_demo.vue
Normal file
29
dashboard/pages/book_demo.vue
Normal 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>
|
||||
100
dashboard/pages/dashboard/events.vue
Normal file
100
dashboard/pages/dashboard/events.vue
Normal 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>
|
||||
74
dashboard/pages/dashboard/settings.vue
Normal file
74
dashboard/pages/dashboard/settings.vue
Normal 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>
|
||||
107
dashboard/pages/dashboard/visits.vue
Normal file
107
dashboard/pages/dashboard/visits.vue
Normal 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
102
dashboard/pages/events.vue
Normal 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
236
dashboard/pages/index.vue
Normal 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>
|
||||
215
dashboard/pages/live_demo.vue
Normal file
215
dashboard/pages/live_demo.vue
Normal 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
160
dashboard/pages/login.vue
Normal 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
152
dashboard/pages/plans.vue
Normal 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
101
dashboard/pages/pricing.vue
Normal 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
122
dashboard/pages/privacy.vue
Normal 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 information—it 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 data—this 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 collection—only 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 LitLyx’s 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>
|
||||
99
dashboard/pages/project_creation.vue
Normal file
99
dashboard/pages/project_creation.vue
Normal 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>
|
||||
111
dashboard/pages/project_selector.vue
Normal file
111
dashboard/pages/project_selector.vue
Normal 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>
|
||||
87
dashboard/pages/report.vue
Normal file
87
dashboard/pages/report.vue
Normal 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
15
dashboard/pages/terms.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
TEST
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
Reference in New Issue
Block a user