mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f86a399840 | ||
|
|
36c4406af2 | ||
|
|
b2afd585bb | ||
|
|
24ae9d0e0d | ||
|
|
b479ca1bbf | ||
|
|
0a748346c5 | ||
|
|
fa7880552a | ||
|
|
06fb8bfab0 | ||
|
|
a876d77d42 | ||
|
|
e6bb58693f | ||
|
|
00e63cc80b | ||
|
|
e43f138945 | ||
|
|
73309e7021 | ||
|
|
80e3b0caa9 | ||
|
|
0a7f2b58d0 | ||
|
|
e953af2c1b | ||
|
|
126296d28f | ||
|
|
8dd10deecc | ||
|
|
f22e65ccc5 | ||
|
|
dfbc64fe33 | ||
|
|
9568566361 | ||
|
|
634cb641f1 | ||
|
|
204e1348b4 | ||
|
|
b73155a176 | ||
|
|
62c72b3ff9 | ||
|
|
79e956e930 | ||
|
|
b27cacf4e6 | ||
|
|
c2846ca595 | ||
|
|
e1953f2f9f | ||
|
|
314660d8a3 | ||
|
|
f516c53b7b | ||
|
|
dad8c521ee | ||
|
|
089d1a418e | ||
|
|
a08624b69b | ||
|
|
3ba6cd171b |
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/node_modules": true
|
||||
}
|
||||
}
|
||||
32
README.md
32
README.md
@@ -4,13 +4,13 @@
|
||||
</p>
|
||||
|
||||
<h4 align="center">
|
||||
🌐 <a href="https://litlyx.com">Website</a> 📚 <a href="https://docs.litlyx.com">Docs</a> 👾 <a href="https://discord.gg/9cQykjsmWX">Join Discord</a> 🔥 <a href="https://dashboard.litlyx.com">Start for free!</a>
|
||||
🌐 <a href="https://litlyx.com">Website</a> 📚 <a href="https://docs.litlyx.com">Docs</a> 👾 <a href="https://discord.gg/9cQykjsmWX">Join Discord</a> 🔥 <a href="https://dashboard.litlyx.com">Try Litlyx Cloud. It's Free.</a>
|
||||
</h4>
|
||||
|
||||
#
|
||||
<p align="center">
|
||||
The easiest, developer-centric analytics tool.<br>
|
||||
Litlyxis an open-source, self-hostable analytics solution for modern framework. Setup takes less than 30 seconds!
|
||||
The freshest, developer-friendly analytics tool.<br>
|
||||
Litlyx is an open-source, self-hostable analytics solution for modern frameworks. Setup takes less than 30 seconds!
|
||||
</p>
|
||||
|
||||
#
|
||||
@@ -23,27 +23,27 @@
|
||||
|
||||
#
|
||||
|
||||
## Pre-Requisites on Cloud Version
|
||||
## Get Started on our Cloud Version
|
||||
|
||||
Sign-up on [Litlyx.com](https://dashboard.litlyx.com) and create a project. Then simply use your project_id to connect Litlyx to your website OR Self-Host Litlyx with Docker.
|
||||
|
||||
## Universal Installation
|
||||
|
||||
```html
|
||||
<script defer data-project="project_id_here" src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></script>
|
||||
<script defer data-project="your_project_id" src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></script>
|
||||
```
|
||||
|
||||
Importing Litlyx with a direct script instantly starts tracking 10 KPIs, including `Page visits`, `Browsers`, `Devices`, `Operating Systems`, `Real-Time Online Users`, `Unique Sessions`, `Countries`, and `Average Session Time`.
|
||||
Importing Litlyx with a direct script instantly starts tracking `Page visits`, `Browsers`, `Devices`, `Operating Systems`, `Bouncing Rate`, `Real-Time Online Users`, `Unique Sessions`, `Countries`, and `Average Session Time`.
|
||||
|
||||
# All Javascript Runtimes
|
||||
|
||||
You can install Litlyx using `npm`, `yarn`, or `pnpm`:
|
||||
You can install Litlyx using `npm`, `pnpm`, `yarn` or any modern package managers:
|
||||
|
||||
```sh
|
||||
npm i litlyx-js
|
||||
```
|
||||
|
||||
Litlyx natively works with all JavaScript / TypeScript frameworks. You can use Litlyx in all WordPress Websites by injecting JS code using a plug-in. Litlyx also works in serverless enviroments with Cloud (or Edge) Functions.
|
||||
Litlyx natively works with all JavaScript / TypeScript frameworks. You can use Litlyx in all WordPress Websites by injecting JS code using a plug-in. Litlyx also works in serverless environments with Cloud (or Edge) Functions.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/tech.png" />
|
||||
@@ -65,7 +65,7 @@ Lit.init('your_project_id');
|
||||
|
||||
After initialization, Litlyx will automatically track analytics such as `Page visits`, `Browsers`, `Devices`, `Operating Systems`, `Real-Time Online Users`, `Unique Sessions`, `Countries`, and `Average Session Time`.
|
||||
|
||||
# Custom Events
|
||||
# Track Custom Events
|
||||
|
||||
You aren't just limited to the built-in KPIs. With Litlyx, you can create your own events to track in your project.
|
||||
|
||||
@@ -80,6 +80,7 @@ Lit.event('click_on_buy_item', {
|
||||
metadata: {
|
||||
'product-name': 'Coca-Cola',
|
||||
'price': 1.50,
|
||||
'currency': 'EUR'
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -119,6 +120,19 @@ docker-compose up
|
||||
|
||||
at localhost:3000 you will see your own instance of the Litlyx Dashboard.
|
||||
|
||||
## Forward data to your local instance with script tag
|
||||
|
||||
To forward your data on your self-hosted instance, you need to set up the following variables: add your `data-host`, add your `data-port`, and add your `data-secure`, setting it to true if it is HTTPS, and false if it is HTTP.
|
||||
|
||||
```html
|
||||
<script defer data-project="your_project_id"
|
||||
data-host="your-host-name"
|
||||
data-port="your-port"
|
||||
data-secure="true/false"
|
||||
src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js">
|
||||
</script>
|
||||
```
|
||||
|
||||
# Official Docs
|
||||
|
||||
For more info read our [documentation](https://docs.litlyx.com). (will be improved in the near future using Mintlify!)
|
||||
|
||||
BIN
assets/claim.png
BIN
assets/claim.png
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 123 KiB |
@@ -19,6 +19,18 @@
|
||||
src: url("../fonts/GeistVF.ttf");
|
||||
}
|
||||
|
||||
|
||||
.actionable-visits-color-checkbox {
|
||||
color: #5655d7;
|
||||
}
|
||||
|
||||
.actionable-sessions-color-checkbox {
|
||||
color: #4abde8;
|
||||
}
|
||||
.actionable-events-color-checkbox {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.geist {
|
||||
font-family: "Geist";
|
||||
}
|
||||
@@ -72,10 +84,14 @@
|
||||
|
||||
|
||||
.hide-scrollbars {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
|
||||
/* Firefox */
|
||||
&::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
display: none;
|
||||
/* Chrome, Safari and Opera */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,4 +12,17 @@
|
||||
|
||||
.test3 {
|
||||
border: 3px solid green !important;
|
||||
}
|
||||
|
||||
|
||||
.bgtest {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
.bgtest2 {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
.bgtest3 {
|
||||
background-color: green;
|
||||
}
|
||||
@@ -68,11 +68,14 @@ function openExternalLink(link: string) {
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="rawButton" class="hidden lg:flex">
|
||||
<div @click="$emit('showRawData')"
|
||||
class="cursor-pointer flex gap-1 items-center justify-center font-semibold poppins rounded-lg text-[#5680f8] hover:text-[#5681f8ce]">
|
||||
<div> Raw data </div>
|
||||
<div class="flex items-center"> <i class="fas fa-arrow-up-right"></i> </div>
|
||||
</div>
|
||||
|
||||
<LyxUiButton @click="$emit('showRawData')" type="primary" class="h-fit">
|
||||
<div class="flex gap-1 items-center justify-center ">
|
||||
<div> Show raw data </div>
|
||||
<div class="flex items-center"> <i class="fas fa-arrow-up-right"></i> </div>
|
||||
</div>
|
||||
</LyxUiButton>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,12 +125,11 @@ function openExternalLink(link: string) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-text font-semibold text-[.9rem] md:text-[1rem] manrope"> {{
|
||||
formatNumberK(element.count) }} </div>
|
||||
</div>
|
||||
<div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-bold text-[1.1rem]">
|
||||
No visits yet
|
||||
<div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-light text-[1.1rem]">
|
||||
No data yet
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 ">
|
||||
35
dashboard/components/BarCard/Browsers.vue
Normal file
35
dashboard/components/BarCard/Browsers.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
const browsersData = useFetch('/api/data/browsers', {
|
||||
headers: useComputedHeaders({ limit: 10, }), lazy: true
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
async function showMore() {
|
||||
dialogBarData.value=[];
|
||||
showDialog.value = true;
|
||||
isDataLoading.value = true;
|
||||
|
||||
const res = await $fetch('/api/data/browsers', {
|
||||
headers: useComputedHeaders({ limit: 1000 }).value
|
||||
});
|
||||
|
||||
dialogBarData.value = res || [];
|
||||
|
||||
isDataLoading.value = false;
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" :data="browsersData.data.value || []"
|
||||
desc="The browsers most used to search your website." :dataIcons="false"
|
||||
:loading="browsersData.pending.value" label="Top Browsers" sub-label="Browsers">
|
||||
</BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
42
dashboard/components/BarCard/Devices.vue
Normal file
42
dashboard/components/BarCard/Devices.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
function transform(data: { _id: string, count: number }[]) {
|
||||
console.log(data);
|
||||
return data.map(e => ({ ...e, _id: e._id == null ? 'unknown' : e._id }))
|
||||
}
|
||||
|
||||
const devicesData = useFetch('/api/data/devices', {
|
||||
headers: useComputedHeaders({ limit: 10, }), lazy: true,
|
||||
transform
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
async function showMore() {
|
||||
dialogBarData.value = [];
|
||||
showDialog.value = true;
|
||||
isDataLoading.value = true;
|
||||
|
||||
const res = await $fetch('/api/data/devices', {
|
||||
headers: useComputedHeaders({ limit: 1000 }).value,
|
||||
});
|
||||
|
||||
|
||||
dialogBarData.value = transform(res || []);
|
||||
|
||||
isDataLoading.value = false;
|
||||
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<BarCardBase @showMore="showMore()" @dataReload="devicesData.refresh()" :data="devicesData.data.value || []"
|
||||
:dataIcons="false" desc="The devices most used to access your website." :loading="devicesData.pending.value"
|
||||
label="Top Devices" sub-label="Devices"></BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
42
dashboard/components/BarCard/Events.vue
Normal file
42
dashboard/components/BarCard/Events.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goToView() {
|
||||
router.push('/dashboard/events');
|
||||
}
|
||||
|
||||
const eventsData = useFetch('/api/data/events', {
|
||||
headers: useComputedHeaders({
|
||||
limit: 10,
|
||||
}), lazy: true
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
async function showMore() {
|
||||
dialogBarData.value=[];
|
||||
showDialog.value = true;
|
||||
isDataLoading.value = true;
|
||||
|
||||
const res = await $fetch('/api/data/events', {
|
||||
headers: useComputedHeaders({ limit: 1000 }).value
|
||||
});
|
||||
|
||||
dialogBarData.value = res || [];
|
||||
|
||||
isDataLoading.value = false;
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<BarCardBase @showMore="showMore()" @showRawData="goToView()"
|
||||
desc="Most frequent user events triggered in this project" @dataReload="eventsData.refresh()"
|
||||
:data="eventsData.data.value || []" :loading="eventsData.pending.value" label="Top Events"
|
||||
sub-label="Events" :rawButton="!isLiveDemo"></BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
50
dashboard/components/BarCard/Geolocations.vue
Normal file
50
dashboard/components/BarCard/Geolocations.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type { IconProvider } from '../BarCard/Base.vue';
|
||||
|
||||
function iconProvider(id: string): ReturnType<IconProvider> {
|
||||
if (id === 'self') return ['icon', 'fas fa-link'];
|
||||
return [
|
||||
'img',
|
||||
`https://raw.githubusercontent.com/hampusborgos/country-flags/main/png250px/${id.toLowerCase()}.png`
|
||||
]
|
||||
}
|
||||
|
||||
const customIconStyle = `width: 2rem; padding: 1px;`
|
||||
|
||||
const geolocationData = useFetch('/api/data/countries', {
|
||||
headers: useComputedHeaders({ limit: 10, }), lazy: true
|
||||
});
|
||||
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
async function showMore() {
|
||||
dialogBarData.value=[];
|
||||
showDialog.value = true;
|
||||
isDataLoading.value = true;
|
||||
|
||||
const res = await $fetch('/api/data/countries', {
|
||||
headers: useComputedHeaders({limit: 1000}).value
|
||||
});
|
||||
|
||||
dialogBarData.value = res?.map(e => {
|
||||
return { ...e, icon: iconProvider(e._id) }
|
||||
}) || [];
|
||||
|
||||
isDataLoading.value = false;
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()"
|
||||
:data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value"
|
||||
label="Top Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"
|
||||
desc=" Lists the countries where users access your website.">
|
||||
</BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
36
dashboard/components/BarCard/OperatingSystems.vue
Normal file
36
dashboard/components/BarCard/OperatingSystems.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
const ossData = useFetch('/api/data/oss', {
|
||||
headers: useComputedHeaders({
|
||||
limit: 10,
|
||||
}), lazy: true
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
|
||||
async function showMore() {
|
||||
dialogBarData.value=[];
|
||||
showDialog.value = true;
|
||||
isDataLoading.value = true;
|
||||
|
||||
const res = await $fetch('/api/data/oss', {
|
||||
headers: useComputedHeaders({ limit: 1000 }).value
|
||||
});
|
||||
|
||||
dialogBarData.value = res || [];
|
||||
|
||||
isDataLoading.value = false;
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<BarCardBase @showMore="showMore()" @dataReload="ossData.refresh()" :data="ossData.data.value || []"
|
||||
desc="The operating systems most commonly used by your website's visitors." :dataIcons="false"
|
||||
:loading="ossData.pending.value" label="Top OS" sub-label="OSs"></BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
53
dashboard/components/BarCard/Referrers.vue
Normal file
53
dashboard/components/BarCard/Referrers.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type { IconProvider } from './Base.vue';
|
||||
|
||||
function iconProvider(id: string): ReturnType<IconProvider> {
|
||||
if (id === 'self') return ['icon', 'fas fa-link'];
|
||||
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${id}&sz=64`]
|
||||
}
|
||||
|
||||
function elementTextTransformer(element: string) {
|
||||
if (element === 'self') return 'Direct Link';
|
||||
return element;
|
||||
}
|
||||
|
||||
const referrersData = useFetch('/api/data/referrers', {
|
||||
headers: useComputedHeaders({
|
||||
limit: 10,
|
||||
}), lazy: true
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
async function showMore() {
|
||||
|
||||
dialogBarData.value=[];
|
||||
|
||||
showDialog.value = true;
|
||||
isDataLoading.value = true;
|
||||
|
||||
const res = await $fetch('/api/data/referrers', {
|
||||
headers: useComputedHeaders({limit: 1000}).value
|
||||
});
|
||||
|
||||
|
||||
dialogBarData.value = res?.map(e => {
|
||||
return { ...e, icon: iconProvider(e._id) }
|
||||
}) || [];
|
||||
|
||||
isDataLoading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
|
||||
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
|
||||
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."
|
||||
:dataIcons="true" :loading="referrersData.pending.value" label="Top Referrers" sub-label="Referrers">
|
||||
</BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
57
dashboard/components/BarCard/Websites.vue
Normal file
57
dashboard/components/BarCard/Websites.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const currentWebsite = ref<string>("");
|
||||
|
||||
const websitesData = useFetch('/api/data/websites', {
|
||||
headers: useComputedHeaders({
|
||||
limit: 10,
|
||||
}), lazy: true
|
||||
});
|
||||
|
||||
const pagesData = useFetch('/api/data/websites_pages', {
|
||||
headers: useComputedHeaders({
|
||||
limit: 10,
|
||||
custom: {
|
||||
'x-website-name': currentWebsite
|
||||
}
|
||||
}), lazy: true
|
||||
});
|
||||
|
||||
|
||||
const isPagesView = ref<boolean>(false);
|
||||
|
||||
const currentData = computed(() => {
|
||||
return isPagesView.value ? pagesData : websitesData
|
||||
})
|
||||
|
||||
|
||||
async function showDetails(website: string) {
|
||||
currentWebsite.value = website;
|
||||
isPagesView.value = true;
|
||||
}
|
||||
|
||||
async function showGeneral() {
|
||||
websitesData.execute();
|
||||
isPagesView.value = false;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goToView() {
|
||||
router.push('/dashboard/visits');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<BarCardBase :hideShowMore="true" @showGeneral="showGeneral()" @showRawData="goToView()"
|
||||
@dataReload="currentData.refresh()" @showDetails="showDetails" :data="currentData.data.value || []"
|
||||
:loading="currentData.pending.value" :label="isPagesView ? 'Top pages' : 'Top Domains'"
|
||||
:sub-label="isPagesView ? 'Page' : 'Domains'"
|
||||
:desc="isPagesView ? 'Most visited pages' : 'Most visited domains in this project'"
|
||||
:interactive="!isPagesView" :rawButton="!isLiveDemo" :isDetailView="isPagesView">
|
||||
</BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type { TProject } from '@schema/ProjectSchema';
|
||||
import CreateSnapshot from './dialog/CreateSnapshot.vue';
|
||||
|
||||
export type Entry = {
|
||||
@@ -27,8 +26,8 @@ type Props = {
|
||||
const route = useRoute();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { isAdmin } = useUserRoles();
|
||||
const loggedUser = useLoggedUser()
|
||||
const { userRoles, setLoggedUser } = useLoggedUser();
|
||||
const { projectList } = useProject();
|
||||
|
||||
const debugMode = process.dev;
|
||||
|
||||
@@ -57,7 +56,7 @@ const { createAlert } = useAlert()
|
||||
async function deleteSnapshot(close: () => any) {
|
||||
await $fetch("/api/snapshot/delete", {
|
||||
method: 'DELETE',
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value,
|
||||
body: JSON.stringify({
|
||||
id: snapshot.value._id.toString(),
|
||||
})
|
||||
@@ -72,11 +71,7 @@ async function generatePDF() {
|
||||
|
||||
try {
|
||||
const res = await $fetch<Blob>('/api/project/generate_pdf', {
|
||||
...signHeaders({
|
||||
'x-snapshot-name': snapshot.value.name,
|
||||
'x-from': snapshot.value.from.toISOString(),
|
||||
'x-to': snapshot.value.to.toISOString(),
|
||||
}),
|
||||
headers: useComputedHeaders({ useSnapshotDates: false, custom: { 'x-snapshot-name': snapshot.value.name } }).value,
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
@@ -101,17 +96,6 @@ function onLogout() {
|
||||
router.push('/login');
|
||||
}
|
||||
|
||||
const { projects } = useProjectsList();
|
||||
const { data: guestProjects } = useGuestProjectsList()
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const selectorProjects = computed(() => {
|
||||
const result: TProject[] = [];
|
||||
if (projects.value) result.push(...projects.value);
|
||||
if (guestProjects.value) result.push(...guestProjects.value);
|
||||
return result;
|
||||
});
|
||||
|
||||
const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
||||
headers: computed(() => {
|
||||
return {
|
||||
@@ -120,27 +104,13 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
||||
})
|
||||
});
|
||||
|
||||
const selected = ref<TProject>(activeProject.value as TProject);
|
||||
watch(selected, () => {
|
||||
setActiveProject(selected.value._id.toString())
|
||||
})
|
||||
|
||||
const isPremium = computed(() => {
|
||||
return activeProject.value?.premium;
|
||||
})
|
||||
|
||||
function isProjectMine(owner?: string) {
|
||||
if (!owner) return false;
|
||||
if (!loggedUser.value?.logged) return;
|
||||
return loggedUser.value.id == owner;
|
||||
}
|
||||
|
||||
const pricingDrawer = usePricingDrawer();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="CVerticalNavigation h-full w-[20rem] bg-lyx-background flex shadow-[1px_0_10px_#000000] rounded-r-lg"
|
||||
<div class="CVerticalNavigation border-solid border-[#202020] border-r-[1px] h-full w-[20rem] bg-lyx-background flex shadow-[1px_0_10px_#000000] rounded-r-lg"
|
||||
:class="{
|
||||
'absolute top-0 w-full md:w-[20rem] z-[45] open': isOpen,
|
||||
'hidden lg:flex': !isOpen
|
||||
@@ -158,36 +128,7 @@ const pricingDrawer = usePricingDrawer();
|
||||
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" class="w-full" v-if="selectorProjects" v-model="selected" :options="selectorProjects">
|
||||
|
||||
<template #option="{ option, active, selected }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||
</div>
|
||||
<div> {{ option.name }} {{ !isProjectMine(option.owner) ? '(Guest)' : '' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||
</div>
|
||||
<div>
|
||||
{{ activeProject?.name || '-' }}
|
||||
{{ !isProjectMine(activeProject?.owner?.toString()) ? '(Guest)' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
<ProjectSelector></ProjectSelector>
|
||||
|
||||
<div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden">
|
||||
<i @click="close()" class="fas fa-close"></i>
|
||||
@@ -196,11 +137,22 @@ const pricingDrawer = usePricingDrawer();
|
||||
</div>
|
||||
|
||||
|
||||
<NuxtLink to="/project_creation" v-if="projects && (projects.length < (maxProjects || 1))"
|
||||
class="flex items-center text-[.8rem] gap-1 justify-end pt-2 pr-2 text-lyx-text-dark hover:text-lyx-text cursor-pointer">
|
||||
<div><i class="fas fa-plus"></i></div>
|
||||
<div> Create new project </div>
|
||||
</NuxtLink>
|
||||
<LyxUiButton to="/project_creation" v-if="projectList && (projectList.length < (maxProjects || 1))"
|
||||
type="outlined" class="w-full py-1 mt-2 text-[.8rem]">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<div><i class="fas fa-plus"></i></div>
|
||||
<div> Create new project </div>
|
||||
</div>
|
||||
</LyxUiButton>
|
||||
|
||||
<LyxUiButton v-if="projectList && (projectList.length >= (maxProjects || 1))" type="outlined"
|
||||
class="w-full py-1 mt-2 text-[.7rem]">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<div><i class="text-lyx-text-darker far fa-lock"></i></div>
|
||||
<div class="text-lyx-text-darker"> Projects limit reached </div>
|
||||
</div>
|
||||
</LyxUiButton>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -211,52 +163,59 @@ const pricingDrawer = usePricingDrawer();
|
||||
<div class="poppins text-[.8rem]">
|
||||
Snapshots
|
||||
</div>
|
||||
<div @click="openSnapshotDialog()"
|
||||
class="poppins text-[.8rem] px-2 rounded-lg outline outline-[2px] outline-lyx-widget-lighter cursor-pointer hover:bg-lyx-widget-lighter">
|
||||
<i class="far fa-plus"></i>
|
||||
Add
|
||||
|
||||
<div class="flex gap-2">
|
||||
<UTooltip text="Download report">
|
||||
<LyxUiButton @click="generatePDF()" type="outlined" class="!px-3 !py-1">
|
||||
<div><i class="far fa-download text-[.8rem]"></i></div>
|
||||
</LyxUiButton>
|
||||
</UTooltip>
|
||||
<UTooltip text="Create new snapshot">
|
||||
<LyxUiButton @click="openSnapshotDialog()" type="outlined" class="!px-3 !py-1">
|
||||
<div><i class="fas fa-plus text-[.9rem]"></i></div>
|
||||
</LyxUiButton>
|
||||
</UTooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" class="w-full" v-model="snapshot" :options="snapshotsItems">
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div :style="'background-color:' + snapshot?.color" class="w-2 h-2 rounded-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" class="w-full" v-model="snapshot" :options="snapshotsItems">
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div :style="'background-color:' + snapshot?.color" class="w-2 h-2 rounded-full">
|
||||
</div>
|
||||
<div class="poppins"> {{ snapshot?.name }} </div>
|
||||
</div>
|
||||
<div class="poppins"> {{ snapshot?.name }} </div>
|
||||
</div>
|
||||
</template>
|
||||
<template #option="{ option }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div :style="'background-color:' + option.color" class="w-2 h-2 rounded-full">
|
||||
</template>
|
||||
<template #option="{ option }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div :style="'background-color:' + option.color" class="w-2 h-2 rounded-full">
|
||||
</div>
|
||||
<div class="poppins"> {{ option.name }} </div>
|
||||
</div>
|
||||
<div class="poppins"> {{ option.name }} </div>
|
||||
</div>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</div>
|
||||
|
||||
<div v-if="snapshot" class="flex flex-col text-[.8rem] mt-2">
|
||||
<div class="flex">
|
||||
<div class="grow poppins"> From:</div>
|
||||
<div class="poppins"> {{ new Date(snapshot.from).toLocaleString('it-IT').split(',')[0].trim() }}
|
||||
<div v-if="snapshot" class="flex flex-col text-[.7rem] mt-2">
|
||||
<div class="flex gap-1 items-center justify-center text-lyx-text-dark">
|
||||
<div class="poppins">
|
||||
{{ new Date(snapshot.from).toLocaleString().split(',')[0].trim()
|
||||
}}
|
||||
</div>
|
||||
<div class="poppins"> to </div>
|
||||
<div class="poppins">
|
||||
{{ new Date(snapshot.to).toLocaleString().split(',')[0].trim() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="grow poppins"> To:</div>
|
||||
<div class="poppins"> {{ new Date(snapshot.to).toLocaleString('it-IT').split(',')[0].trim() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LyxUiButton @click="generatePDF()" type="secondary" class="w-full text-center mt-4">
|
||||
Download report
|
||||
</LyxUiButton>
|
||||
|
||||
<div class="mt-2" v-if="snapshot._id.toString().startsWith('default') === false">
|
||||
<UPopover placement="bottom">
|
||||
@@ -281,7 +240,7 @@ const pricingDrawer = usePricingDrawer();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-lyx-widget-lighter h-[2px] w-full"></div>
|
||||
<div class="bg-[#202020] h-[1px] w-full"></div>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
|
||||
@@ -289,8 +248,8 @@ const pricingDrawer = usePricingDrawer();
|
||||
|
||||
<div v-for="entry of section.entries" :class="{ 'grow flex items-end': entry.grow }">
|
||||
|
||||
<div v-if="(!entry.adminOnly || (isAdmin && !isAdminHidden))"
|
||||
class="bg-lyx-background cursor-pointer text-lyx-text-dark py-[.35rem] px-2 rounded-lg text-[.95rem] flex items-center"
|
||||
<div v-if="(!entry.adminOnly || (userRoles.isAdmin.value && !isAdminHidden))"
|
||||
class="bg-lyx-background w-full cursor-pointer text-lyx-text-dark py-[.35rem] px-2 rounded-lg text-[.95rem] flex items-center"
|
||||
:class="{
|
||||
'!text-lyx-text-darker pointer-events-none': entry.disabled,
|
||||
'bg-lyx-background-lighter !text-lyx-text/90': route.path == (entry.to || '#'),
|
||||
@@ -305,7 +264,7 @@ const pricingDrawer = usePricingDrawer();
|
||||
<div class="manrope grow">
|
||||
{{ entry.label }}
|
||||
</div>
|
||||
<div v-if="entry.premiumOnly && !isPremium" class="flex items-center">
|
||||
<div v-if="entry.premiumOnly && !userRoles.isPremium.value" class="flex items-center">
|
||||
<i class="fal fa-lock"></i>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
@@ -317,27 +276,30 @@ const pricingDrawer = usePricingDrawer();
|
||||
</div>
|
||||
|
||||
<div class="grow"></div>
|
||||
<div class="bg-lyx-widget-lighter h-[2px] px-4 w-full mb-3"></div>
|
||||
|
||||
<div class="bg-[#202020] h-[1px] w-full px-4 mb-3"></div>
|
||||
|
||||
<div class="flex justify-end px-2">
|
||||
|
||||
<div class="grow flex gap-3">
|
||||
<NuxtLink to="https://github.com/litlyx/litlyx" target="_blank"
|
||||
<!-- <NuxtLink to="https://github.com/litlyx/litlyx" target="_blank"
|
||||
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
||||
<i class="fab fa-github"></i>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="https://discord.gg/9cQykjsmWX" target="_blank"
|
||||
</NuxtLink> -->
|
||||
<!-- <NuxtLink to="https://discord.gg/9cQykjsmWX" target="_blank"
|
||||
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
||||
<i class="fab fa-discord"></i>
|
||||
</NuxtLink>
|
||||
</NuxtLink> -->
|
||||
<NuxtLink to="https://x.com/litlyx" target="_blank"
|
||||
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
||||
<i class="fab fa-x-twitter"></i>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="https://dev.to/litlyx-org" target="_blank"
|
||||
<!-- <NuxtLink to="https://dev.to/litlyx-org" target="_blank"
|
||||
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
||||
<i class="fab fa-dev"></i>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/admin" v-if="isAdmin"
|
||||
</NuxtLink> -->
|
||||
|
||||
<NuxtLink to="/admin" v-if="userRoles.isAdmin.value"
|
||||
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
||||
<i class="fas fa-cat"></i>
|
||||
</NuxtLink>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const { project } = useProject();
|
||||
const { createAlert } = useAlert();
|
||||
|
||||
import 'highlight.js/styles/stackoverflow-dark.css';
|
||||
@@ -18,7 +18,7 @@ onMounted(() => {
|
||||
|
||||
function copyProjectId() {
|
||||
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
||||
navigator.clipboard.writeText(activeProject.value?._id?.toString() || '');
|
||||
navigator.clipboard.writeText(project.value?._id?.toString() || '');
|
||||
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ function copyScript() {
|
||||
const createScriptText = () => {
|
||||
return [
|
||||
'<script defer ',
|
||||
`data-project="${activeProject.value?._id}" `,
|
||||
`data-project="${project.value?._id}" `,
|
||||
'src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></',
|
||||
'script>'
|
||||
].join('')
|
||||
@@ -43,7 +43,7 @@ function copyScript() {
|
||||
|
||||
const scriptText = computed(() => {
|
||||
return [
|
||||
`<script defer data-project="${activeProject.value?._id.toString()}"`,
|
||||
`<script defer data-project="${project.value?._id.toString()}"`,
|
||||
`\nsrc="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js">\n<`,
|
||||
`/script>`
|
||||
].join('');
|
||||
@@ -57,7 +57,7 @@ function reloadPage() {
|
||||
|
||||
<template>
|
||||
|
||||
<div v-if="!firstInteraction && activeProject" class="mt-[5vh] flex flex-col">
|
||||
<div v-if="!firstInteraction && project" class="mt-[5vh] flex flex-col">
|
||||
|
||||
<div class="flex gap-4 items-center justify-center">
|
||||
<div class="animate-pulse w-[1.5rem] h-[1.5rem] bg-accent rounded-full"> </div>
|
||||
@@ -76,26 +76,32 @@ function reloadPage() {
|
||||
|
||||
<div class="flex items-center justify-center mt-10">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex gap-6">
|
||||
<div>
|
||||
<CardTitled class="h-full" title="Tutorial" sub="Coming soon. For now enjoy our launch video.">
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<iframe width="560" height="315"
|
||||
|
||||
<div class="flex gap-6 xl:flex-row flex-col">
|
||||
|
||||
<div class="h-full w-full">
|
||||
<CardTitled class="h-full w-full xl:min-w-[500px]" title="Tutorial"
|
||||
sub="Coming soon. For now enjoy our launch video.">
|
||||
|
||||
<div class="flex items-center justify-center h-full w-full">
|
||||
|
||||
<iframe class="w-full h-full min-h-[400px]"
|
||||
src="https://www.youtube.com/embed/GntyWMR7jsY?si=YGGkQwrk6-Iqmn8w" title="Litlyx"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
</CardTitled>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<CardTitled title="Quick Integration"
|
||||
sub="Start tracking web analytics in one line. (works everywhere js is supported)">
|
||||
<div class="flex flex-col items-end gap-4">
|
||||
<div class="w-full">
|
||||
<div class="w-full xl:text-[1rem] text-[.8rem]">
|
||||
<pre><code class="language-html">{{ scriptText }}</code></pre>
|
||||
</div>
|
||||
<LyxUiButton type="secondary" @click="copyScript()">
|
||||
@@ -109,7 +115,7 @@ function reloadPage() {
|
||||
<CardTitled class="h-full w-full" title="Project id"
|
||||
sub="This is the identifier for this project, used to forward data">
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="w-full text-[.9rem] text-[#acacac]"> {{ activeProject?._id }} </div>
|
||||
<div class="w-full text-[.9rem] text-[#acacac]"> {{ project?._id }} </div>
|
||||
<LyxUiButton type="secondary" @click="copyProjectId()"> Copy </LyxUiButton>
|
||||
</div>
|
||||
</CardTitled>
|
||||
@@ -122,7 +128,7 @@ function reloadPage() {
|
||||
<CardTitled class="w-full h-full" title="Documentation"
|
||||
sub="Learn how to use Litlyx in every tech stack">
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="flex justify-center w-full">
|
||||
<div class="justify-center w-full hidden xl:flex">
|
||||
<svg width="680" height="100" viewBox="0 0 680 100" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="path-1-inside-1_473_1361" fill="white">
|
||||
@@ -263,7 +269,7 @@ function reloadPage() {
|
||||
|
||||
|
||||
|
||||
<!-- <div class="flex justify-center gap-10 flex-col lg:flex-row items-center lg:items-stretch px-10">
|
||||
<!-- <div class="flex justify-center gap-10 flex-col xl:flex-row items-center xl: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>
|
||||
@@ -273,7 +279,7 @@ function reloadPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-menu p-6 rounded-xl flex flex-col gap-2 w-full lg:max-w-[40vw]">
|
||||
<div class="bg-menu p-6 rounded-xl flex flex-col gap-2 w-full xl:max-w-[40vw]">
|
||||
<div class="poppins font-semibold">
|
||||
Start logging visits in 1 click | Plug anywhere !
|
||||
</div>
|
||||
|
||||
55
dashboard/components/ProjectSelector.vue
Normal file
55
dashboard/components/ProjectSelector.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type { TProject } from '@schema/ProjectSchema';
|
||||
|
||||
const { user } = useLoggedUser()
|
||||
|
||||
const { projectList, guestProjectList,allProjectList, actions, project } = useProject();
|
||||
|
||||
|
||||
function isProjectMine(owner?: string) {
|
||||
if (!owner) return false;
|
||||
if (!user.value) return false;
|
||||
if (!user.value.logged) return;
|
||||
return user.value.id == owner;
|
||||
}
|
||||
|
||||
function onChange(e: TProject) {
|
||||
actions.setActiveProject(e._id.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" class="w-full" v-if="allProjectList" @change="onChange" :value="project" :options="allProjectList">
|
||||
|
||||
<template #option="{ option, active, selected }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||
</div>
|
||||
<div> {{ option.name }} {{ !isProjectMine(option.owner) ? '(Guest)' : '' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||
</div>
|
||||
<div>
|
||||
{{ project?.name || '-' }}
|
||||
{{ !isProjectMine(project?.owner?.toString()) ? '(Guest)' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
|
||||
</template>
|
||||
33
dashboard/components/banner/LimitsInfo.vue
Normal file
33
dashboard/components/banner/LimitsInfo.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
const limitsInfo = await useFetch("/api/project/limits_info", {
|
||||
lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
|
||||
});
|
||||
|
||||
const pricingDrawer = usePricingDrawer();
|
||||
|
||||
function goToUpgrade() {
|
||||
pricingDrawer.visible.value = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div v-if="limitsInfo.data.value && limitsInfo.data.value.limited"
|
||||
class="w-full bg-[#fbbf2422] p-4 rounded-lg text-[.9rem] flex items-center">
|
||||
<div class="flex flex-col grow">
|
||||
<div class="poppins font-semibold text-[#fbbf24]">
|
||||
Limit reached
|
||||
</div>
|
||||
<div class="poppins text-[#fbbf24]">
|
||||
Litlyx cannot receive new data as you reached your plan's limit. Resume all the great
|
||||
features and collect even more data with a higher plan.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
38
dashboard/components/banner/Offer.vue
Normal file
38
dashboard/components/banner/Offer.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
const pricingDrawer = usePricingDrawer();
|
||||
|
||||
function goToUpgrade() {
|
||||
pricingDrawer.visible.value = true;
|
||||
}
|
||||
|
||||
const { project } = useProject()
|
||||
|
||||
const isPremium = computed(() => {
|
||||
return project.value?.premium ?? false;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div v-if="!isPremium" class="w-full bg-[#5680f822] p-4 rounded-lg text-[.9rem] flex items-center">
|
||||
<div class="flex flex-col grow">
|
||||
<div class="poppins font-semibold text-lyx-primary">
|
||||
Launch offer: 25% off forever with code <span class="text-white font-bold text-[1rem]">LIT25</span> at checkout
|
||||
from Acceleration Plan and beyond.
|
||||
</div>
|
||||
<!-- <div class="poppins text-lyx-primary">
|
||||
We're offering an exclusive 25% discount forever on all plans starting from the Acceleration
|
||||
Plan for our first 100 users who believe in our project.
|
||||
<br>
|
||||
Redeem Code: <span class="text-white font-bold text-[1rem]">LIT25</span> at checkout to
|
||||
claim your discount.
|
||||
</div> -->
|
||||
</div>
|
||||
<div>
|
||||
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,12 +3,8 @@ import { onMounted } from 'vue';
|
||||
import DateService, { type Slice } from '@services/DateService';
|
||||
import type { ChartData, ChartOptions, TooltipModel } from 'chart.js';
|
||||
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||
registerChartComponents();
|
||||
|
||||
const errorData = ref<{ errored: boolean, text: string }>({
|
||||
errored: false,
|
||||
text: ''
|
||||
})
|
||||
const errorData = ref<{ errored: boolean, text: string }>({ errored: false, text: '' })
|
||||
|
||||
const chartOptions = ref<ChartOptions<'line'>>({
|
||||
responsive: true,
|
||||
@@ -102,7 +98,6 @@ const chartData = ref<ChartData<'line' | 'bar' | 'bubble'>>({
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
const { lineChartProps, lineChartRef, update: updateChart } = useLineChart({ chartData: (chartData as any), options: chartOptions });
|
||||
|
||||
const externalTooltipElement = ref<null | HTMLDivElement>(null);
|
||||
@@ -123,29 +118,30 @@ function externalTooltipHandler(context: { chart: any, tooltip: TooltipModel<'li
|
||||
return;
|
||||
}
|
||||
const { left: positionX, top: positionY } = chart.canvas.getBoundingClientRect();
|
||||
|
||||
|
||||
const xSwap = tooltip.caretX > (window.innerWidth * 0.5) ? -450 : -100;
|
||||
|
||||
tooltipEl.style.opacity = '1';
|
||||
tooltipEl.style.left = positionX + tooltip.caretX + 'px';
|
||||
|
||||
tooltipEl.style.left = positionX + (tooltip.caretX + xSwap) + 'px';
|
||||
|
||||
tooltipEl.style.top = positionY + tooltip.caretY + 'px';
|
||||
tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
|
||||
|
||||
}
|
||||
|
||||
|
||||
const selectLabels: { label: string, value: Slice }[] = [
|
||||
{ label: 'Hour', value: 'hour' },
|
||||
{ label: 'Day', value: 'day' },
|
||||
{ label: 'Month', value: 'month' },
|
||||
];
|
||||
|
||||
const selectedSlice = computed(() => selectLabels[selectedLabelIndex.value].value);
|
||||
|
||||
const selectedLabelIndex = ref<number>(1);
|
||||
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const allDatesFull = ref<string[]>([]);
|
||||
|
||||
|
||||
function transformResponse(input: { _id: string, count: number }[]) {
|
||||
const data = input.map(e => e.count);
|
||||
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, selectLabels[selectedLabelIndex.value].value));
|
||||
@@ -153,50 +149,31 @@ function transformResponse(input: { _id: string, count: number }[]) {
|
||||
return { data, labels }
|
||||
}
|
||||
|
||||
const body = computed(() => {
|
||||
return {
|
||||
from: safeSnapshotDates.value.from,
|
||||
to: safeSnapshotDates.value.to,
|
||||
slice: selectLabels[selectedLabelIndex.value].value
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function onResponseError(e: any) {
|
||||
console.log('ON RESPONSE ERROR')
|
||||
errorData.value = { errored: true, text: e.response._data.message ?? 'Generic error' }
|
||||
}
|
||||
|
||||
function onResponse(e: any) {
|
||||
console.log('ON RESPONSE')
|
||||
if (e.response.status != 500) errorData.value = { errored: false, text: '' }
|
||||
}
|
||||
|
||||
const visitsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/visits`, {
|
||||
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse,
|
||||
lazy: true, immediate: false,
|
||||
onResponseError,
|
||||
onResponse
|
||||
|
||||
const visitsData = useFetch('/api/timeline/visits', {
|
||||
headers: useComputedHeaders({ slice: selectedSlice }), lazy: true,
|
||||
transform: transformResponse, onResponseError, onResponse
|
||||
});
|
||||
|
||||
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events`, {
|
||||
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse,
|
||||
lazy: true, immediate: false,
|
||||
onResponseError,
|
||||
onResponse
|
||||
const sessionsData = useFetch('/api/timeline/sessions', {
|
||||
headers: useComputedHeaders({ slice: selectedSlice }), lazy: true,
|
||||
transform: transformResponse, onResponseError, onResponse
|
||||
});
|
||||
|
||||
const sessionsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions`, {
|
||||
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse,
|
||||
lazy: true, immediate: false,
|
||||
onResponseError,
|
||||
onResponse
|
||||
const eventsData = useFetch('/api/timeline/events', {
|
||||
headers: useComputedHeaders({ slice: selectedSlice }), lazy: true,
|
||||
transform: transformResponse, onResponseError, onResponse
|
||||
});
|
||||
|
||||
|
||||
const readyToDisplay = computed(() => {
|
||||
return !visitsData.pending.value && !eventsData.pending.value && !sessionsData.pending.value;
|
||||
});
|
||||
const readyToDisplay = computed(() => !visitsData.pending.value && !eventsData.pending.value && !sessionsData.pending.value);
|
||||
|
||||
watch(readyToDisplay, () => {
|
||||
if (readyToDisplay.value === true) onDataReady();
|
||||
@@ -220,14 +197,10 @@ function createGradient(startColor: string) {
|
||||
}
|
||||
|
||||
function onDataReady() {
|
||||
console.log('DATA READY');
|
||||
|
||||
if (!visitsData.data.value) return;
|
||||
if (!eventsData.data.value) return;
|
||||
if (!sessionsData.data.value) return;
|
||||
|
||||
console.log('DATA READY 2');
|
||||
|
||||
chartData.value.labels = visitsData.data.value.labels;
|
||||
|
||||
const maxChartY = Math.max(...visitsData.data.value.data, ...sessionsData.data.value.data);
|
||||
@@ -244,9 +217,7 @@ function onDataReady() {
|
||||
chartData.value.datasets[1].backgroundColor = [createGradient('#4abde8')];
|
||||
chartData.value.datasets[2].backgroundColor = [createGradient('#fbbf24')];
|
||||
|
||||
console.log('UPDATE CHART');
|
||||
updateChart();
|
||||
|
||||
}
|
||||
|
||||
const currentTooltipData = ref<{ visits: number, events: number, sessions: number, date: string }>({
|
||||
@@ -262,22 +233,14 @@ function onLegendChange(dataset: any, index: number, checked: any) {
|
||||
dataset.hidden = !checked;
|
||||
}
|
||||
|
||||
const legendColors = [
|
||||
'#5655d7',
|
||||
'#4abde8',
|
||||
'#fbbf24'
|
||||
]
|
||||
const legendColors = ref<string[]>(['#5655d7', '#4abde8', '#fbbf24'])
|
||||
const legendClasses = ref<string[]>([
|
||||
'actionable-visits-color-checkbox',
|
||||
'actionable-sessions-color-checkbox',
|
||||
'actionable-events-color-checkbox'
|
||||
])
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
visitsData.execute();
|
||||
eventsData.execute();
|
||||
sessionsData.execute();
|
||||
});
|
||||
|
||||
|
||||
const inLiveDemo = isLiveDemo();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -288,8 +251,8 @@ const inLiveDemo = isLiveDemo();
|
||||
</SelectButton>
|
||||
</template>
|
||||
|
||||
<div class="flex gap-6 w-full justify-between">
|
||||
<LyxUiButton type="secondary" :to="inLiveDemo ? '#' : '/analyst'" :disabled="inLiveDemo">
|
||||
<div class="flex gap-6 w-full justify-between lg:flex-row flex-col">
|
||||
<LyxUiButton type="secondary" :to="isLiveDemo ? '#' : '/analyst'" :disabled="isLiveDemo">
|
||||
<div class="flex items-center gap-2 px-10">
|
||||
<i class="far fa-sparkles text-yellow-400"></i>
|
||||
<div class="poppins text-lyx-text"> Ask AI </div>
|
||||
@@ -297,9 +260,11 @@ const inLiveDemo = isLiveDemo();
|
||||
</LyxUiButton>
|
||||
<div class="flex gap-6">
|
||||
<div v-for="(dataset, index) of chartData.datasets" class="flex gap-2 items-center text-[.9rem]">
|
||||
|
||||
<UCheckbox :ui="{
|
||||
color: `text-[${legendColors[index]}]`
|
||||
color: legendClasses[index]
|
||||
}" :model-value="true" @change="onLegendChange(dataset, index, $event)"></UCheckbox>
|
||||
|
||||
<label class="mt-[2px]"> {{ dataset.label }} </label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const isShowMore = ref<boolean>(false);
|
||||
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
Authorization: authorizationHeaderComputed.value,
|
||||
limit: isShowMore.value === true ? '200' : '10'
|
||||
}
|
||||
});
|
||||
|
||||
const browsersData = useFetch(`/api/metrics/${activeProject.value?._id}/data/browsers`, {
|
||||
method: 'POST', headers, lazy: true, immediate: false
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
|
||||
function showMore() {
|
||||
isShowMore.value = true;
|
||||
showDialog.value = true;
|
||||
dialogBarData.value = browsersData.data.value || [];
|
||||
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
browsersData.execute();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<DashboardBarsCard @showMore="showMore()" @dataReload="browsersData.refresh()"
|
||||
:data="browsersData.data.value || []" desc="The browsers most used to search your website."
|
||||
:dataIcons="false" :loading="browsersData.pending.value" label="Top Browsers" sub-label="Browsers">
|
||||
</DashboardBarsCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,7 +9,8 @@ const props = defineProps<{
|
||||
color: string,
|
||||
data?: number[],
|
||||
labels?: string[],
|
||||
ready?: boolean
|
||||
ready?: boolean,
|
||||
slow?: boolean
|
||||
}>();
|
||||
|
||||
const { snapshotDuration } = useSnapshot()
|
||||
@@ -29,18 +30,20 @@ const uTooltipText = computed(() => {
|
||||
<LyxUiCard class="flex !p-0 flex-col overflow-hidden relative max-h-[12rem] aspect-[2/1] w-full">
|
||||
<div v-if="ready" class="flex p-4 items-start">
|
||||
<div class="flex items-center mt-2 mr-4">
|
||||
<i :style="`color: ${props.color}`" :class="icon" class="text-[1.6rem] 2xl:text-[2rem]"></i>
|
||||
<i :style="`color: ${props.color}`" :class="icon" class="text-[1.3rem] 2xl:text-[1.5rem]"></i>
|
||||
</div>
|
||||
<div class="flex flex-col grow">
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="brockmann text-text-dirty text-[1.6rem] 2xl:text-[1.9rem]"> {{ value }} </div>
|
||||
<div class="poppins text-text-sub text-[.7rem] 2xl:text-[.85rem] mb-2"> {{ avg }} </div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="brockmann text-text-dirty text-[1.2rem] 2xl:text-[1.4rem]">
|
||||
{{ value }}
|
||||
</div>
|
||||
<div class="poppins text-text-sub text-[.65rem] 2xl:text-[.8rem]"> {{ avg }} </div>
|
||||
</div>
|
||||
<div class="poppins text-text-sub text-[.9rem] 2xl:text-base"> {{ text }} </div>
|
||||
<div class="poppins text-text-sub text-[.9rem] 2xl:text-[1rem]"> {{ text }} </div>
|
||||
</div>
|
||||
<div v-if="trend" class="flex flex-col items-center gap-1">
|
||||
<UTooltip :text="uTooltipText">
|
||||
<div class="flex items-center gap-3 rounded-xl px-2 py-1"
|
||||
<div class="flex items-center gap-3 rounded-md px-2 py-1"
|
||||
:style="`background-color: ${props.color}33`">
|
||||
<i :class="trend > 0 ? 'fa-arrow-trend-up' : 'fa-arrow-trend-down'"
|
||||
class="far text-[.9rem] 2xl:text-[1rem]" :style="`color: ${props.color}`"></i>
|
||||
@@ -59,8 +62,9 @@ const uTooltipText = computed(() => {
|
||||
:color="props.color">
|
||||
</DashboardEmbedChartCard>
|
||||
</div>
|
||||
<div v-if="!ready" class="flex justify-center items-center w-full h-full">
|
||||
<div v-if="!ready" class="flex justify-center items-center w-full h-full flex-col gap-2">
|
||||
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||
<div v-if="props.slow"> Can be very slow on large snapshots </div>
|
||||
</div>
|
||||
</LyxUiCard>
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const isShowMore = ref<boolean>(false);
|
||||
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
Authorization: authorizationHeaderComputed.value,
|
||||
limit: isShowMore.value === true ? '200' : '10'
|
||||
}
|
||||
});
|
||||
|
||||
const devicesData = useFetch(`/api/metrics/${activeProject.value?._id}/data/devices`, {
|
||||
method: 'POST', headers, lazy: true, immediate: false
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
|
||||
function showMore() {
|
||||
isShowMore.value = true;
|
||||
showDialog.value = true;
|
||||
dialogBarData.value = devicesData.data.value || [];
|
||||
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
devicesData.execute();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<DashboardBarsCard @showMore="showMore()" @dataReload="devicesData.refresh()" :data="devicesData.data.value || []" :dataIcons="false"
|
||||
desc="The devices most used to access your website." :loading="devicesData.pending.value" label="Top Devices"
|
||||
sub-label="Devices"></DashboardBarsCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,51 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goToView() {
|
||||
router.push('/dashboard/events');
|
||||
}
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const isShowMore = ref<boolean>(false);
|
||||
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
Authorization: authorizationHeaderComputed.value,
|
||||
limit: isShowMore.value === true ? '200' : '10'
|
||||
}
|
||||
});
|
||||
|
||||
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/data/events`, {
|
||||
method: 'POST', headers, lazy: true, immediate: false
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
function showMore() {
|
||||
isShowMore.value = true;
|
||||
showDialog.value = true;
|
||||
dialogBarData.value = eventsData.data.value || [];
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
eventsData.execute();
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<DashboardBarsCard @showMore="showMore()" @showRawData="goToView()"
|
||||
desc="Most frequent user events triggered in this project" @dataReload="eventsData.refresh()"
|
||||
:data="eventsData.data.value || []" :loading="eventsData.pending.value" label="Top Events"
|
||||
sub-label="Events" :rawButton="!isLiveDemo()"></DashboardBarsCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -77,7 +77,7 @@ const chartData = ref<ChartData<'doughnut'>>({
|
||||
|
||||
const { doughnutChartProps, doughnutChartRef } = useDoughnutChart({ chartData: chartData, options: chartOptions });
|
||||
|
||||
const activeProjectId = useActiveProjectId();
|
||||
const { projectId } = useProject();
|
||||
|
||||
const { safeSnapshotDates } = useSnapshot();
|
||||
|
||||
@@ -98,19 +98,8 @@ function transformResponse(input: CustomEventsAggregated[]) {
|
||||
}
|
||||
}
|
||||
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
'Authorization': authorizationHeaderComputed.value,
|
||||
'x-schema': 'events',
|
||||
'x-limit': "6",
|
||||
'x-pid': activeProjectId.data.value || ''
|
||||
}
|
||||
});
|
||||
|
||||
const eventsData = useFetch(`/api/data/query`, {
|
||||
method: 'POST', headers, lazy: true, immediate: false, transform: transformResponse
|
||||
const eventsData = useFetch(`/api/data/events`, {
|
||||
headers: useComputedHeaders({ limit: 6 }), lazy: true, immediate: false, transform: transformResponse
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type { IconProvider } from './BarsCard.vue';
|
||||
|
||||
function iconProvider(id: string): ReturnType<IconProvider> {
|
||||
if (id === 'self') return ['icon', 'fas fa-link'];
|
||||
return [
|
||||
'img',
|
||||
`https://raw.githubusercontent.com/hampusborgos/country-flags/main/png250px/${id.toLowerCase()}.png`
|
||||
]
|
||||
}
|
||||
|
||||
const customIconStyle = `width: 2rem; padding: 1px;`
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const isShowMore = ref<boolean>(false);
|
||||
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
Authorization: authorizationHeaderComputed.value,
|
||||
limit: isShowMore.value === true ? '200' : '10'
|
||||
}
|
||||
});
|
||||
|
||||
const geolocationData = useFetch(`/api/metrics/${activeProject.value?._id}/data/countries`, {
|
||||
method: 'POST', headers, lazy: true, immediate: false
|
||||
});
|
||||
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
function showMore() {
|
||||
|
||||
isShowMore.value = true;
|
||||
showDialog.value = true;
|
||||
|
||||
dialogBarData.value = geolocationData.data.value?.map(e => {
|
||||
return { ...e, icon: iconProvider(e._id) }
|
||||
}) || [];
|
||||
isDataLoading.value = false;
|
||||
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
geolocationData.execute();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<DashboardBarsCard @showMore="showMore()" @dataReload="geolocationData.refresh()" :data="geolocationData.data.value || []" :dataIcons="false"
|
||||
:loading="geolocationData.pending.value" label="Top Countries" sub-label="Countries" :iconProvider="iconProvider"
|
||||
:customIconStyle="customIconStyle" desc=" Lists the countries where users access your website.">
|
||||
</DashboardBarsCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,44 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const isShowMore = ref<boolean>(false);
|
||||
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
Authorization: authorizationHeaderComputed.value,
|
||||
limit: isShowMore.value === true ? '200' : '10'
|
||||
}
|
||||
});
|
||||
|
||||
const ossData = useFetch(`/api/metrics/${activeProject.value?._id}/data/oss`, {
|
||||
method: 'POST', headers, lazy: true, immediate: false
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
|
||||
function showMore() {
|
||||
isShowMore.value = true;
|
||||
showDialog.value = true;
|
||||
dialogBarData.value = ossData.data.value || [];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
ossData.execute();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<DashboardBarsCard @showMore="showMore()" @dataReload="ossData.refresh()" :data="ossData.data.value || []"
|
||||
desc="The operating systems most commonly used by your website's visitors." :dataIcons="false"
|
||||
:loading="ossData.pending.value" label="Top OS" sub-label="OSs"></DashboardBarsCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type { IconProvider } from './BarsCard.vue';
|
||||
import ReferrerBarChart from '../referrer/ReferrerBarChart.vue';
|
||||
|
||||
function iconProvider(id: string): ReturnType<IconProvider> {
|
||||
if (id === 'self') return ['icon', 'fas fa-link'];
|
||||
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${id}&sz=64`]
|
||||
}
|
||||
|
||||
function elementTextTransformer(element: string) {
|
||||
if (element === 'self') return 'Direct Link';
|
||||
return element;
|
||||
}
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const isShowMore = ref<boolean>(false);
|
||||
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
Authorization: authorizationHeaderComputed.value,
|
||||
limit: isShowMore.value === true ? '200' : '10'
|
||||
}
|
||||
});
|
||||
|
||||
const referrersData = useFetch(`/api/metrics/${activeProject.value?._id}/data/referrers`, {
|
||||
method: 'POST', headers, lazy: true, immediate: false
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
// const customDialog = useCustomDialog();
|
||||
|
||||
// function onShowDetails(referrer: string) {
|
||||
// customDialog.openDialog(ReferrerBarChart, { slice: 'day', referrer });
|
||||
// }
|
||||
|
||||
function showMore() {
|
||||
|
||||
isShowMore.value = true;
|
||||
showDialog.value = true;
|
||||
dialogBarData.value = referrersData.data.value?.map(e => {
|
||||
return { ...e, icon: iconProvider(e._id) }
|
||||
}) || [];
|
||||
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
referrersData.execute();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<DashboardBarsCard @showMore="showMore()"
|
||||
:elementTextTransformer="elementTextTransformer" :iconProvider="iconProvider"
|
||||
@dataReload="referrersData.refresh()" :showLink=true :data="referrersData.data.value || []"
|
||||
:interactive="false" desc="Where users find your website." :dataIcons="true" :loading="referrersData.pending.value"
|
||||
label="Top Referrers" sub-label="Referrers"></DashboardBarsCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,52 +3,14 @@
|
||||
import DateService from '@services/DateService';
|
||||
import type { Slice } from '@services/DateService';
|
||||
|
||||
const { data: metricsInfo } = useMetricsData();
|
||||
|
||||
const { snapshot, safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const snapshotFrom = computed(() => new Date(snapshot.value?.from || '0').getTime());
|
||||
const snapshotTo = computed(() => new Date(snapshot.value?.to || Date.now()).getTime());
|
||||
|
||||
const snapshotDays = computed(() => {
|
||||
return (snapshotTo.value - snapshotFrom.value) / 1000 / 60 / 60 / 24;
|
||||
const to = new Date(safeSnapshotDates.value.to).getTime();
|
||||
const from = new Date(safeSnapshotDates.value.from).getTime();
|
||||
return (to - from) / 1000 / 60 / 60 / 24;
|
||||
});
|
||||
|
||||
const avgVisitDay = computed(() => {
|
||||
if (!visitsData.data.value) return '0.00';
|
||||
const counts = visitsData.data.value.data.reduce((a, e) => e + a, 0);
|
||||
const avg = counts / Math.max(snapshotDays.value, 1);
|
||||
return avg.toFixed(2);
|
||||
});
|
||||
|
||||
const avgEventsDay = computed(() => {
|
||||
if (!eventsData.data.value) return '0.00';
|
||||
const counts = eventsData.data.value.data.reduce((a, e) => e + a, 0);
|
||||
const avg = counts / Math.max(snapshotDays.value, 1);
|
||||
return avg.toFixed(2);
|
||||
});
|
||||
|
||||
const avgSessionsDay = computed(() => {
|
||||
if (!sessionsData.data.value) return '0.00';
|
||||
const counts = sessionsData.data.value.data.reduce((a, e) => e + a, 0);
|
||||
const avg = counts / Math.max(snapshotDays.value, 1);
|
||||
return avg.toFixed(2);
|
||||
});
|
||||
|
||||
|
||||
const avgSessionDuration = computed(() => {
|
||||
if (!metricsInfo.value) return '0.00';
|
||||
const avg = metricsInfo.value.avgSessionDuration;
|
||||
let hours = 0;
|
||||
let minutes = 0;
|
||||
let seconds = 0;
|
||||
seconds += avg * 60;
|
||||
while (seconds > 60) { seconds -= 60; minutes += 1; }
|
||||
while (minutes > 60) { minutes -= 60; hours += 1; }
|
||||
return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s`
|
||||
});
|
||||
|
||||
|
||||
const chartSlice = computed(() => {
|
||||
const snapshotSizeMs = new Date(snapshot.value.to).getTime() - new Date(snapshot.value.from).getTime();
|
||||
if (snapshotSizeMs < 1000 * 60 * 60 * 24 * 6) return 'hour' as Slice;
|
||||
@@ -59,52 +21,80 @@ const chartSlice = computed(() => {
|
||||
|
||||
|
||||
function transformResponse(input: { _id: string, count: number }[]) {
|
||||
const data = input.map(e => e.count);
|
||||
|
||||
const data = input.map(e => e.count || 0);
|
||||
|
||||
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, chartSlice.value));
|
||||
const pool = [...input.map(e => e.count)];
|
||||
pool.pop();
|
||||
|
||||
const pool = [...input.map(e => e.count || 0)];
|
||||
|
||||
const avg = pool.reduce((a, e) => a + e, 0) / pool.length;
|
||||
const diffPercent: number = (100 / avg * (input.at(-1)?.count || 0)) - 100;
|
||||
|
||||
const targets = input.slice(Math.floor(input.length / 4 * 3));
|
||||
const targetAvg = targets.reduce((a, e) => a + e.count, 0) / targets.length;
|
||||
|
||||
const diffPercent: number = (100 / avg * (targetAvg)) - 100;
|
||||
|
||||
const trend = Math.max(Math.min(diffPercent, 99), -99);
|
||||
|
||||
return { data, labels, trend }
|
||||
|
||||
}
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
function getBody() {
|
||||
return JSON.stringify({
|
||||
from: safeSnapshotDates.value.from,
|
||||
to: safeSnapshotDates.value.to,
|
||||
slice: chartSlice.value
|
||||
});
|
||||
}
|
||||
|
||||
const visitsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/visits`, {
|
||||
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||
lazy: true, immediate: false
|
||||
const visitsData = useFetch('/api/timeline/visits', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
});
|
||||
const sessionsData = useFetch('/api/timeline/sessions', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
});
|
||||
const sessionsDurationData = useFetch('/api/timeline/sessions_duration', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
});
|
||||
const bouncingRateData = useFetch('/api/timeline/bouncing_rate', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
});
|
||||
|
||||
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events`, {
|
||||
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||
lazy: true, immediate: false
|
||||
const avgVisitDay = computed(() => {
|
||||
if (!visitsData.data.value) return '0.00';
|
||||
const counts = visitsData.data.value.data.reduce((a, e) => e + a, 0);
|
||||
const avg = counts / Math.max(snapshotDays.value, 1);
|
||||
return avg.toFixed(2);
|
||||
});
|
||||
|
||||
const sessionsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions`, {
|
||||
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||
lazy: true, immediate: false
|
||||
const avgSessionsDay = computed(() => {
|
||||
if (!sessionsData.data.value) return '0.00';
|
||||
const counts = sessionsData.data.value.data.reduce((a, e) => e + a, 0);
|
||||
const avg = counts / Math.max(snapshotDays.value, 1);
|
||||
return avg.toFixed(2);
|
||||
});
|
||||
|
||||
const sessionsDurationData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions_duration`, {
|
||||
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||
lazy: true, immediate: false
|
||||
});
|
||||
const avgBouncingRate = computed(() => {
|
||||
if (!bouncingRateData.data.value) return '0.00 %'
|
||||
|
||||
const counts = bouncingRateData.data.value.data
|
||||
.filter(e => e > 0)
|
||||
.reduce((a, e) => e + a, 0);
|
||||
|
||||
onMounted(async () => {
|
||||
visitsData.execute();
|
||||
eventsData.execute();
|
||||
sessionsData.execute();
|
||||
sessionsDurationData.execute();
|
||||
const avg = counts / Math.max(bouncingRateData.data.value.data.filter(e => e > 0).length, 1);
|
||||
return avg.toFixed(2) + ' %';
|
||||
})
|
||||
|
||||
const avgSessionDuration = computed(() => {
|
||||
if (!sessionsDurationData.data.value) return '0.00 %'
|
||||
|
||||
const counts = sessionsDurationData.data.value.data
|
||||
.filter(e => e > 0)
|
||||
.reduce((a, e) => e + a, 0);
|
||||
|
||||
const avg = counts / Math.max(sessionsDurationData.data.value.data.filter(e => e > 0).length, 1);
|
||||
|
||||
let hours = 0;
|
||||
let minutes = 0;
|
||||
let seconds = 0;
|
||||
seconds += avg * 60;
|
||||
while (seconds > 60) { seconds -= 60; minutes += 1; }
|
||||
while (minutes > 60) { minutes -= 60; hours += 1; }
|
||||
return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s`
|
||||
});
|
||||
|
||||
|
||||
@@ -120,10 +110,9 @@ onMounted(async () => {
|
||||
:data="visitsData.data.value?.data" :labels="visitsData.data.value?.labels" color="#5655d7">
|
||||
</DashboardCountCard>
|
||||
|
||||
<DashboardCountCard :ready="!eventsData.pending.value" icon="far fa-flag" text="Total custom events"
|
||||
:value="formatNumberK(eventsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
|
||||
:avg="formatNumberK(avgEventsDay) + '/day'" :trend="eventsData.data.value?.trend"
|
||||
:data="eventsData.data.value?.data" :labels="eventsData.data.value?.labels" color="#1e9b86">
|
||||
<DashboardCountCard :ready="!bouncingRateData.pending.value" icon="far fa-chart-user" text="Bouncing rate"
|
||||
:value="avgBouncingRate" :trend="bouncingRateData.data.value?.trend" :slow="true"
|
||||
:data="bouncingRateData.data.value?.data" :labels="bouncingRateData.data.value?.labels" color="#1e9b86">
|
||||
</DashboardCountCard>
|
||||
|
||||
|
||||
@@ -134,12 +123,12 @@ onMounted(async () => {
|
||||
</DashboardCountCard>
|
||||
|
||||
|
||||
<DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer" text="Total avg session time"
|
||||
:value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
|
||||
<DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer"
|
||||
text="Total avg session time" :value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
|
||||
:data="sessionsDurationData.data.value?.data" :labels="sessionsDurationData.data.value?.labels"
|
||||
color="#f56523">
|
||||
</DashboardCountCard>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const { project } = useProject();
|
||||
|
||||
const { onlineUsers, stopWatching, startWatching } = useOnlineUsers();
|
||||
onMounted(() => startWatching());
|
||||
onUnmounted(() => stopWatching());
|
||||
@@ -9,8 +11,9 @@ onUnmounted(() => stopWatching());
|
||||
const { createAlert } = useAlert();
|
||||
|
||||
function copyProjectId() {
|
||||
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
||||
navigator.clipboard.writeText((activeProject.value?._id || 0).toString());
|
||||
if (!navigator.clipboard) return alert('You can\'t copy in HTTP');
|
||||
if (!project.value) return alert('Project not loaded');
|
||||
navigator.clipboard.writeText((project.value._id).toString());
|
||||
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
||||
}
|
||||
|
||||
@@ -31,37 +34,37 @@ function showAnomalyInfoAlert() {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full px-6 py-2 lg:py-6 font-bold text-text-sub/40 flex flex-col xl:flex-row text-lg gap-2 xl:gap-12 lg:text-2xl">
|
||||
class="w-full px-6 pb-2 lg:pb-6 font-bold text-text-sub/40 flex flex-col xl:flex-row text-lg gap-2 xl:gap-12 lg:text-2xl">
|
||||
|
||||
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
|
||||
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
||||
<div class="poppins font-medium text-[1.2rem]"> {{ onlineUsers }} Online users</div>
|
||||
<div class="poppins font-medium text-[.9rem]"> {{ onlineUsers.data }} Online users</div>
|
||||
</div>
|
||||
|
||||
<div class="grow"></div>
|
||||
|
||||
<div class="flex md:gap-2 items-center md:justify-start flex-col md:flex-row">
|
||||
<div class="poppins font-medium text-lyx-text-darker text-[1.2rem]">Project:</div>
|
||||
<div class="text-lyx-text poppins font-medium text-[1.2rem]"> {{ activeProject?.name || 'Loading...' }}
|
||||
<!-- <div class="flex md:gap-2 items-center md:justify-start flex-col md:flex-row">
|
||||
<div class="poppins font-medium text-lyx-text-darker text-[.9rem]">Project:</div>
|
||||
<div class="text-lyx-text poppins font-medium text-[.9rem]"> {{ project?.name || 'Loading...' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row md:gap-2 items-center md:justify-start">
|
||||
<div class="poppins font-medium text-lyx-text-darker text-[1.2rem]">Project id:</div>
|
||||
<div class="poppins font-medium text-lyx-text-darker text-[.9rem]">Project id:</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="text-lyx-text poppins font-medium text-[1.2rem]">
|
||||
{{ activeProject?._id || 'Loading...' }}
|
||||
<div class="text-lyx-text poppins font-medium text-[.9rem]">
|
||||
{{ project?._id || 'Loading...' }}
|
||||
</div>
|
||||
<div class="flex items-center ml-3">
|
||||
<i @click="copyProjectId()"
|
||||
class="far fa-copy text-lyx-text hover:text-lyx-primary cursor-pointer text-[1.2rem]"></i>
|
||||
class="far fa-copy text-lyx-text hover:text-lyx-primary cursor-pointer text-[.9rem]"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
|
||||
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
||||
<div class="poppins font-regular text-[1rem]"> AI Anomaly Detector </div>
|
||||
<div class="poppins font-regular text-[.9rem]"> AI Anomaly Detector </div>
|
||||
<div class="flex items-center">
|
||||
<i class="far fa-info-circle text-[.9rem] hover:text-lyx-primary cursor-pointer"
|
||||
@click="showAnomalyInfoAlert"></i>
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type { VisitsWebsiteAggregated } from '~/server/api/metrics/[project_id]/data/websites';
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const isShowMore = ref<boolean>(false);
|
||||
|
||||
const currentWebsite = ref<string>("");
|
||||
|
||||
const websitesHeaders = computed(() => {
|
||||
return {
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
Authorization: authorizationHeaderComputed.value,
|
||||
limit: isShowMore.value === true ? '200' : '10'
|
||||
}
|
||||
});
|
||||
|
||||
const pagesHeaders = computed(() => {
|
||||
return {
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
Authorization: authorizationHeaderComputed.value,
|
||||
limit: isShowMore.value === true ? '200' : '10',
|
||||
'x-website-name': currentWebsite.value
|
||||
}
|
||||
});
|
||||
|
||||
const websitesData = useFetch(`/api/metrics/${activeProject.value?._id}/data/websites`, {
|
||||
method: 'POST', headers: websitesHeaders, lazy: true, immediate: false
|
||||
});
|
||||
|
||||
const pagesData = useFetch(`/api/metrics/${activeProject.value?._id}/data/pages`, {
|
||||
method: 'POST', headers: pagesHeaders, lazy: true, immediate: false
|
||||
});
|
||||
|
||||
const isPagesView = ref<boolean>(false);
|
||||
|
||||
const currentData = computed(() => {
|
||||
return isPagesView.value ? pagesData : websitesData
|
||||
})
|
||||
|
||||
|
||||
async function showDetails(website: string) {
|
||||
currentWebsite.value = website;
|
||||
pagesData.execute();
|
||||
isPagesView.value = true;
|
||||
}
|
||||
|
||||
async function showGeneral() {
|
||||
websitesData.execute();
|
||||
isPagesView.value = false;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goToView() {
|
||||
router.push('/dashboard/visits');
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
websitesData.execute();
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<DashboardBarsCard :hideShowMore="true" @showGeneral="showGeneral()" @showRawData="goToView()"
|
||||
@dataReload="currentData.refresh()" @showDetails="showDetails" :data="currentData.data.value || []"
|
||||
:loading="currentData.pending.value" :label="isPagesView ? 'Top pages' : 'Top Websites'"
|
||||
:sub-label="isPagesView ? 'Page' : 'Website'"
|
||||
:desc="isPagesView ? 'Most visited pages' : 'Most visited website in this project'"
|
||||
:interactive="!isPagesView" :rawButton="!isLiveDemo()" :isDetailView="isPagesView">
|
||||
</DashboardBarsCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -42,7 +42,7 @@ const { createAlert } = useAlert()
|
||||
async function confirmSnapshot() {
|
||||
await $fetch("/api/snapshot/create", {
|
||||
method: 'POST',
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value,
|
||||
body: JSON.stringify({
|
||||
name: snapshotName.value,
|
||||
color: currentColor.value,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChartData, ChartOptions } from 'chart.js';
|
||||
import { defineChartComponent } from 'vue-chart-3';
|
||||
registerChartComponents();
|
||||
|
||||
const FunnelChart = defineChartComponent('funnel', 'funnel');
|
||||
|
||||
@@ -57,14 +56,35 @@ const chartData = ref<ChartData<'funnel'>>({
|
||||
datasets: [
|
||||
{
|
||||
data: [],
|
||||
backgroundColor: ['#5680F8' + '77'],
|
||||
backgroundColor: [
|
||||
'#5680F877',
|
||||
"#6bbbe377",
|
||||
"#a6d5cb77",
|
||||
"#fae0b977",
|
||||
"#f28e8e77",
|
||||
"#e3a7e477",
|
||||
"#c4a8e177",
|
||||
"#8cc1d877",
|
||||
"#f9c2cd77",
|
||||
"#b4e3b277",
|
||||
"#ffdfba77",
|
||||
"#e9c3b577",
|
||||
"#d5b8d677",
|
||||
"#add7f677",
|
||||
"#ffd1dc77",
|
||||
"#ffe7a177",
|
||||
"#a8e6cf77",
|
||||
"#d4a5a577",
|
||||
"#f3d6e477",
|
||||
"#c3aed677"
|
||||
],
|
||||
// borderColor: '#0000CC',
|
||||
// borderWidth: 4,
|
||||
fill: true,
|
||||
tension: 0.45,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
hoverBackgroundColor: '#5680F8',
|
||||
hoverBackgroundColor: '#26262677',
|
||||
// hoverBorderColor: 'white',
|
||||
// hoverBorderWidth: 2,
|
||||
},
|
||||
@@ -90,20 +110,10 @@ onMounted(async () => {
|
||||
|
||||
});
|
||||
|
||||
const activeProjectId = useActiveProjectId();
|
||||
const { safeSnapshotDates } = useSnapshot();
|
||||
|
||||
const eventsCount = await useFetch<{ _id: string, count: number }[]>(`/api/data/query`, {
|
||||
...signHeaders({
|
||||
'x-pid': activeProjectId.data.value || '',
|
||||
'x-schema': 'events',
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
'x-query-limit': '1000'
|
||||
}), lazy: true
|
||||
const eventsData = useFetch(`/api/data/events`, {
|
||||
headers: useComputedHeaders(), lazy: true
|
||||
});
|
||||
|
||||
|
||||
const enabledEvents = ref<string[]>([]);
|
||||
|
||||
async function onEventCheck(eventName: string) {
|
||||
@@ -119,7 +129,7 @@ async function onEventCheck(eventName: string) {
|
||||
chartData.value.datasets[0].data = [];
|
||||
|
||||
for (const enabledEvent of enabledEvents.value) {
|
||||
const target = (eventsCount.data.value ?? []).find(e => e._id == enabledEvent);
|
||||
const target = (eventsData.data.value ?? []).find(e => e._id == enabledEvent);
|
||||
chartData.value.datasets[0].data.push(target?.count || 0);
|
||||
}
|
||||
}
|
||||
@@ -128,16 +138,19 @@ async function onEventCheck(eventName: string) {
|
||||
|
||||
|
||||
<template>
|
||||
<CardTitled title="Funnel" sub="Funnel events">
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div>
|
||||
<div class="min-w-[20rem]">
|
||||
<CardTitled title="Funnel"
|
||||
sub="Monitor and analyze the actions your users are performing on your platform to gain insights into their behavior and optimize the user experience">
|
||||
<div class="flex gap-2 justify-between lg:flex-row flex-col">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="min-w-[20rem] text-lyx-text-darker">
|
||||
Select two or more events
|
||||
</div>
|
||||
<div v-for="event of eventsCount.data.value">
|
||||
<UCheckbox @change="onEventCheck(event._id)" :value="enabledEvents.includes(event._id)"
|
||||
:label="event._id">
|
||||
</UCheckbox>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div v-for="event of eventsData.data.value">
|
||||
<UCheckbox color="secondary" @change="onEventCheck(event._id)"
|
||||
:value="enabledEvents.includes(event._id)" :label="event._id">
|
||||
</UCheckbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow">
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const eventNames = ref<string[]>([]);
|
||||
const eventNames = await useFetch<string[]>(`/api/data/events_data/names`, {
|
||||
headers: useComputedHeaders()
|
||||
});
|
||||
|
||||
const selectedEventName = ref<string>();
|
||||
const metadataFields = ref<string[]>([]);
|
||||
const selectedMetadataField = ref<string>();
|
||||
const metadataFieldGrouped = ref<any[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
eventNames.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/names`, signHeaders());
|
||||
});
|
||||
|
||||
watch(selectedEventName, () => {
|
||||
getMetadataFields();
|
||||
@@ -21,7 +20,9 @@ watch(selectedMetadataField, () => {
|
||||
});
|
||||
|
||||
async function getMetadataFields() {
|
||||
metadataFields.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/metadata_fields?name=${selectedEventName.value}`, signHeaders());
|
||||
metadataFields.value = await $fetch<string[]>(`/api/data/events_data/metadata_fields?name=${selectedEventName.value}`, {
|
||||
headers: useComputedHeaders().value
|
||||
});
|
||||
selectedMetadataField.value = undefined;
|
||||
currentSearchText.value = "";
|
||||
}
|
||||
@@ -38,10 +39,12 @@ async function getMetadataFieldGrouped() {
|
||||
name: selectedEventName.value,
|
||||
field: selectedMetadataField.value
|
||||
}
|
||||
|
||||
|
||||
const queryParamsString = Object.keys(queryParams).map((key) => `${key}=${queryParams[key]}`).join('&');
|
||||
|
||||
metadataFieldGrouped.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/metadata_field_group?${queryParamsString}`, signHeaders());
|
||||
metadataFieldGrouped.value = await $fetch<string[]>(`/api/data/events_data/metadata_field_group?${queryParamsString}`, {
|
||||
headers: useComputedHeaders().value
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -71,11 +74,80 @@ const canSearch = computed(() => {
|
||||
|
||||
<CardTitled title="Event metadata analyzer" sub="Filter events metadata fields to analyze them" class="w-full p-4">
|
||||
|
||||
<div class="p-2 flex flex-col">
|
||||
<div class="">
|
||||
|
||||
<LyxUiCard class="h-full w-full flex gap-2">
|
||||
|
||||
<div class="flex-[2]">
|
||||
<div class="flex flex-col gap-2">
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" searchable searchable-placeholder="Search an event..." class="w-full"
|
||||
placeholder="Select an event" :options="eventNames.data.value || []"
|
||||
v-model="selectedEventName">
|
||||
</USelectMenu>
|
||||
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" searchable searchable-placeholder="Search a field..." class="w-full"
|
||||
placeholder="Select a field" :options="metadataFields" v-model="selectedMetadataField">
|
||||
</USelectMenu>
|
||||
</div>
|
||||
|
||||
<div class="text-lyx-text-darker poppins mt-4 flex items-center gap-4 lg:flex-row flex-col">
|
||||
<div class="w-[10rem]">
|
||||
Search results: {{ metadataFieldGroupedFiltered.length }}
|
||||
</div>
|
||||
<div v-if="canSearch" class="h-full flex items-center text-[1.2rem]">
|
||||
|
||||
<div class="bg-lyx-widget-light flex items-center rounded-md pl-4">
|
||||
<div><i class="far fa-search"></i></div>
|
||||
<input class="bg-transparent px-4 py-2 text-[1rem] outline-none" type="text"
|
||||
placeholder="Filter by metadata name" v-model="currentSearchText">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 lg:mt-4 mt-10">
|
||||
|
||||
<div class="bg-lyx-widget-light text-lyx-text-dark px-3 py-2 rounded-md w-fit"
|
||||
v-for="item of metadataFieldGroupedFiltered">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div> {{ item._id || 'OLD_EVENTS' }} </div>
|
||||
<div class="px-1"> {{ item.count }} </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <div class="border-solid border-[#212121] border-l-[1px]"></div> -->
|
||||
|
||||
<!-- <div class="flex-[1]">
|
||||
<div class="poppins font-semibold"> </div>
|
||||
</div> -->
|
||||
|
||||
</LyxUiCard>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <div class="p-2 flex flex-col">
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full"
|
||||
placeholder="Select an event" :options="eventNames" v-model="selectedEventName">
|
||||
placeholder="Select an event" :options="eventNames.data.value || []" v-model="selectedEventName">
|
||||
</USelectMenu>
|
||||
|
||||
<USelectMenu v-if="metadataFields.length > 0" searchable searchable-placeholder="Search a field..."
|
||||
@@ -100,17 +172,11 @@ const canSearch = computed(() => {
|
||||
Search results: {{ metadataFieldGroupedFiltered.length }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div v-for="item of metadataFieldGroupedFiltered">
|
||||
<div class="flex gap-2">
|
||||
<div> {{ item._id || 'OLD_EVENTS' }} </div>
|
||||
<div> {{ item.count }} </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</CardTitled>
|
||||
|
||||
|
||||
|
||||
@@ -6,28 +6,17 @@ import DateService, { type Slice } from '@services/DateService';
|
||||
const props = defineProps<{ slice: Slice }>();
|
||||
const slice = computed(() => props.slice);
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const body = computed(() => {
|
||||
return {
|
||||
from: safeSnapshotDates.value.from,
|
||||
to: safeSnapshotDates.value.to,
|
||||
slice: slice.value,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function transformResponse(input: { _id: string, name: string, count: number }[]) {
|
||||
|
||||
const fixed = fixMetrics({
|
||||
data: input,
|
||||
from: input[0]._id,
|
||||
to: safeSnapshotDates.value.to
|
||||
}, slice.value, {
|
||||
advanced: true,
|
||||
advancedGroupKey: 'name'
|
||||
});
|
||||
},
|
||||
slice.value,
|
||||
{ advanced: true, advancedGroupKey: 'name' });
|
||||
|
||||
const parsedDatasets: any[] = [];
|
||||
|
||||
@@ -72,6 +61,7 @@ function transformResponse(input: { _id: string, name: string, count: number }[]
|
||||
datasets: parsedDatasets,
|
||||
labels: fixed.labels
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const errorData = ref<{ errored: boolean, text: string }>({
|
||||
@@ -90,8 +80,10 @@ function onResponse(e: any) {
|
||||
if (e.response.status != 500) errorData.value = { errored: false, text: '' }
|
||||
}
|
||||
|
||||
const eventsStackedData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events_stacked`, {
|
||||
method: 'POST', body, lazy: true, immediate: false, transform: transformResponse, ...signHeaders(),
|
||||
const eventsStackedData = useFetch(`/api/timeline/events_stacked`, {
|
||||
lazy: true, immediate: false,
|
||||
transform: transformResponse,
|
||||
headers: useComputedHeaders({ slice }),
|
||||
onResponseError,
|
||||
onResponse
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const eventNames = await useFetch<string[]>(`/api/data/events_data/names`, {
|
||||
headers: useComputedHeaders()
|
||||
});
|
||||
|
||||
const eventNames = ref<string[]>([]);
|
||||
const selectedEventName = ref<string>();
|
||||
|
||||
onMounted(async () => {
|
||||
eventNames.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/names`, signHeaders());
|
||||
});
|
||||
|
||||
const userFlowData = ref<any>();
|
||||
const analyzing = ref<boolean>(false);
|
||||
@@ -26,7 +24,10 @@ async function getUserFlowData() {
|
||||
|
||||
const queryParamsString = Object.keys(queryParams).map((key) => `${key}=${queryParams[key]}`).join('&');
|
||||
|
||||
userFlowData.value = await $fetch(`/api/metrics/${activeProject.value?._id.toString()}/events/flow_from_name?${queryParamsString}`, signHeaders());
|
||||
userFlowData.value = await $fetch(`/api/data/events_data/flow_from_name?${queryParamsString}`, {
|
||||
headers: useComputedHeaders().value
|
||||
});
|
||||
|
||||
analyzing.value = false;
|
||||
}
|
||||
|
||||
@@ -38,25 +39,39 @@ async function analyzeEvent() {
|
||||
|
||||
<template>
|
||||
<CardTitled title="Event User Flow"
|
||||
sub="Track your user's journey from external links to in-app events, maintaining a complete view of their path from entry to engagement." class="w-full p-4">
|
||||
sub="Track your user's journey from external links to in-app events, maintaining a complete view of their path from entry to engagement."
|
||||
class="w-full p-4">
|
||||
|
||||
<div class="p-2 flex flex-col gap-3">
|
||||
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full"
|
||||
placeholder="Select an event" :options="eventNames" v-model="selectedEventName">
|
||||
</USelectMenu>
|
||||
<div v-if="selectedEventName && !analyzing" class="flex justify-center">
|
||||
<div @click="analyzeEvent()"
|
||||
class="bg-bg w-fit px-8 py-2 poppins rounded-lg hover:bg-bg/80 cursor-pointer">
|
||||
Analyze
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
<div class="py-2 flex items-center gap-3">
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" searchable searchable-placeholder="Search an event..." class="w-full" placeholder="Select an event"
|
||||
:options="eventNames.data.value || []" v-model="selectedEventName">
|
||||
</USelectMenu>
|
||||
<div v-if="selectedEventName && !analyzing" class="flex justify-center">
|
||||
<LyxUiButton @click="analyzeEvent()" type="primary" class="w-fit px-8 py-1">
|
||||
Analyze
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="analyzing">
|
||||
Analyzing...
|
||||
<div
|
||||
class="backdrop-blur-[1px] z-[20] w-full h-full flex items-center justify-center font-bold rockmann">
|
||||
<i
|
||||
class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2" v-if="userFlowData">
|
||||
<div class="flex gap-4 items-center bg-bg py-1 px-2 rounded-lg"
|
||||
<div class="flex gap-4 items-center bg-bg py-2 px-2 bg-lyx-widget-light rounded-lg"
|
||||
v-for="(count, referrer) in userFlowData">
|
||||
<div class="w-5 h-5 flex items-center justify-center">
|
||||
<img :src="`https://s2.googleusercontent.com/s2/favicons?domain=${referrer}&sz=64`"
|
||||
@@ -67,8 +82,8 @@ async function analyzeEvent() {
|
||||
<div> {{ count.toFixed(2).replace('.', ',') }} % </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</CardTitled>
|
||||
</template>
|
||||
@@ -1,82 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
export type PricingCardProp = {
|
||||
title: string,
|
||||
cost: string,
|
||||
features: string[],
|
||||
desc: string,
|
||||
active: boolean,
|
||||
planId: number,
|
||||
isDowngrade: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{ data: PricingCardProp }>();
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
async function onUpgradeClick() {
|
||||
const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create`, {
|
||||
...signHeaders({ 'content-type': 'application/json' }),
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ planId: props.data.planId })
|
||||
})
|
||||
if (!res) alert('Something went wrong');
|
||||
window.open(res);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 bg-[#303030] rounded-xl pricing-card flex flex-col">
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="text-[1.1rem] font-semibold mb-4">
|
||||
{{ data.title }}
|
||||
</div>
|
||||
<div class="flex gap-1 items-end mb-2">
|
||||
<div class="text-[1.1rem] font-semibold">
|
||||
€{{ data.cost }}
|
||||
</div>
|
||||
<div class="text-text-sub text-[.9rem] mb-[.15rem]">
|
||||
per month
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.active" class="text-[1rem] bg-[#1f1f22] rounded-md py-2 text-center">
|
||||
Current active plan
|
||||
</div>
|
||||
<div @click="onUpgradeClick()" v-if="!data.active && !data.isDowngrade"
|
||||
class="cursor-pointer text-[1rem] font-semibold bg-[#3a3af5] rounded-md py-2 text-center">
|
||||
Upgrade
|
||||
</div>
|
||||
<div @click="onUpgradeClick()" v-if="!data.active && data.isDowngrade"
|
||||
class="cursor-pointer text-[1rem] font-semibold bg-[#1f1f22] text-red-400 rounded-md py-2 text-center">
|
||||
Downgrade
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-400 h-[1px] w-full my-4"></div>
|
||||
|
||||
<div class="flex flex-col gap-1 grow">
|
||||
<div class="flex gap-2 items-center" v-for="feature of data.features">
|
||||
<i class="fas fa-check"></i>
|
||||
<div>
|
||||
{{ feature }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-400 h-[1px] w-full my-4"></div>
|
||||
|
||||
<div class="text-text-sub text-[.9rem] h-[20%]">
|
||||
{{ data.desc }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pricing-card * {
|
||||
font-family: "Poppins";
|
||||
}
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@ export type PricingCardProp = {
|
||||
|
||||
const props = defineProps<{ datas: PricingCardProp[], defaultIndex?: number }>();
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const { project } = useProject();
|
||||
|
||||
const currentIndex = ref<number>(props.defaultIndex || 0);
|
||||
|
||||
@@ -24,8 +24,13 @@ const data = computed(() => {
|
||||
})
|
||||
|
||||
async function onUpgradeClick() {
|
||||
const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create`, {
|
||||
...signHeaders({ 'content-type': 'application/json' }),
|
||||
const res = await $fetch<string>(`/api/pay/create`, {
|
||||
headers: useComputedHeaders({
|
||||
useSnapshotDates: false,
|
||||
custom: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
}).value,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ planId: data.value.planId })
|
||||
})
|
||||
@@ -42,7 +47,7 @@ async function onUpgradeClick() {
|
||||
|
||||
<div class="flex flex-col gap-3 text-center pt-3">
|
||||
<div v-if="data.active"
|
||||
class="absolute right-6 top-3 poppins text-[.75rem] bg-[#222A42] outline outline-[1px] outline-[#5680F8] px-3 py-[.1rem] rounded-sm">
|
||||
class="absolute right-6 top-3 poppins text-[.75rem] bg-transparent border-[#262626] border-solid border-[1px] px-3 py-[.1rem] rounded-sm">
|
||||
Active
|
||||
</div>
|
||||
<div v-if="!data.active && data.title === 'Growth'"
|
||||
@@ -67,7 +72,10 @@ async function onUpgradeClick() {
|
||||
}" :min="0" :max="datas.length - 1" v-model="currentIndex">
|
||||
</URange>
|
||||
</div>
|
||||
<div class="poppins" v-for="sub of data.subs"> {{ sub }} </div>
|
||||
<div :class="{ '!text-[.8rem] !text-lyx-text-darker': sub.includes('€') }" class="poppins text-[.9rem]"
|
||||
v-for="sub of data.subs">
|
||||
{{ sub }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sep bg-[#262626] h-[1px] my-8"></div>
|
||||
@@ -82,20 +90,27 @@ async function onUpgradeClick() {
|
||||
</div>
|
||||
|
||||
<div class="mt-10 flex">
|
||||
<div class="w-full" v-if="data.planId > -1">
|
||||
<div @click="onUpgradeClick()" v-if="!data.active && !data.isDowngrade"
|
||||
class="cursor-pointer text-[1rem] font-semibold bg-[#3a3af5] rounded-md py-2 text-center">
|
||||
|
||||
<div class="w-full flex" v-if="data.planId > -1">
|
||||
|
||||
|
||||
<LyxUiButton class="rounded-md py-2 w-full text-center" type="primary" @click="onUpgradeClick()"
|
||||
v-if="!data.active && !data.isDowngrade">
|
||||
Upgrade
|
||||
</div>
|
||||
<div @click="onUpgradeClick()" v-if="!data.active && data.isDowngrade"
|
||||
class="w-full cursor-pointer text-[1rem] font-semibold bg-[#1f1f22] text-red-400 rounded-md py-2 text-center">
|
||||
</LyxUiButton>
|
||||
|
||||
<LyxUiButton class="rounded-md py-2 w-full text-center" type="danger" @click="onUpgradeClick()"
|
||||
v-if="!data.active && data.isDowngrade">
|
||||
Downgrade
|
||||
</div>
|
||||
</LyxUiButton>
|
||||
|
||||
</div>
|
||||
<NuxtLink v-if="data.planId === -1" :to="data.link || 'https://dashboard.litlyx.com'"
|
||||
class="bg-[#222A42] cursor-pointer outline outline-[1px] outline-[#5680F8] w-full !rounded-md text-center text-[.9rem] !py-2 ">
|
||||
|
||||
<LyxUiButton v-if="data.planId === -1" :to="data.link || 'https://dashboard.litlyx.com'"
|
||||
class="rounded-md py-2 w-full text-center" type="primary">
|
||||
{{ data.cta }}
|
||||
</NuxtLink>
|
||||
</LyxUiButton>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,9 @@ import type { PricingCardProp } from './PricingCardGeneric.vue';
|
||||
|
||||
|
||||
const { data: planData, refresh: refreshPlanData } = useFetch('/api/project/plan', {
|
||||
...signHeaders(),
|
||||
lazy: true
|
||||
lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
|
||||
});
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
watch(activeProject, () => {
|
||||
refreshPlanData();
|
||||
});
|
||||
|
||||
|
||||
function getPricingsData() {
|
||||
|
||||
const freePricing: PricingCardProp[] = [
|
||||
@@ -23,7 +15,7 @@ function getPricingsData() {
|
||||
price: '€0 / mo',
|
||||
subs: [
|
||||
'Up to 5000 visits/events per month',
|
||||
'CPM 0€ per visit/event'
|
||||
|
||||
],
|
||||
features: [
|
||||
'Email support',
|
||||
@@ -70,7 +62,7 @@ function getPricingsData() {
|
||||
price: '€4,99 / mo',
|
||||
subs: [
|
||||
'Up to 50.000 visits/events per month',
|
||||
'CPM 0,10€ per visit/event'
|
||||
'0,00010€ per visit/event'
|
||||
],
|
||||
features: [
|
||||
'Slack support',
|
||||
@@ -90,7 +82,7 @@ function getPricingsData() {
|
||||
price: '€9,99 / mo',
|
||||
subs: [
|
||||
'Up to 150.000 visits/events per month',
|
||||
'CPM 0,06€ per visit/event'
|
||||
'0,00006€ per visit/event'
|
||||
],
|
||||
features: [
|
||||
'Slack support',
|
||||
@@ -110,7 +102,7 @@ function getPricingsData() {
|
||||
price: '€29,99 / mo',
|
||||
subs: [
|
||||
'Up to 500.000 visits/events per month',
|
||||
'CPM 0,059€ per visit/event'
|
||||
'0,000059€ per visit/event'
|
||||
],
|
||||
features: [
|
||||
'Slack support',
|
||||
@@ -130,7 +122,7 @@ function getPricingsData() {
|
||||
price: '€59,99 / mo',
|
||||
subs: [
|
||||
'Up to 1.000.000 visits/events per month',
|
||||
'CPM 0,059€ per visit/event'
|
||||
'0,000059€ per visit/event'
|
||||
],
|
||||
features: [
|
||||
'Slack support',
|
||||
@@ -138,7 +130,7 @@ function getPricingsData() {
|
||||
'Unlimited reports',
|
||||
'AI Tokens: 5.000',
|
||||
'Server type: SHARED',
|
||||
'Data retention: 1 Year'
|
||||
'Data retention: 3 Year'
|
||||
],
|
||||
cta: 'Go to Cloud Dashboard',
|
||||
active: (planData.value?.premium_type || 0) == 104,
|
||||
@@ -150,7 +142,7 @@ function getPricingsData() {
|
||||
price: '€99,99 / mo',
|
||||
subs: [
|
||||
'Up to 2.500.000 visits/events per month',
|
||||
'CPM 0,039€ per visit/event'
|
||||
'0,000039€ per visit/event'
|
||||
],
|
||||
features: [
|
||||
'Slack support',
|
||||
@@ -158,7 +150,7 @@ function getPricingsData() {
|
||||
'Unlimited reports',
|
||||
'AI Tokens: 10.000',
|
||||
'Server type: DEDICATED',
|
||||
'Data retention: 2 Years'
|
||||
'Data retention: 7 Years'
|
||||
],
|
||||
cta: 'Go to Cloud Dashboard',
|
||||
active: (planData.value?.premium_type || 0) == 105,
|
||||
@@ -170,7 +162,7 @@ function getPricingsData() {
|
||||
price: '€149,99 / mo',
|
||||
subs: [
|
||||
'Up to 5.000.000 visits/events per month',
|
||||
'CPM 0,029€ per visit/event'
|
||||
'0,000029€ per visit/event'
|
||||
],
|
||||
features: [
|
||||
'Slack support',
|
||||
@@ -178,7 +170,7 @@ function getPricingsData() {
|
||||
'Unlimited reports',
|
||||
'AI Tokens: 20.000',
|
||||
'Server type: DEDICATED',
|
||||
'Data retention: 3 Years'
|
||||
'Data retention: 8 Years'
|
||||
],
|
||||
cta: 'Go to Cloud Dashboard',
|
||||
active: (planData.value?.premium_type || 0) == 106,
|
||||
@@ -190,15 +182,18 @@ function getPricingsData() {
|
||||
return { freePricing, customPricing, slidePricings }
|
||||
}
|
||||
|
||||
|
||||
const { projectId } = useProject();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(evt: 'onCloseClick'): void
|
||||
}>();
|
||||
|
||||
async function onLifetimeUpgradeClick() {
|
||||
const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create-onetime`, {
|
||||
...signHeaders({ 'content-type': 'application/json' }),
|
||||
const res = await $fetch<string>(`/api/pay/create-onetime`, {
|
||||
...signHeaders({
|
||||
'content-type': 'application/json',
|
||||
'x-pid': projectId.value ?? ''
|
||||
}),
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ planId: 2001 })
|
||||
})
|
||||
@@ -218,7 +213,8 @@ async function onLifetimeUpgradeClick() {
|
||||
|
||||
<div class="flex gap-8 mt-10 h-max xl:flex-row flex-col">
|
||||
<PricingCardGeneric class="flex-1" :datas="getPricingsData().freePricing"></PricingCardGeneric>
|
||||
<PricingCardGeneric class="flex-1" :datas="getPricingsData().slidePricings" :default-index="2"></PricingCardGeneric>
|
||||
<PricingCardGeneric class="flex-1" :datas="getPricingsData().slidePricings" :default-index="2">
|
||||
</PricingCardGeneric>
|
||||
<PricingCardGeneric class="flex-1" :datas="getPricingsData().customPricing"></PricingCardGeneric>
|
||||
</div>
|
||||
|
||||
@@ -278,7 +274,7 @@ async function onLifetimeUpgradeClick() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="rounded-lg px-10 py-3 bg-[#303030]">
|
||||
<div class="rounded-lg px-10 py-3 bg-[#151515]">
|
||||
<a href="mailto:help@litlyx.com" class="poppins text-[1.3rem]">
|
||||
help@litlyx.com
|
||||
</a>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { TApiSettings } from '@schema/ApiSettingsSchema';
|
||||
import type { SettingsTemplateEntry } from './Template.vue';
|
||||
|
||||
const { project, actions, projectList, isGuest, projectId } = useProject();
|
||||
|
||||
const entries: SettingsTemplateEntry[] = [
|
||||
{ id: 'pname', title: 'Name', text: 'Project name' },
|
||||
@@ -11,8 +12,7 @@ const entries: SettingsTemplateEntry[] = [
|
||||
{ id: 'pdelete', title: 'Delete', text: 'Delete current project' },
|
||||
]
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const projectNameInputVal = ref<string>(activeProject.value?.name || '');
|
||||
const projectNameInputVal = ref<string>(project.value?.name || '');
|
||||
|
||||
const apiKeys = ref<TApiSettings[]>([]);
|
||||
|
||||
@@ -20,14 +20,17 @@ const newApiKeyName = ref<string>('');
|
||||
|
||||
async function updateApiKeys() {
|
||||
newApiKeyName.value = '';
|
||||
apiKeys.value = await $fetch<TApiSettings[]>('/api/keys/get_all', signHeaders());
|
||||
apiKeys.value = await $fetch<TApiSettings[]>('/api/keys/get_all', signHeaders({
|
||||
'x-pid': project.value?._id.toString() ?? ''
|
||||
}));
|
||||
}
|
||||
|
||||
async function createApiKey() {
|
||||
try {
|
||||
const res = await $fetch<TApiSettings>('/api/keys/create', {
|
||||
method: 'POST', ...signHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'x-pid': project.value?._id.toString() ?? ''
|
||||
}),
|
||||
body: JSON.stringify({ name: newApiKeyName.value })
|
||||
});
|
||||
@@ -42,7 +45,8 @@ async function deleteApiKey(api_id: string) {
|
||||
try {
|
||||
const res = await $fetch<TApiSettings>('/api/keys/delete', {
|
||||
method: 'DELETE', ...signHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'x-pid': project.value?._id.toString() ?? ''
|
||||
}),
|
||||
body: JSON.stringify({ api_id })
|
||||
});
|
||||
@@ -56,15 +60,15 @@ async function deleteApiKey(api_id: string) {
|
||||
|
||||
onMounted(() => {
|
||||
updateApiKeys();
|
||||
})
|
||||
});
|
||||
|
||||
watch(activeProject, () => {
|
||||
projectNameInputVal.value = activeProject.value?.name || "";
|
||||
watch(project, () => {
|
||||
projectNameInputVal.value = project.value?.name || "";
|
||||
updateApiKeys();
|
||||
})
|
||||
});
|
||||
|
||||
const canChange = computed(() => {
|
||||
if (activeProject.value?.name == projectNameInputVal.value) return false;
|
||||
if (project.value?.name == projectNameInputVal.value) return false;
|
||||
if (projectNameInputVal.value.length === 0) return false;
|
||||
return true;
|
||||
});
|
||||
@@ -73,31 +77,41 @@ const canChange = computed(() => {
|
||||
async function changeProjectName() {
|
||||
await $fetch("/api/project/change_name", {
|
||||
method: 'POST',
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
...signHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
'x-pid': project.value?._id.toString() ?? ''
|
||||
}),
|
||||
body: JSON.stringify({ name: projectNameInputVal.value })
|
||||
});
|
||||
location.reload();
|
||||
}
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
async function deleteProject() {
|
||||
if (!activeProject.value) return;
|
||||
const sure = confirm(`Are you sure to delete the project ${activeProject.value.name} ?`);
|
||||
if (!project.value) return;
|
||||
const sure = confirm(`Are you sure to delete the project ${project.value.name} ?`);
|
||||
if (!sure) return;
|
||||
|
||||
try {
|
||||
|
||||
await $fetch('/api/project/delete', {
|
||||
method: 'DELETE',
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ project_id: activeProject.value._id.toString() })
|
||||
...signHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
'x-pid': project.value?._id.toString() ?? ''
|
||||
}),
|
||||
body: JSON.stringify({ project_id: project.value._id.toString() })
|
||||
});
|
||||
|
||||
const projectsList = useProjectsList()
|
||||
await projectsList.refresh();
|
||||
|
||||
const firstProjectId = projectsList.data.value?.[0]?._id.toString();
|
||||
await actions.refreshProjectsList()
|
||||
|
||||
const firstProjectId = projectList.value?.[0]?._id.toString();
|
||||
if (firstProjectId) {
|
||||
await setActiveProject(firstProjectId);
|
||||
await actions.setActiveProject(firstProjectId);
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
|
||||
@@ -110,8 +124,6 @@ async function deleteProject() {
|
||||
|
||||
const { createAlert } = useAlert()
|
||||
|
||||
const activeProjectId = useActiveProjectId()
|
||||
|
||||
function copyScript() {
|
||||
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
||||
|
||||
@@ -119,7 +131,7 @@ function copyScript() {
|
||||
const createScriptText = () => {
|
||||
return [
|
||||
'<script defer ',
|
||||
`data-project="${activeProjectId.data.value}" `,
|
||||
`data-project="${projectId.value}" `,
|
||||
'src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></',
|
||||
'script>'
|
||||
].join('')
|
||||
@@ -132,7 +144,7 @@ function copyScript() {
|
||||
|
||||
function copyProjectId() {
|
||||
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
||||
navigator.clipboard.writeText(activeProjectId.data.value || '');
|
||||
navigator.clipboard.writeText(projectId.value || '');
|
||||
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
||||
}
|
||||
|
||||
@@ -142,17 +154,18 @@ function copyProjectId() {
|
||||
|
||||
|
||||
<template>
|
||||
<SettingsTemplate :entries="entries" :key="activeProject?.name || 'NONE'">
|
||||
<SettingsTemplate :entries="entries" :key="project?.name || 'NONE'">
|
||||
<template #pname>
|
||||
<div class="flex items-center gap-4">
|
||||
<LyxUiInput class="w-full px-4 py-2" v-model="projectNameInputVal"></LyxUiInput>
|
||||
<LyxUiInput class="w-full px-4 py-2" :disabled="isGuest" v-model="projectNameInputVal"></LyxUiInput>
|
||||
<LyxUiButton v-if="!isGuest" @click="changeProjectName()" :disabled="!canChange" type="primary"> Change
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
</template>
|
||||
<template #api>
|
||||
<div class="flex items-center gap-4" v-if="apiKeys && apiKeys.length < 5">
|
||||
<LyxUiInput class="grow px-4 py-2" placeholder="ApiKeyName" v-model="newApiKeyName"></LyxUiInput>
|
||||
<LyxUiInput class="grow px-4 py-2" :disabled="isGuest" placeholder="ApiKeyName" v-model="newApiKeyName">
|
||||
</LyxUiInput>
|
||||
<LyxUiButton v-if="!isGuest" @click="createApiKey()" :disabled="newApiKeyName.length < 3"
|
||||
type="primary">
|
||||
<i class="far fa-plus"></i>
|
||||
@@ -164,7 +177,7 @@ function copyProjectId() {
|
||||
<div class="flex gap-8 items-center">
|
||||
<div class="grow">Name: {{ apiKey.apiName }}</div>
|
||||
<div>{{ apiKey.apiKey }}</div>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex justify-end" v-if="!isGuest">
|
||||
<i class="far fa-trash cursor-pointer" @click="deleteApiKey(apiKey._id.toString())"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,7 +187,7 @@ function copyProjectId() {
|
||||
</template>
|
||||
<template #pid>
|
||||
<LyxUiCard class="w-full flex items-center">
|
||||
<div class="grow">{{ activeProject?._id.toString() }}</div>
|
||||
<div class="grow">{{ project?._id.toString() }}</div>
|
||||
<div><i class="far fa-copy" @click="copyProjectId()"></i></div>
|
||||
</LyxUiCard>
|
||||
</template>
|
||||
@@ -182,14 +195,19 @@ function copyProjectId() {
|
||||
<LyxUiCard class="w-full flex items-center">
|
||||
<div class="grow">
|
||||
{{ `
|
||||
<script defer data-project="${activeProject?._id}"
|
||||
<script defer data-project="${project?._id}"
|
||||
src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></script>` }}
|
||||
</div>
|
||||
<div><i class="far fa-copy" @click="copyScript()"></i></div>
|
||||
<div class="hidden lg:flex"><i class="far fa-copy" @click="copyScript()"></i></div>
|
||||
</LyxUiCard>
|
||||
<div class="flex justify-end w-full">
|
||||
<LyxUiButton type="outline" class="flex lg:hidden mt-4">
|
||||
Copy script
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
</template>
|
||||
<template #pdelete>
|
||||
<div class="flex justify-end" v-if="!isGuest">
|
||||
<div class="flex lg:justify-end" v-if="!isGuest">
|
||||
<LyxUiButton type="danger" @click="deleteProject()">
|
||||
Delete project
|
||||
</LyxUiButton>
|
||||
|
||||
@@ -16,10 +16,10 @@ const props = defineProps<SettingsTemplateProp>();
|
||||
|
||||
|
||||
<template>
|
||||
<div class="mt-10 px-4">
|
||||
<div class="mt-10 px-4 xl:pb-0 pb-[10rem]">
|
||||
<div v-for="(entry, index) of props.entries" class="flex flex-col">
|
||||
<div class="flex">
|
||||
<div class="flex-[2]">
|
||||
<div class="flex xl:flex-row flex-col gap-4 xl:gap-0">
|
||||
<div class="xl:flex-[2]">
|
||||
<div class="poppins font-medium text-lyx-text">
|
||||
{{ entry.title }}
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@ const props = defineProps<SettingsTemplateProp>();
|
||||
{{ entry.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-[3]">
|
||||
<div class="xl:flex-[3]">
|
||||
<slot :name="entry.id"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,16 +3,16 @@ import dayjs from 'dayjs';
|
||||
import type { SettingsTemplateEntry } from './Template.vue';
|
||||
import { getPlanFromId, PREMIUM_PLAN, type PREMIUM_TAG } from '@data/PREMIUM';
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const { projectId, isGuest } = useProject();
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const { data: planData, refresh: planRefresh, pending: planPending } = useFetch('/api/project/plan', {
|
||||
...signHeaders(), lazy: true
|
||||
const { data: planData, pending: planPending } = useFetch('/api/project/plan', {
|
||||
lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
|
||||
});
|
||||
|
||||
const { data: customerAddress, refresh: refreshCustomerAddress } = useFetch(`/api/pay/${activeProject.value?._id.toString()}/customer_info`, {
|
||||
...signHeaders(), lazy: true
|
||||
const { data: customerAddress } = useFetch(`/api/pay/customer_info`, {
|
||||
lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
|
||||
});
|
||||
|
||||
const percent = computed(() => {
|
||||
@@ -45,8 +45,8 @@ const prettyExpireDate = computed(() => {
|
||||
});
|
||||
|
||||
|
||||
const { data: invoices, refresh: invoicesRefresh, pending: invoicesPending } = useFetch(`/api/pay/${activeProject.value?._id.toString()}/invoices`, {
|
||||
...signHeaders(), lazy: true
|
||||
const { data: invoices, pending: invoicesPending } = useFetch(`/api/pay/invoices`, {
|
||||
lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
|
||||
})
|
||||
|
||||
function openInvoice(link: string) {
|
||||
@@ -75,7 +75,7 @@ const entries: SettingsTemplateEntry[] = [
|
||||
]
|
||||
|
||||
watch(customerAddress, () => {
|
||||
console.log('UPDATE',customerAddress.value)
|
||||
console.log('UPDATE', customerAddress.value)
|
||||
if (!customerAddress.value) return;
|
||||
currentBillingInfo.value = customerAddress.value;
|
||||
});
|
||||
@@ -92,21 +92,22 @@ const currentBillingInfo = ref<any>({
|
||||
const { createAlert } = useAlert()
|
||||
|
||||
async function saveBillingInfo() {
|
||||
|
||||
|
||||
try {
|
||||
const res = await $fetch(`/api/pay/${activeProject.value?._id.toString()}/update_customer`, {
|
||||
method: 'POST',
|
||||
...signHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
body: JSON.stringify(currentBillingInfo.value)
|
||||
});
|
||||
const res = await $fetch(`/api/pay/update_customer`, {
|
||||
method: 'POST',
|
||||
...signHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
'x-pid': projectId.value ?? ''
|
||||
}),
|
||||
body: JSON.stringify(currentBillingInfo.value)
|
||||
});
|
||||
|
||||
createAlert('Customer updated','Customer updated successfully', 'far fa-check', 5000);
|
||||
createAlert('Customer updated', 'Customer updated successfully', 'far fa-check', 5000);
|
||||
|
||||
} catch(ex) {
|
||||
createAlert('Error updating customer','An error occurred while updating the customer', 'far fa-error', 8000);
|
||||
}
|
||||
} catch (ex) {
|
||||
createAlert('Error updating customer', 'An error occurred while updating the customer', 'far fa-error', 8000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -125,24 +126,24 @@ const { visible } = usePricingDrawer();
|
||||
|
||||
<SettingsTemplate v-if="!invoicesPending && !planPending" :entries="entries">
|
||||
<template #info>
|
||||
<div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<LyxUiInput class="px-2 py-1" placeholder="Address line 1" v-model="currentBillingInfo.line1">
|
||||
<div v-if="!isGuest">
|
||||
<div class="flex flex-col gap-4">
|
||||
<LyxUiInput class="px-2 py-2 !bg-[#161616]" placeholder="Address line 1" v-model="currentBillingInfo.line1">
|
||||
</LyxUiInput>
|
||||
<LyxUiInput class="px-2 py-1" placeholder="Address line 2" v-model="currentBillingInfo.line2">
|
||||
<LyxUiInput class="px-2 py-2 !bg-[#161616]" placeholder="Address line 2" v-model="currentBillingInfo.line2">
|
||||
</LyxUiInput>
|
||||
<div class="flex gap-2 w-full">
|
||||
<LyxUiInput class="px-2 py-1 w-full" placeholder="Country"
|
||||
<div class="flex gap-4 w-full">
|
||||
<LyxUiInput class="px-2 py-2 w-full !bg-[#161616]" placeholder="Country"
|
||||
v-model="currentBillingInfo.country">
|
||||
</LyxUiInput>
|
||||
<LyxUiInput class="px-2 py-1 w-full" placeholder="Postal code"
|
||||
<LyxUiInput class="px-2 py-2 w-full !bg-[#161616]" placeholder="Postal code"
|
||||
v-model="currentBillingInfo.postal_code">
|
||||
</LyxUiInput>
|
||||
</div>
|
||||
<div class="flex gap-2 w-full">
|
||||
<LyxUiInput class="px-2 py-1 w-full" placeholder="City" v-model="currentBillingInfo.city">
|
||||
<div class="flex gap-4 w-full">
|
||||
<LyxUiInput class="px-2 py-2 w-full !bg-[#161616]" placeholder="City" v-model="currentBillingInfo.city">
|
||||
</LyxUiInput>
|
||||
<LyxUiInput class="px-2 py-1 w-full" placeholder="State" v-model="currentBillingInfo.state">
|
||||
<LyxUiInput class="px-2 py-2 w-full !bg-[#161616]" placeholder="State" v-model="currentBillingInfo.state">
|
||||
</LyxUiInput>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,28 +157,22 @@ const { visible } = usePricingDrawer();
|
||||
<template #plan>
|
||||
<LyxUiCard v-if="planData" class="flex flex-col w-full">
|
||||
<div class="flex flex-col gap-6 px-8 grow">
|
||||
<div class="flex justify-between flex-col sm:flex-row">
|
||||
<div class="flex justify-between items-center 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">
|
||||
class="flex lato text-[.7rem] bg-transparent border-[#262626] border-[1px] px-[.6rem] rounded-sm">
|
||||
{{ planData.premium ? getPremiumName(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]"> €
|
||||
{{ getPremiumPrice(planData.premium_type) }} </div>
|
||||
<div class="poppins text-text-sub mt-2"> per month </div>
|
||||
<div class="flex items-center ml-2">
|
||||
<i class="far fa-info-circle text-[.8rem]"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
@@ -195,16 +190,14 @@ const { visible } = usePricingDrawer();
|
||||
</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 justify-between px-8 flex-col lg:flex-row gap-2 lg:gap-0 items-center">
|
||||
<div class="flex gap-2 text-text-sub text-[.9rem]">
|
||||
<div class="poppins"> Expire date:</div>
|
||||
<div> {{ prettyExpireDate }}</div>
|
||||
</div>
|
||||
<div v-if="!isGuest" @click="visible = true"
|
||||
class="cursor-pointer flex items-center gap-2 text-[.9rem] text-white font-semibold bg-accent px-4 py-1 rounded-lg drop-shadow-[0_0_8px_#000000]">
|
||||
<div class="poppins"> Upgrade plan </div>
|
||||
<i class="fas fa-arrow-up-right"></i>
|
||||
</div>
|
||||
<LyxUiButton v-if="!isGuest" @click="visible = true" type="primary">
|
||||
Upgrade plan
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
</LyxUiCard>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { SettingsTemplateEntry } from './Template.vue';
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const { projectId, isGuest } = useProject();
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
|
||||
const columns = [
|
||||
{ key: 'me', label: '' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
@@ -15,7 +14,9 @@ const columns = [
|
||||
// { key: 'pending', label: 'Pending' },
|
||||
]
|
||||
|
||||
const { data: members, refresh: refreshMembers, pending: pendingMembers } = useFetch('/api/project/members/list', signHeaders());
|
||||
const { data: members, refresh: refreshMembers } = useFetch('/api/project/members/list', {
|
||||
headers: useComputedHeaders({ useSnapshotDates: false })
|
||||
});
|
||||
|
||||
const showAddMember = ref<boolean>(false);
|
||||
|
||||
@@ -30,7 +31,10 @@ async function kickMember(email: string) {
|
||||
|
||||
await $fetch('/api/project/members/kick', {
|
||||
method: 'POST',
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
...signHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
'x-pid': projectId.value ?? ''
|
||||
}),
|
||||
body: JSON.stringify({ email }),
|
||||
onResponseError({ request, response, options }) {
|
||||
alert(response.statusText);
|
||||
@@ -55,7 +59,10 @@ async function addMember() {
|
||||
|
||||
await $fetch('/api/project/members/add', {
|
||||
method: 'POST',
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
...signHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
'x-pid': projectId.value ?? ''
|
||||
}),
|
||||
body: JSON.stringify({ email: addMemberEmail.value }),
|
||||
onResponseError({ request, response, options }) {
|
||||
alert(response.statusText);
|
||||
@@ -71,10 +78,6 @@ async function addMember() {
|
||||
|
||||
}
|
||||
|
||||
watch(activeProject, () => {
|
||||
refreshMembers();
|
||||
})
|
||||
|
||||
const entries: SettingsTemplateEntry[] = [
|
||||
{ id: 'add', title: 'Add member', text: 'Add new member to project' },
|
||||
{ id: 'members', title: 'Members', text: 'Manage members of current project' },
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { TProject } from "@schema/ProjectSchema";
|
||||
|
||||
const projects = useFetch<TProject[]>('/api/project/list', {
|
||||
key: 'projectslist', ...signHeaders()
|
||||
});
|
||||
|
||||
export function useProjectsList() {
|
||||
return { ...projects, projects: projects.data }
|
||||
}
|
||||
|
||||
const guestProjects = useFetch<TProject[]>('/api/project/list_guest', {
|
||||
key: 'guestProjectslist', ...signHeaders()
|
||||
});
|
||||
|
||||
export function useGuestProjectsList() {
|
||||
return { ...guestProjects, guestProjects: guestProjects.data }
|
||||
}
|
||||
|
||||
const activeProjectId = useFetch<string>(`/api/user/active_project`, {
|
||||
key: 'activeProjectId', ...signHeaders(),
|
||||
});
|
||||
|
||||
export const isGuest = computed(() => {
|
||||
if (!guestProjects.data.value) return false;
|
||||
const guestTarget = guestProjects.data.value.find(e => e._id.toString() == activeProjectId.data.value);
|
||||
if (guestTarget) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
export function useActiveProjectId() {
|
||||
return { ...activeProjectId, pid: activeProjectId.data }
|
||||
}
|
||||
|
||||
export function useActiveProject() {
|
||||
if (isLiveDemo()) {
|
||||
const { data: liveDemoProject } = useLiveDemo();
|
||||
return liveDemoProject;
|
||||
}
|
||||
return computed(() => {
|
||||
if (!projects.data.value) return;
|
||||
if (!activeProjectId.data.value) return;
|
||||
const target = projects.data.value.find(e => e._id.toString() == activeProjectId.data.value);
|
||||
if (target) return target;
|
||||
if (!guestProjects.data.value) return;
|
||||
const guestTarget = guestProjects.data.value.find(e => e._id.toString() == activeProjectId.data.value);
|
||||
return guestTarget;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function setActiveProject(project_id: string) {
|
||||
changingProject.value = true;
|
||||
await new Promise(e => setTimeout(e, 500));
|
||||
await $fetch<string>(`/api/user/set_active_project?project_id=${project_id}`, signHeaders());
|
||||
await activeProjectId.refresh();
|
||||
changingProject.value = false;
|
||||
}
|
||||
|
||||
export const changingProject = ref<boolean>(false);
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
|
||||
|
||||
const ACCESS_TOKEN_STATE_KEY = 'access_token';
|
||||
const ACCESS_TOKEN_COOKIE_KEY = 'access_token';
|
||||
|
||||
const tokenCookie = useCookie(ACCESS_TOKEN_COOKIE_KEY, { expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30) });
|
||||
const token = ref<string | undefined>();
|
||||
|
||||
export function signHeaders(headers?: Record<string, string>) {
|
||||
const { token } = useAccessToken()
|
||||
@@ -15,26 +14,12 @@ export const authorizationHeaderComputed = computed(() => {
|
||||
return token.value ? 'Bearer ' + token.value : '';
|
||||
});
|
||||
|
||||
function setToken(value: string) {
|
||||
tokenCookie.value = value;
|
||||
token.value = value;
|
||||
}
|
||||
|
||||
export function useAccessToken() {
|
||||
|
||||
const tokenCookie = useCookie(ACCESS_TOKEN_COOKIE_KEY, { expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30) })
|
||||
|
||||
const token = useState<string | undefined | null>(ACCESS_TOKEN_STATE_KEY);
|
||||
const needLoad = useState<boolean>('needAccessTokenLoad', () => true);
|
||||
|
||||
|
||||
const readToken = () => {
|
||||
token.value = tokenCookie.value;
|
||||
needLoad.value = false;
|
||||
}
|
||||
const setToken = (newToken: string) => {
|
||||
tokenCookie.value = newToken;
|
||||
token.value = tokenCookie.value;
|
||||
needLoad.value = false;
|
||||
}
|
||||
|
||||
if (needLoad.value == true) readToken();
|
||||
|
||||
|
||||
return { token, readToken, setToken, needLoad }
|
||||
if (!token.value) token.value = tokenCookie.value as any;
|
||||
return { setToken, token }
|
||||
}
|
||||
@@ -1,80 +1,47 @@
|
||||
import type { InternalApi } from 'nitropack';
|
||||
import type { WatchSource, WatchStopHandle } from 'vue';
|
||||
import type { StringExpressionOperator } from "mongoose";
|
||||
|
||||
type RefOrPrimitive<T> = T | Ref<T> | ComputedRef<T>
|
||||
|
||||
type NitroFetchRequest = Exclude<keyof InternalApi, `/_${string}` | `/api/_${string}`> | (string & {});
|
||||
|
||||
export type CustomFetchOptions = {
|
||||
watchProps?: WatchSource[],
|
||||
lazy?: boolean,
|
||||
method?: string,
|
||||
getBody?: () => Record<string, any>,
|
||||
watchKey?: string
|
||||
export type CustomOptions = {
|
||||
useSnapshotDates?: boolean,
|
||||
useActivePid?: boolean,
|
||||
slice?: RefOrPrimitive<string>,
|
||||
limit?: RefOrPrimitive<number | string>,
|
||||
custom?: Record<string, RefOrPrimitive<string>>
|
||||
}
|
||||
|
||||
type OnResponseCallback<TData> = (data: Ref<TData | undefined>) => any
|
||||
type OnRequestCallback = () => any
|
||||
const { token } = useAccessToken();
|
||||
const { projectId } = useProject();
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
function getValueFromRefOrPrimitive<T>(data?: T | Ref<T> | ComputedRef<T>) {
|
||||
if (!data) return;
|
||||
if (isRef(data)) return data.value;
|
||||
return data;
|
||||
}
|
||||
|
||||
const watchStopHandles: Record<string, WatchStopHandle> = {}
|
||||
export function useComputedHeaders(customOptions?: CustomOptions) {
|
||||
const useSnapshotDates = customOptions?.useSnapshotDates || true;
|
||||
const useActivePid = customOptions?.useActivePid || true;
|
||||
|
||||
export function useCustomFetch<T>(url: NitroFetchRequest, getHeaders: () => Record<string, string>, options?: CustomFetchOptions) {
|
||||
const headers = computed<Record<string, string>>(() => {
|
||||
|
||||
const pending = ref<boolean>(false);
|
||||
const data = ref<T | undefined>();
|
||||
const error = ref<Error | undefined>();
|
||||
|
||||
let onResponseCallback: OnResponseCallback<T> = () => { }
|
||||
let onRequestCallback: OnRequestCallback = () => { }
|
||||
|
||||
const onResponse = (callback: OnResponseCallback<T>) => {
|
||||
onResponseCallback = callback;
|
||||
}
|
||||
|
||||
const onRequest = (callback: OnRequestCallback) => {
|
||||
onRequestCallback = callback;
|
||||
}
|
||||
|
||||
const execute = async () => {
|
||||
onRequestCallback();
|
||||
pending.value = true;
|
||||
error.value = undefined;
|
||||
try {
|
||||
|
||||
data.value = await $fetch<T>(url, {
|
||||
headers: getHeaders(),
|
||||
method: (options?.method || 'GET') as any,
|
||||
body: options?.getBody ? JSON.stringify(options.getBody()) : undefined
|
||||
});
|
||||
|
||||
onResponseCallback(data);
|
||||
} catch (err) {
|
||||
error.value = err as Error;
|
||||
} finally {
|
||||
pending.value = false;
|
||||
const parsedCustom: Record<string, string> = {}
|
||||
const customKeys = Object.keys(customOptions?.custom || {});
|
||||
for (const key of customKeys) {
|
||||
parsedCustom[key] = getValueFromRefOrPrimitive((customOptions?.custom || {})[key]) ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.lazy !== true) {
|
||||
execute();
|
||||
}
|
||||
return {
|
||||
'Authorization': `Bearer ${token.value}`,
|
||||
'x-pid': useActivePid ? (projectId.value ?? '') : '',
|
||||
'x-from': useSnapshotDates ? (safeSnapshotDates.value.from ?? '') : '',
|
||||
'x-to': useSnapshotDates ? (safeSnapshotDates.value.to ?? '') : '',
|
||||
'x-slice': getValueFromRefOrPrimitive(customOptions?.slice) ?? '',
|
||||
'x-limit': getValueFromRefOrPrimitive(customOptions?.limit)?.toString() ?? '',
|
||||
...parsedCustom
|
||||
}
|
||||
})
|
||||
|
||||
if (options?.watchProps) {
|
||||
|
||||
const watchStop = watch(options.watchProps, () => {
|
||||
execute();
|
||||
});
|
||||
|
||||
const key = options?.watchKey || `${url}`;
|
||||
if (watchStopHandles[key]) watchStopHandles[key]();
|
||||
watchStopHandles[key] = watchStop;
|
||||
|
||||
console.log('Watchers:', Object.keys(watchStopHandles).length);
|
||||
|
||||
|
||||
}
|
||||
|
||||
const refresh = execute;
|
||||
|
||||
return { pending, execute, data, error, refresh, onResponse, onRequest };
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
|
||||
|
||||
export function createRequestOptions(method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', sign: boolean, body?: Record<string, any>, headers: Record<string, string> = {}) {
|
||||
|
||||
|
||||
const requestHeaders = sign ? signHeaders(headers) : headers;
|
||||
let requestBody;
|
||||
|
||||
if (method === 'POST' || method == 'PUT' || method == 'PATCH') {
|
||||
requestBody = body ? JSON.stringify(body) : undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
method,
|
||||
headers: requestHeaders,
|
||||
body: requestBody
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import type { Slice } from "@services/DateService";
|
||||
import DateService from "@services/DateService";
|
||||
import type { MetricsCounts } from "~/server/api/metrics/[project_id]/counts";
|
||||
import type { BrowsersAggregated } from "~/server/api/metrics/[project_id]/data/browsers";
|
||||
import type { CountriesAggregated } from "~/server/api/metrics/[project_id]/data/countries";
|
||||
import type { DevicesAggregated } from "~/server/api/metrics/[project_id]/data/devices";
|
||||
import type { CustomEventsAggregated } from "~/server/api/metrics/[project_id]/data/events";
|
||||
import type { OssAggregated } from "~/server/api/metrics/[project_id]/data/oss";
|
||||
import type { ReferrersAggregated } from "~/server/api/metrics/[project_id]/data/referrers";
|
||||
import type { VisitsWebsiteAggregated } from "~/server/api/metrics/[project_id]/data/websites";
|
||||
import type { MetricsTimeline } from "~/server/api/metrics/[project_id]/timeline/generic";
|
||||
|
||||
export function useMetricsData() {
|
||||
const activeProject = useActiveProject();
|
||||
const metricsInfo = useFetch<MetricsCounts>(`/api/metrics/${activeProject.value?._id}/counts`, {
|
||||
...signHeaders(),
|
||||
lazy: true
|
||||
});
|
||||
return metricsInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// const { safeSnapshotDates, snapshot } = useSnapshot()
|
||||
// const activeProject = useActiveProject();
|
||||
|
||||
// const createFromToHeaders = (headers: Record<string, string> = {}) => ({
|
||||
// 'x-from': safeSnapshotDates.value.from,
|
||||
// 'x-to': safeSnapshotDates.value.to,
|
||||
// ...headers
|
||||
// });
|
||||
|
||||
// const createFromToBody = (body: Record<string, any> = {}) => ({
|
||||
// from: safeSnapshotDates.value.from,
|
||||
// to: safeSnapshotDates.value.to,
|
||||
// ...body
|
||||
// });
|
||||
|
||||
|
||||
|
||||
// export function useFirstInteractionData() {
|
||||
// const activeProject = useActiveProject();
|
||||
// const metricsInfo = useFetch<boolean>(`/api/metrics/${activeProject.value?._id}/first_interaction`, signHeaders());
|
||||
// return metricsInfo;
|
||||
// }
|
||||
|
||||
|
||||
// export function useTimelineAdvanced<T>(endpoint: string, slice: Ref<Slice>, customBody: Object = {}) {
|
||||
// const response = useCustomFetch<T>(
|
||||
// `/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`,
|
||||
// () => signHeaders({ 'Content-Type': 'application/json' }).headers, {
|
||||
// method: 'POST',
|
||||
// getBody: () => createFromToBody({ slice: slice.value, ...customBody }),
|
||||
// lazy: true,
|
||||
// watchProps: [snapshot, slice]
|
||||
// });
|
||||
// return response;
|
||||
// }
|
||||
|
||||
|
||||
// export function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers' | 'events_stacked', slice: Ref<Slice>) {
|
||||
// return useTimelineAdvanced<{ _id: string, count: number }[]>(endpoint, slice);
|
||||
// }
|
||||
|
||||
// export async function useReferrersTimeline(referrer: string, slice: Ref<Slice>) {
|
||||
// return await useTimelineAdvanced<{ _id: string, count: number }[]>('referrers', slice, { referrer });
|
||||
// }
|
||||
|
||||
// export function useEventsStackedTimeline(slice: Ref<Slice>) {
|
||||
// return useTimelineAdvanced<{ _id: string, name: string, count: number }[]>('events_stacked', slice);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// export async function useTimelineDataRaw(timelineEndpointName: string, slice: SliceName) {
|
||||
// const activeProject = useActiveProject();
|
||||
|
||||
// const response = await $fetch<{ data: MetricsTimeline[], from: string, to: string }>(
|
||||
// `/api/metrics/${activeProject.value?._id}/timeline/${timelineEndpointName}`, {
|
||||
// method: 'POST',
|
||||
// ...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
// body: JSON.stringify({ slice }),
|
||||
// });
|
||||
|
||||
// return response;
|
||||
// }
|
||||
|
||||
// export async function useTimelineData(timelineEndpointName: string, slice: SliceName) {
|
||||
// const response = await useTimelineDataRaw(timelineEndpointName, slice);
|
||||
// if (!response) return;
|
||||
// const fixed = fixMetrics(response, slice);
|
||||
// return fixed;
|
||||
// }
|
||||
|
||||
// export function usePagesData(website: string, limit: number = 10) {
|
||||
// const activeProject = useActiveProject();
|
||||
|
||||
// const res = useFetch<VisitsWebsiteAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/pages`, {
|
||||
// ...signHeaders({
|
||||
// 'x-query-limit': limit.toString(),
|
||||
// 'x-website-name': website
|
||||
// }),
|
||||
// key: `pages_data:${website}:${limit}`,
|
||||
// lazy: true
|
||||
// });
|
||||
|
||||
// return res;
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
// export function useWebsitesData(limit: number = 10) {
|
||||
// const res = useCustomFetch<VisitsWebsiteAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/websites`,
|
||||
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
|
||||
// { lazy: false, watchProps: [snapshot] }
|
||||
// );
|
||||
// return res;
|
||||
// }
|
||||
|
||||
// export function useEventsData(limit: number = 10) {
|
||||
// const res = useCustomFetch<CustomEventsAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/events`,
|
||||
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
|
||||
// { lazy: false, watchProps: [snapshot] }
|
||||
// );
|
||||
// return res;
|
||||
// }
|
||||
|
||||
// export function useReferrersData(limit: number = 10) {
|
||||
// const res = useCustomFetch<ReferrersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/referrers`,
|
||||
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
|
||||
// { lazy: false, watchProps: [snapshot] }
|
||||
// );
|
||||
// return res;
|
||||
// }
|
||||
|
||||
// export function useBrowsersData(limit: number = 10) {
|
||||
// const res = useCustomFetch<BrowsersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/browsers`,
|
||||
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
|
||||
// { lazy: false, watchProps: [snapshot] }
|
||||
// );
|
||||
// return res;
|
||||
// }
|
||||
|
||||
// export function useOssData(limit: number = 10) {
|
||||
// const res = useCustomFetch<OssAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/oss`,
|
||||
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
|
||||
// { lazy: false, watchProps: [snapshot] }
|
||||
// );
|
||||
// return res;
|
||||
// }
|
||||
|
||||
// export function useGeolocationData(limit: number = 10) {
|
||||
// const res = useCustomFetch<CountriesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/countries`,
|
||||
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
|
||||
// { lazy: false, watchProps: [snapshot] }
|
||||
// );
|
||||
// return res;
|
||||
// }
|
||||
|
||||
// export function useDevicesData(limit: number = 10) {
|
||||
// const res = useCustomFetch<DevicesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/devices`,
|
||||
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
|
||||
// { lazy: false, watchProps: [snapshot] }
|
||||
// );
|
||||
// return res;
|
||||
// }
|
||||
@@ -1,13 +1,11 @@
|
||||
|
||||
|
||||
export function isLiveDemo() {
|
||||
const route = useRoute();
|
||||
const route = useRoute();
|
||||
export const isLiveDemo = computed(() => {
|
||||
return route.path == '/live_demo';
|
||||
}
|
||||
})
|
||||
|
||||
const liveDemoData = useFetch('/api/live_demo');
|
||||
|
||||
export function useLiveDemo() {
|
||||
return liveDemoData;
|
||||
}
|
||||
export function useLiveDemo() { return liveDemoData; }
|
||||
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
import type { AuthContext } from "~/server/middleware/01-authorization";
|
||||
|
||||
const LOGGED_USER_STATE_KEY = 'logged_user';
|
||||
|
||||
const loggedUser = ref<AuthContext | undefined>();
|
||||
|
||||
export function useLoggedUser() {
|
||||
const loggedUserState = useState<AuthContext | undefined>(LOGGED_USER_STATE_KEY);
|
||||
return loggedUserState;
|
||||
}
|
||||
const setLoggedUser = (authContext?: AuthContext) => {
|
||||
loggedUser.value = authContext;
|
||||
};
|
||||
|
||||
export function setLoggedUser(authContext?: AuthContext) {
|
||||
useLoggedUser().value = authContext;
|
||||
}
|
||||
|
||||
export const isAdminHidden = ref<boolean>(false);
|
||||
|
||||
export function useUserRoles() {
|
||||
const isLogged = computed(() => {
|
||||
return loggedUser.value?.logged;
|
||||
})
|
||||
|
||||
function getUserRoles() {
|
||||
const isPremium = computed(() => {
|
||||
const loggedUser = useLoggedUser();
|
||||
if (!loggedUser.value?.logged) return false;
|
||||
return loggedUser.value.user.roles.includes('PREMIUM');
|
||||
});
|
||||
const isAdmin = computed(() => {
|
||||
const loggedUser = useLoggedUser();
|
||||
if (!loggedUser.value?.logged) return false;
|
||||
return loggedUser.value.user.roles.includes('ADMIN');
|
||||
});
|
||||
|
||||
return { isPremium, isAdmin }
|
||||
}
|
||||
|
||||
export const isAdminHidden = ref<boolean>(false);
|
||||
|
||||
export function useLoggedUser() {
|
||||
return {
|
||||
isLogged,
|
||||
user: loggedUser,
|
||||
userRoles: getUserRoles(),
|
||||
setLoggedUser
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
|
||||
|
||||
const onlineUsers = ref<number>(-1);
|
||||
|
||||
async function getOnlineUsers() {
|
||||
const activeProject = useActiveProject();
|
||||
if (!activeProject.value) return onlineUsers.value = -1;
|
||||
const online = await $fetch<number>(`/api/metrics/${activeProject.value._id}/live_users`, signHeaders());
|
||||
onlineUsers.value = online;
|
||||
}
|
||||
const onlineUsers = useFetch<number>(`/api/data/live_users`, {
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }), immediate: false
|
||||
});
|
||||
|
||||
let watching: any;
|
||||
|
||||
function startWatching(instant: boolean = true) {
|
||||
if (instant) getOnlineUsers();
|
||||
if (instant) onlineUsers.execute();
|
||||
watching = setInterval(async () => {
|
||||
await getOnlineUsers();
|
||||
onlineUsers.refresh();
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
@@ -26,7 +21,6 @@ export function useOnlineUsers() {
|
||||
|
||||
return {
|
||||
onlineUsers,
|
||||
getOnlineUsers,
|
||||
startWatching,
|
||||
stopWatching
|
||||
}
|
||||
|
||||
84
dashboard/composables/useProject.ts
Normal file
84
dashboard/composables/useProject.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
|
||||
import type { TProject } from "@schema/ProjectSchema";
|
||||
import { ProjectSnapshotModel } from "@schema/ProjectSnapshot";
|
||||
|
||||
const { token } = useAccessToken();
|
||||
|
||||
const projectsRequest = useFetch<TProject[]>('/api/project/list', {
|
||||
headers: computed(() => {
|
||||
return {
|
||||
'Authorization': `Bearer ${token.value}`
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const guestProjectsRequest = useFetch<TProject[]>('/api/project/list_guest', {
|
||||
headers: computed(() => {
|
||||
return {
|
||||
'Authorization': `Bearer ${token.value}`
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const projectList = computed(() => {
|
||||
return projectsRequest.data.value;
|
||||
});
|
||||
|
||||
const allProjectList = computed(() => {
|
||||
return [...(projectsRequest.data.value || []), ...(guestProjectsRequest.data.value || [])]
|
||||
})
|
||||
|
||||
const guestProjectList = computed(() => {
|
||||
return guestProjectsRequest.data.value;
|
||||
})
|
||||
|
||||
const refreshProjectsList = () => projectsRequest.refresh();
|
||||
|
||||
const activeProjectId = ref<string | undefined>();
|
||||
|
||||
const setActiveProject = (project_id: string) => {
|
||||
activeProjectId.value = project_id;
|
||||
localStorage.setItem('active_pid', project_id);
|
||||
}
|
||||
|
||||
const project = computed(() => {
|
||||
|
||||
if (isLiveDemo.value) return useLiveDemo().data.value;
|
||||
|
||||
if (!allProjectList.value) return;
|
||||
if (allProjectList.value.length == 0) return;
|
||||
|
||||
if (activeProjectId.value) {
|
||||
const target = allProjectList.value.find(e => e._id.toString() == activeProjectId.value);
|
||||
if (target) return target;
|
||||
}
|
||||
|
||||
const savedActive = localStorage.getItem('active_pid');
|
||||
if (savedActive) {
|
||||
const target = allProjectList.value.find(e => e._id.toString() == savedActive);
|
||||
if (target) {
|
||||
activeProjectId.value = savedActive;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
activeProjectId.value = allProjectList.value[0]._id.toString();
|
||||
return allProjectList.value[0];
|
||||
})
|
||||
|
||||
const projectId = computed(() => project.value?._id.toString())
|
||||
|
||||
|
||||
const isGuest = computed(() => {
|
||||
return (projectList.value || []).find(e => e._id.toString() === activeProjectId.value) == undefined;
|
||||
})
|
||||
|
||||
export function useProject() {
|
||||
|
||||
const actions = {
|
||||
refreshProjectsList,
|
||||
setActiveProject
|
||||
}
|
||||
|
||||
return { project, allProjectList, guestProjectList, projectList, actions, projectId, isGuest }
|
||||
}
|
||||
8
dashboard/composables/useRefreshKey.ts
Normal file
8
dashboard/composables/useRefreshKey.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
const { project } = useProject()
|
||||
|
||||
export const refreshKey = computed(() => {
|
||||
if (!project.value) return 'null';
|
||||
return project.value._id.toString();
|
||||
})
|
||||
@@ -1,31 +1,36 @@
|
||||
import type { TProjectSnapshot } from "@schema/ProjectSnapshot";
|
||||
|
||||
|
||||
const { projectId, project } = useProject();
|
||||
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
'Authorization': signHeaders().headers.Authorization,
|
||||
'x-pid': projectId.value ?? ''
|
||||
}
|
||||
});
|
||||
const remoteSnapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', {
|
||||
...signHeaders(),
|
||||
immediate: false
|
||||
headers
|
||||
});
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
watch(activeProject, async () => {
|
||||
watch(project, async () => {
|
||||
await remoteSnapshots.refresh();
|
||||
snapshot.value = isLiveDemo() ? snapshots.value[0] : snapshots.value[1];
|
||||
snapshot.value = isLiveDemo.value ? snapshots.value[0] : snapshots.value[1];
|
||||
});
|
||||
|
||||
const snapshots = computed(() => {
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const getDefaultSnapshots: () => TProjectSnapshot[] = () => [
|
||||
{
|
||||
project_id: activeProject.value?._id as any,
|
||||
project_id: project.value?._id as any,
|
||||
_id: 'default0' as any,
|
||||
name: 'All',
|
||||
from: new Date(activeProject.value?.created_at || 0),
|
||||
from: new Date(project.value?.created_at || 0),
|
||||
to: new Date(Date.now()),
|
||||
color: '#CCCCCC'
|
||||
},
|
||||
{
|
||||
project_id: activeProject.value?._id as any,
|
||||
project_id: project.value?._id as any,
|
||||
_id: 'default1' as any,
|
||||
name: 'Last month',
|
||||
from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30),
|
||||
@@ -33,7 +38,7 @@ const snapshots = computed(() => {
|
||||
color: '#00CC00'
|
||||
},
|
||||
{
|
||||
project_id: activeProject.value?._id as any,
|
||||
project_id: project.value?._id as any,
|
||||
_id: 'default2' as any,
|
||||
name: 'Last week',
|
||||
from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
|
||||
@@ -41,7 +46,7 @@ const snapshots = computed(() => {
|
||||
color: '#0F02D2'
|
||||
},
|
||||
{
|
||||
project_id: activeProject.value?._id as any,
|
||||
project_id: project.value?._id as any,
|
||||
_id: 'default3' as any,
|
||||
name: 'Last day',
|
||||
from: new Date(Date.now() - 1000 * 60 * 60 * 24),
|
||||
@@ -56,7 +61,7 @@ const snapshots = computed(() => {
|
||||
];
|
||||
})
|
||||
|
||||
const snapshot = ref<TProjectSnapshot>(isLiveDemo() ? snapshots.value[0] : snapshots.value[1]);
|
||||
const snapshot = ref<TProjectSnapshot>(isLiveDemo.value ? snapshots.value[0] : snapshots.value[1]);
|
||||
|
||||
const safeSnapshotDates = computed(() => {
|
||||
const from = new Date(snapshot.value?.from || 0).toISOString();
|
||||
@@ -75,8 +80,5 @@ const snapshotDuration = computed(() => {
|
||||
});
|
||||
|
||||
export function useSnapshot() {
|
||||
if (remoteSnapshots.status.value === 'idle') {
|
||||
remoteSnapshots.execute();
|
||||
}
|
||||
return { snapshot, snapshots, safeSnapshotDates, updateSnapshots, snapshotDuration }
|
||||
}
|
||||
@@ -4,10 +4,8 @@ import type { Section } from '~/components/CVerticalNavigation.vue';
|
||||
|
||||
import { Lit } from 'litlyx-js';
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const isPremium = computed(() => {
|
||||
return activeProject.value?.premium;
|
||||
});
|
||||
const { userRoles, isLogged } = useLoggedUser();
|
||||
const { project } = useProject();
|
||||
|
||||
const pricingDrawer = usePricingDrawer();
|
||||
|
||||
@@ -15,32 +13,37 @@ const sections: Section[] = [
|
||||
{
|
||||
title: '',
|
||||
entries: [
|
||||
{ label: 'Dashboard', to: '/', icon: 'fal fa-table-layout' },
|
||||
{ label: 'Events', to: '/events', icon: 'fal fa-square-bolt' },
|
||||
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
||||
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
||||
{ label: 'AI Analyst', to: '/analyst', icon: 'fal fa-sparkles' },
|
||||
{ label: 'Security', to: '/security', icon: 'fal fa-shield' },
|
||||
{ label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||
{ label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||
{ label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
|
||||
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||
// { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
|
||||
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
|
||||
{
|
||||
grow: true,
|
||||
label: 'Documentation', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true,
|
||||
label: 'Docs', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true,
|
||||
action() { Lit.event('docs_clicked') },
|
||||
},
|
||||
{
|
||||
label: 'Slack support', icon: 'fab fa-slack',
|
||||
to: '#',
|
||||
premiumOnly: true,
|
||||
action() {
|
||||
if (isGuest.value === true) return;
|
||||
if (isPremium.value === true) {
|
||||
window.open('https://join.slack.com/t/litlyx/shared_invite/zt-2q3oawn29-hZlu_fBUBlc4052Ooe3FZg', '_blank');
|
||||
} else {
|
||||
pricingDrawer.visible.value = true;
|
||||
}
|
||||
},
|
||||
label: 'Discord support', icon: 'fab fa-discord',
|
||||
to: 'https://discord.gg/9cQykjsmWX',
|
||||
external: true,
|
||||
},
|
||||
// {
|
||||
// label: 'Slack support', icon: 'fab fa-slack',
|
||||
// to: '#',
|
||||
// premiumOnly: true,
|
||||
// action() {
|
||||
// if (isLogged.value === true) return;
|
||||
// if (userRoles.isPremium.value === true) {
|
||||
// window.open('https://join.slack.com/t/litlyx/shared_invite/zt-2q3oawn29-hZlu_fBUBlc4052Ooe3FZg', '_blank');
|
||||
// } else {
|
||||
// pricingDrawer.visible.value = true;
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -78,7 +81,7 @@ const { isOpen, close, open } = useMenu();
|
||||
</CVerticalNavigation>
|
||||
|
||||
|
||||
<div class="overflow-hidden w-full bg-lyx-background-light relative h-full">
|
||||
<div class="overflow-hidden w-full bg-lyx-background relative h-full">
|
||||
|
||||
<div v-if="showDialog" class="barrier w-full h-full z-[34] absolute bg-black/50 backdrop-blur-[2px]">
|
||||
<i
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { AuthContext } from "~/server/middleware/01-authorization";
|
||||
|
||||
async function executeUserLogin(token: string): Promise<[true, AuthContext] | [false, null]> {
|
||||
const user = await $fetch<AuthContext>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
console.log('USER RESPSONSE', user);
|
||||
if (!user) return [false, null];
|
||||
if (user.logged == false) return [false, null];
|
||||
return [true, user];
|
||||
@@ -26,6 +25,7 @@ async function handleUserLogin(authContext?: AuthContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { setLoggedUser } = useLoggedUser();
|
||||
setLoggedUser(newContext);
|
||||
|
||||
}
|
||||
@@ -34,13 +34,22 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
|
||||
if (!to.name) return;
|
||||
|
||||
const loggedUser = useLoggedUser();
|
||||
await handleUserLogin(loggedUser.value);
|
||||
const { user } = useLoggedUser();
|
||||
|
||||
if (loggedUser.value?.logged) {
|
||||
if (to.path == '/login') return '/';
|
||||
await handleUserLogin(user.value);
|
||||
|
||||
if (user.value?.logged) {
|
||||
if (to.path == '/login' || to.path == '/register') return '/';
|
||||
} else {
|
||||
if (to.path != '/login' && to.path != '/live_demo') return '/login';
|
||||
if (
|
||||
to.path != '/login' &&
|
||||
to.path != '/register' &&
|
||||
to.path != '/live_demo' &&
|
||||
to.path != '/jwt_login'
|
||||
) {
|
||||
console.log('REDIRECT TO LOGIN', to.path);
|
||||
return '/login';
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"chartjs-plugin-annotation": "^2.2.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"dayjs": "^1.11.11",
|
||||
"google-auth-library": "^9.9.0",
|
||||
"google-auth-library": "^9.10.0",
|
||||
"googleapis": "^144.0.0",
|
||||
"highlight.js": "^11.10.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"litlyx-js": "^1.0.2",
|
||||
|
||||
@@ -138,7 +138,7 @@ async function resetCount(project_id: string) {
|
||||
<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> Hide from the bar </div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -4,17 +4,18 @@ import VueMarkdown from 'vue-markdown-render';
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const { project } = useProject();
|
||||
|
||||
const { data: chatsList, refresh: reloadChatsList } = useFetch(`/api/ai/${activeProject.value?._id}/chats_list`, {
|
||||
...signHeaders()
|
||||
const { data: chatsList, refresh: reloadChatsList } = useFetch(`/api/ai/chats_list`, {
|
||||
headers: useComputedHeaders({ useSnapshotDates: false })
|
||||
});
|
||||
|
||||
|
||||
|
||||
const viewChatsList = computed(() => (chatsList.value || []).toReversed());
|
||||
|
||||
const { data: chatsRemaining, refresh: reloadChatsRemaining } = useFetch(`/api/ai/${activeProject.value?._id}/chats_remaining`, signHeaders());
|
||||
const { data: chatsRemaining, refresh: reloadChatsRemaining } = useFetch(`/api/ai/chats_remaining`, {
|
||||
headers: useComputedHeaders({ useSnapshotDates: false })
|
||||
});
|
||||
|
||||
const currentText = ref<string>("");
|
||||
const loading = ref<boolean>(false);
|
||||
@@ -27,7 +28,7 @@ const scroller = ref<HTMLDivElement | null>(null);
|
||||
async function sendMessage() {
|
||||
|
||||
if (loading.value) return;
|
||||
if (!activeProject.value) return;
|
||||
if (!project.value) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
@@ -42,11 +43,15 @@ async function sendMessage() {
|
||||
|
||||
try {
|
||||
|
||||
const res = await $fetch(`/api/ai/${activeProject.value._id.toString()}/send_message`, {
|
||||
const res = await $fetch(`/api/ai/send_message`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
...signHeaders({ 'Content-Type': 'application/json' })
|
||||
headers: useComputedHeaders({
|
||||
useSnapshotDates: false,
|
||||
custom: { 'Content-Type': 'application/json' }
|
||||
}).value
|
||||
});
|
||||
|
||||
currentChatMessages.value.push({ role: 'assistant', content: res.content || 'nocontent', charts: res.charts.map(e => JSON.parse(e)) });
|
||||
|
||||
await reloadChatsRemaining();
|
||||
@@ -74,7 +79,7 @@ async function sendMessage() {
|
||||
|
||||
async function openChat(chat_id?: string) {
|
||||
menuOpen.value = false;
|
||||
if (!activeProject.value) return;
|
||||
if (!project.value) return;
|
||||
|
||||
currentChatMessages.value = [];
|
||||
|
||||
@@ -83,7 +88,9 @@ async function openChat(chat_id?: string) {
|
||||
return;
|
||||
}
|
||||
currentChatId.value = chat_id;
|
||||
const messages = await $fetch(`/api/ai/${activeProject.value._id}/${chat_id}/get_messages`, signHeaders());
|
||||
const messages = await $fetch(`/api/ai/${chat_id}/get_messages`, {
|
||||
headers: useComputedHeaders({useSnapshotDates:false}).value
|
||||
});
|
||||
if (!messages) return;
|
||||
|
||||
currentChatMessages.value = messages.map(e => ({ ...e, charts: e.charts.map(k => JSON.parse(k)) })) as any;
|
||||
@@ -117,14 +124,16 @@ const defaultPrompts = [
|
||||
]
|
||||
|
||||
async function deleteChat(chat_id: string) {
|
||||
if (!activeProject.value) return;
|
||||
if (!project.value) return;
|
||||
const sure = confirm("Are you sure to delete the chat ?");
|
||||
if (!sure) return;
|
||||
if (currentChatId.value === chat_id) {
|
||||
currentChatId.value = "";
|
||||
currentChatMessages.value = [];
|
||||
}
|
||||
await $fetch(`/api/ai/${activeProject.value._id}/${chat_id}/delete`, signHeaders());
|
||||
await $fetch(`/api/ai/${chat_id}/delete`, {
|
||||
headers: useComputedHeaders({useSnapshotDates:false}).value
|
||||
});
|
||||
await reloadChatsList();
|
||||
}
|
||||
|
||||
@@ -133,23 +142,21 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="w-full h-full overflow-y-hidden">
|
||||
|
||||
<div class="flex flex-row h-full">
|
||||
<div class="flex flex-row h-full overflow-y-hidden">
|
||||
|
||||
<div class="flex-[5] py-8 flex flex-col items-center relative bg-lyx-background-light">
|
||||
<div class="flex-[5] py-8 flex h-full flex-col items-center relative overflow-y-hidden">
|
||||
|
||||
<div class="flex flex-col items-center mt-[20vh] px-28" v-if="currentChatMessages.length == 0">
|
||||
<div class="w-[10rem]">
|
||||
<div class="flex flex-col items-center xl:mt-[20vh] px-8 xl:px-28"
|
||||
v-if="currentChatMessages.length == 0">
|
||||
<div class="w-[7rem] xl:w-[10rem]">
|
||||
<img :src="'analyst.png'" class="w-full h-full">
|
||||
</div>
|
||||
<div v-if="!isGuest" class="poppins text-[1.2rem]">
|
||||
How can i help you today?
|
||||
<div class="poppins text-[1.2rem] text-center">
|
||||
Ask me anything about your data
|
||||
</div>
|
||||
<div v-if="isGuest" class="poppins text-[1.2rem]">
|
||||
Im not allowed to help guests :c
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-6" v-if="!isGuest">
|
||||
<div class="flex flex-col xl:grid xl:grid-cols-2 gap-4 mt-6">
|
||||
<div v-for="prompt of defaultPrompts" @click="currentText = prompt"
|
||||
class="bg-lyx-widget-light hover:bg-lyx-widget-lighter cursor-pointer p-4 rounded-lg poppins text-center whitespace-pre-wrap flex items-center justify-center text-[.9rem]">
|
||||
{{ prompt }}
|
||||
@@ -201,17 +208,15 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div v-if="!isGuest" class="flex gap-2 items-center absolute bottom-8 left-0 w-full px-10 xl:px-28">
|
||||
<div class="flex gap-2 items-center md:absolute fixed bottom-8 left-0 w-full px-10 xl:px-28">
|
||||
<input @keydown="onKeyDown" v-model="currentText"
|
||||
class="bg-lyx-widget-light w-full focus:outline-none px-4 py-2 rounded-lg" type="text">
|
||||
<div @click="sendMessage()"
|
||||
class="bg-lyx-widget-light hhover:bg-lyx-widget-lighter cursor-pointer px-4 py-2 rounded-full">
|
||||
class="bg-lyx-widget-light hhover:bg-lyx-widget-light cursor-pointer px-4 py-2 rounded-full">
|
||||
<i class="far fa-arrow-up"></i>
|
||||
</div>
|
||||
<div @click="menuOpen = !menuOpen"
|
||||
class="bg-lyx-widget-light lg:hidden hhover:bg-lyx-widget-lighter cursor-pointer px-4 py-2 rounded-full">
|
||||
class="bg-lyx-widget-light xl:hidden hhover:bg-lyx-widget-light cursor-pointer px-4 py-2 rounded-full">
|
||||
<i class="far fa-message"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,39 +225,33 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
||||
|
||||
|
||||
<div :class="{
|
||||
'absolute': menuOpen,
|
||||
'hidden lg:flex': !menuOpen
|
||||
}" class="flex-[2] bg-lyx-widget-light p-6 flex flex-col gap-4 h-full overflow-hidden">
|
||||
'absolute top-0 left-0 w-full': menuOpen,
|
||||
'hidden xl:flex': !menuOpen
|
||||
}" class="flex-[2] bg-lyx-background-light p-6 flex flex-col gap-4 h-full overflow-hidden">
|
||||
|
||||
<div class="gap-2 flex flex-col">
|
||||
<div class="lg:hidden absolute right-4 top-4 text-[1.5rem]">
|
||||
<div class="xl:hidden absolute right-6 top-2 text-[1.5rem]">
|
||||
<i @click="menuOpen = false" class="fas fa-close cursor-pointer"></i>
|
||||
</div>
|
||||
<div class="poppins font-semibold text-[1.5rem]">
|
||||
What Lit can do for you?
|
||||
</div>
|
||||
<div class="poppins text-text/75">
|
||||
Ask anything from your data history, visualize and overlap charts, explore events or metadata,
|
||||
and enjoy a highly personalized data analysis experience.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 items-center pt-3">
|
||||
<div class="bg-accent w-5 h-5 rounded-full animate-pulse">
|
||||
<div class="flex justify-between items-center pt-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-accent w-5 h-5 rounded-full animate-pulse">
|
||||
</div>
|
||||
<div class="manrope font-semibold text-text-dirty"> {{ chatsRemaining }} remaining requests
|
||||
</div>
|
||||
</div>
|
||||
<div class="manrope font-semibold"> {{ chatsRemaining }} remaining AI requests </div>
|
||||
<LyxUiButton type="primary" class="text-[.9rem] text-center " @click="pricingDrawerVisible = true">
|
||||
Upgrade
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
|
||||
<LyxUiButton type="primary" class="text-[.9rem] text-center w-full"
|
||||
@click="pricingDrawerVisible = true">
|
||||
Upgrade plan for more requests
|
||||
</LyxUiButton>
|
||||
|
||||
<div class="poppins font-semibold text-[1.1rem]"> History </div>
|
||||
|
||||
<div class="px-2">
|
||||
<div @click="openChat()"
|
||||
class="bg-lyx-widget-lighter cursor-pointer hover:bg-lyx-widget rounded-lg px-4 py-3 poppins flex gap-4 items-center">
|
||||
class="bg-lyx-widget-light cursor-pointer hover:bg-lyx-widget rounded-lg px-4 py-3 poppins flex gap-4 items-center">
|
||||
<div> <i class="fas fa-plus"></i> </div>
|
||||
<div> New chat </div>
|
||||
</div>
|
||||
@@ -262,7 +261,7 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
||||
<div class="overflow-y-auto">
|
||||
<div class="flex flex-col gap-2 px-2">
|
||||
<div :class="{ '!bg-accent/60': chat._id.toString() === currentChatId }"
|
||||
class="flex rounded-lg items-center gap-4 w-full px-4 bg-lyx-widget-lighter hover:bg-lyx-widget"
|
||||
class="flex text-lyx-text-dark text-[.9rem] font-light rounded-lg items-center gap-4 w-full px-4 bg-lyx-widget-light hover:bg-lyx-widget"
|
||||
v-for="chat of viewChatsList">
|
||||
<i @click="deleteChat(chat._id.toString())"
|
||||
class="far fa-trash hover:text-gray-300 cursor-pointer"></i>
|
||||
@@ -299,7 +298,7 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
|
||||
p {
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1em;
|
||||
|
||||
@@ -4,9 +4,9 @@ import type { MetricsCounts } from '~/server/api/metrics/[project_id]/counts';
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const { project } = useProject();
|
||||
|
||||
const isPremium = computed(() => (activeProject.value?.premium_type || 0) > 0);
|
||||
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
|
||||
|
||||
const metricsInfo = ref<number>(0);
|
||||
|
||||
@@ -28,13 +28,13 @@ 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(),
|
||||
const { data: tableData, pending: loadingData } = await useFetch<any[]>(() =>
|
||||
`/api/metrics/${project.value?._id}/query?type=1&orderBy=${sort.value.column}&order=${sort.value.direction}&page=${page.value}&limit=${itemsPerPage}`, {
|
||||
...signHeaders(), lazy: true
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const counts = await $fetch<MetricsCounts>(`/api/metrics/${activeProject.value?._id}/counts`, signHeaders());
|
||||
const counts = await $fetch<MetricsCounts>(`/api/metrics/${project.value?._id}/counts`, signHeaders());
|
||||
metricsInfo.value = counts.eventsCount;
|
||||
});
|
||||
|
||||
@@ -42,12 +42,14 @@ const creatingCsv = ref<boolean>(false);
|
||||
|
||||
async function downloadCSV() {
|
||||
creatingCsv.value = true;
|
||||
const result = await $fetch(`/api/project/generate_csv?mode=events&slice=${options.indexOf(selectedTimeFrom.value)}`, signHeaders());
|
||||
const blob = new Blob([result], { type: 'text/csv' });
|
||||
const result = await $fetch(`/api/project/generate_csv?mode=events&slice=${options.indexOf(selectedTimeFrom.value)}`, {
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value
|
||||
});
|
||||
const blob = new Blob([result as any], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'ReportVisits.csv';
|
||||
a.download = 'ReportEvents.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
@@ -93,11 +95,18 @@ function goToUpgrade() {
|
||||
<div> It can take a few minutes </div>
|
||||
</div>
|
||||
<div class="w-[15rem] flex flex-col gap-0">
|
||||
<USelectMenu v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
||||
</div>
|
||||
|
||||
<div v-if="isPremium" @click="downloadCSV()"
|
||||
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
||||
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
|
||||
Download CSV
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<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>
|
||||
@@ -4,9 +4,9 @@ import type { MetricsCounts } from '~/server/api/metrics/[project_id]/counts';
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const { project } = useProject();
|
||||
|
||||
const isPremium = computed(() => (activeProject.value?.premium_type || 0) > 0);
|
||||
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
|
||||
|
||||
const metricsInfo = ref<number>(0);
|
||||
|
||||
@@ -34,13 +34,13 @@ 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(),
|
||||
const { data: tableData, pending: loadingData } = await useFetch<any[]>(() =>
|
||||
`/api/metrics/${project.value?._id}/query?type=0&orderBy=${sort.value.column}&order=${sort.value.direction}&page=${page.value}&limit=${itemsPerPage}`, {
|
||||
...signHeaders(), lazy: true
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const counts = await $fetch<MetricsCounts>(`/api/metrics/${activeProject.value?._id}/counts`, signHeaders());
|
||||
const counts = await $fetch<MetricsCounts>(`/api/metrics/${project.value?._id}/counts`, signHeaders());
|
||||
metricsInfo.value = counts.visitsCount;
|
||||
});
|
||||
|
||||
@@ -49,8 +49,10 @@ const creatingCsv = ref<boolean>(false);
|
||||
|
||||
async function downloadCSV() {
|
||||
creatingCsv.value = true;
|
||||
const result = await $fetch(`/api/project/generate_csv?mode=visits&slice=${options.indexOf(selectedTimeFrom.value)}`, signHeaders());
|
||||
const blob = new Blob([result], { type: 'text/csv' });
|
||||
const result = await $fetch(`/api/project/generate_csv?mode=visits&slice=${options.indexOf(selectedTimeFrom.value)}`, {
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value
|
||||
});
|
||||
const blob = new Blob([result as any], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -78,7 +80,6 @@ function goToUpgrade() {
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
|
||||
@@ -99,11 +100,18 @@ function goToUpgrade() {
|
||||
<div> It can take a few minutes </div>
|
||||
</div>
|
||||
<div class="w-[15rem] flex flex-col gap-0">
|
||||
<USelectMenu v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
||||
</div>
|
||||
|
||||
<div v-if="isPremium" @click="downloadCSV()"
|
||||
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
||||
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
|
||||
Download CSV
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,11 +10,7 @@ const selectLabelsEvents = [
|
||||
];
|
||||
const eventsStackedSelectIndex = ref<number>(0);
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
const { snapshot } = useSnapshot();
|
||||
|
||||
const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProject.value?._id.toString()}`);
|
||||
|
||||
const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeaders({ custom: { 'x-schema': 'events' } }), lazy: true });
|
||||
|
||||
</script>
|
||||
|
||||
@@ -22,9 +18,31 @@ const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProje
|
||||
<template>
|
||||
<div class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
|
||||
|
||||
<div class="flex gap-6 flex-col xl:flex-row h-full">
|
||||
|
||||
<CardTitled :key="refreshKey" class="p-4 flex-[4] w-full h-full" title="Events"
|
||||
<LyxUiCard class="w-full flex justify-between items-center lg:flex-row flex-col gap-6 lg:gap-0">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div>
|
||||
Total events: {{ eventsData.data.value?.[0]?.count || '0' }}
|
||||
</div>
|
||||
<div v-if="(eventsData.data.value?.[0]?.count || 0) === 0">
|
||||
Waiting for your first event...
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<LyxUiButton type="secondary" target="_blank" to="https://docs.litlyx.com/custom-events">
|
||||
Trigger your first event
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
</LyxUiCard>
|
||||
|
||||
|
||||
<div>
|
||||
<BarCardEvents :key="refreshKey"></BarCardEvents>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-6 flex-col xl:flex-row xl:h-full">
|
||||
|
||||
<CardTitled :key="refreshKey" class="p-4 xl:flex-[4] w-full h-full" title="Events"
|
||||
sub="Events stacked bar chart.">
|
||||
<template #header>
|
||||
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
|
||||
@@ -37,21 +55,26 @@ const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProje
|
||||
</div>
|
||||
</CardTitled>
|
||||
|
||||
<CardTitled :key="refreshKey" class="p-4 flex-[2] w-full h-full" title="Top events"
|
||||
<CardTitled :key="refreshKey" class="p-4 xl:flex-[2] w-full h-full" title="Top events"
|
||||
sub="Displays key events.">
|
||||
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
|
||||
</CardTitled>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="flex">
|
||||
<EventsFunnelChart :key="refreshKey" class="w-full"></EventsFunnelChart>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="flex">
|
||||
<EventsUserFlow :key="refreshKey"></EventsUserFlow>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex">
|
||||
<EventsMetadataAnalyzer :key="refreshKey"></EventsMetadataAnalyzer>
|
||||
</div>
|
||||
|
||||
@@ -2,159 +2,64 @@
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const { data: projects } = useProjectsList();
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const mainChartSelectIndex = ref<number>(1);
|
||||
const sessionsChartSelectIndex = ref<number>(1);
|
||||
|
||||
|
||||
const route = useRoute();
|
||||
const { project, projectList, projectId } = useProject();
|
||||
|
||||
const justLogged = computed(() => route.query.just_logged);
|
||||
const jwtLogin = computed(() => route.query.jwt_login as string);
|
||||
|
||||
const limitsInfo = ref<{
|
||||
limited: boolean,
|
||||
maxLimit: number,
|
||||
limit: number,
|
||||
total: number,
|
||||
percent: number
|
||||
}>();
|
||||
|
||||
const justLogged = computed(() => {
|
||||
return route.query.just_logged;
|
||||
})
|
||||
|
||||
const { token, setToken } = useAccessToken();
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.query.just_logged) return location.href = '/';
|
||||
limitsInfo.value = await $fetch<any>("/api/project/limits_info", signHeaders());
|
||||
watch(activeProject, async () => {
|
||||
limitsInfo.value = await $fetch<any>("/api/project/limits_info", signHeaders());
|
||||
});
|
||||
});
|
||||
|
||||
if (jwtLogin.value) {
|
||||
setToken(jwtLogin.value);
|
||||
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
||||
const loggedUser = useLoggedUser();
|
||||
loggedUser.user = user;
|
||||
// setTimeout(() => { location.reload(); }, 100);
|
||||
}
|
||||
|
||||
const firstInteractionUrl = computed(() => {
|
||||
return `/api/metrics/${activeProject.value?._id}/first_interaction`
|
||||
});
|
||||
if (justLogged.value) { setTimeout(() => { location.href = '/' }, 500) }
|
||||
|
||||
const firstInteraction = useFetch<boolean>(firstInteractionUrl, {
|
||||
...signHeaders(),
|
||||
lazy: true
|
||||
});
|
||||
|
||||
const selectLabels = [
|
||||
{ label: 'Hour', value: 'hour' },
|
||||
{ label: 'Day', value: 'day' },
|
||||
// { label: 'Month', value: 'month' },
|
||||
];
|
||||
|
||||
|
||||
|
||||
const { snapshot } = useSnapshot();
|
||||
|
||||
const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProject.value?._id.toString()}`);
|
||||
|
||||
const isPremium = computed(() => {
|
||||
return activeProject.value?.premium;
|
||||
})
|
||||
|
||||
const pricingDrawer = usePricingDrawer();
|
||||
|
||||
function goToUpgrade() {
|
||||
pricingDrawer.visible.value = true;
|
||||
}
|
||||
const firstInteraction = useFetch<boolean>('/api/project/first_interaction', {
|
||||
lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
|
||||
});
|
||||
|
||||
const showDashboard = computed(() => project.value && firstInteraction.data.value);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="dashboard w-full h-full overflow-y-auto pb-20 md:pt-4 lg:pt-0">
|
||||
<div class="dashboard w-full h-full overflow-y-auto overflow-x-hidden pb-[7rem] md:pt-4 lg:pt-0">
|
||||
|
||||
<div :key="'home-' + isLiveDemo()"
|
||||
v-if="projects && activeProject && (firstInteraction.data.value === true) && !justLogged">
|
||||
<div v-if="showDashboard">
|
||||
|
||||
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||
<div v-if="limitsInfo && limitsInfo.limited"
|
||||
class="w-full bg-[#fbbf2422] p-4 rounded-lg text-[.9rem] flex items-center">
|
||||
<div class="flex flex-col grow">
|
||||
<div class="poppins font-semibold text-[#fbbf24]">
|
||||
Limit reached
|
||||
</div>
|
||||
<div class="poppins text-[#fbbf24]">
|
||||
Litlyx cannot receive new data as you reached your plan's limit. Resume all the great
|
||||
features and collect even more data with a higher plan.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isPremium" class="w-full bg-[#5680f822] p-4 rounded-lg text-[.9rem] flex items-center">
|
||||
<div class="flex flex-col grow">
|
||||
<div class="poppins font-semibold text-lyx-primary">
|
||||
Launch offer: 25% off
|
||||
</div>
|
||||
<div class="poppins text-lyx-primary">
|
||||
We're offering an exclusive 25% discount forever on all plans starting from the Acceleration
|
||||
Plan for our first 100 users who believe in our project.
|
||||
<br>
|
||||
Redeem Code: <span class="text-white font-bold text-[1rem]">LIT25</span> at checkout to
|
||||
claim your discount.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
<BannerLimitsInfo :key="refreshKey"></BannerLimitsInfo>
|
||||
<BannerOffer :key="refreshKey"></BannerOffer>
|
||||
</div>
|
||||
|
||||
<DashboardTopSection></DashboardTopSection>
|
||||
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
||||
|
||||
|
||||
<div>
|
||||
<DashboardTopSection :key="refreshKey"></DashboardTopSection>
|
||||
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
||||
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
||||
</div>
|
||||
<!--
|
||||
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row">
|
||||
|
||||
<CardTitled :key="refreshKey" class="p-4 flex-1 w-full" 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 :key="refreshKey" class="p-4 flex-1 w-full" 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 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 :key="refreshKey"></DashboardWebsitesBarCard>
|
||||
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<DashboardEventsBarCard :key="refreshKey"></DashboardEventsBarCard>
|
||||
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,10 +67,10 @@ function goToUpgrade() {
|
||||
<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 :key="refreshKey"></DashboardReferrersBarCard>
|
||||
<BarCardBrowsers :key="refreshKey"></BarCardBrowsers>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<DashboardBrowsersBarCard :key="refreshKey"></DashboardBrowsersBarCard>
|
||||
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -173,35 +78,30 @@ function goToUpgrade() {
|
||||
<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 :key="refreshKey"></DashboardOssBarCard>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<DashboardGeolocationBarCard :key="refreshKey"></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 :key="refreshKey"></DashboardDevicesBarCard>
|
||||
<BarCardGeolocations :key="refreshKey"></BarCardGeolocations>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<BarCardDevices :key="refreshKey"></BarCardDevices>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<FirstInteraction v-if="!justLogged" :refresh-interaction="firstInteraction.refresh"
|
||||
:first-interaction="(firstInteraction.data.value || false)"></FirstInteraction>
|
||||
|
||||
<div class="text-text/85 mt-8 ml-8 poppis text-[1.2rem]" v-if="projects && projects.length == 0 && !justLogged">
|
||||
<div class="text-text/85 mt-8 ml-8 poppis text-[1.2rem]"
|
||||
v-if="projectList && projectList.length == 0 && !justLogged">
|
||||
Create your first project...
|
||||
</div>
|
||||
|
||||
<div v-if="justLogged" class="text-[2rem]">
|
||||
The page will refresh soon
|
||||
<div v-if="justLogged" class="text-[2rem] w-full h-full flex items-center justify-center">
|
||||
<div
|
||||
class="backdrop-blur-[1px] z-[20] left-0 top-0 w-full h-full flex items-center justify-center font-bold rockmann absolute">
|
||||
<i class="fas fa-spinner text-[2rem] text-[#727272] animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
31
dashboard/pages/jwt_login.vue
Normal file
31
dashboard/pages/jwt_login.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
definePageMeta({ layout: 'none' });
|
||||
|
||||
const route = useRoute();
|
||||
const jwtLogin = computed(() => route.query.jwt_login as string);
|
||||
const { token, setToken } = useAccessToken();
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
if (jwtLogin.value) {
|
||||
setToken(jwtLogin.value);
|
||||
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
||||
const loggedUser = useLoggedUser();
|
||||
loggedUser.user = user;
|
||||
setTimeout(() => { location.href = '/project_creation?just_logged=true' }, 100);
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div> You will be redirected soon </div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -78,7 +78,7 @@ const selectLabelsEvents = [
|
||||
<div class="flex gap-2 md:pt-0 pt-4">
|
||||
<LyxUiButton link="/" type="primary"
|
||||
class="poppins font-semibold text-[.9rem] lg:text-[1.2rem] flex items-center !px-14 py-4">
|
||||
Get started for free
|
||||
Go to dashboard
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,13 +86,14 @@ const selectLabelsEvents = [
|
||||
|
||||
|
||||
<div>
|
||||
<DashboardTopCards></DashboardTopCards>
|
||||
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
||||
<DashboardActionableChart></DashboardActionableChart>
|
||||
<div class="mt-6 px-6 hidden lg:flex gap-6 flex-col 2xl:flex-row w-full">
|
||||
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex gap-6 flex-col xl:flex-row p-6">
|
||||
|
||||
<CardTitled class="p-4 flex-[4] w-full h-full" title="Events" sub="Events stacked bar chart.">
|
||||
@@ -115,47 +116,35 @@ const selectLabelsEvents = [
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex w-full justify-center 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 w-full gap-6 flex-col xl:flex-row">
|
||||
<div class="flex-1">
|
||||
<DashboardReferrersBarCard></DashboardReferrersBarCard>
|
||||
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<DashboardBrowsersBarCard></DashboardBrowsersBarCard>
|
||||
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
|
||||
</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>
|
||||
<BarCardBrowsers :key="refreshKey"></BarCardBrowsers>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<!-- <DashboardGeolocationBarCard></DashboardGeolocationBarCard> -->
|
||||
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
||||
</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">
|
||||
<BarCardGeolocations :key="refreshKey"></BarCardGeolocations>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<BarCardDevices :key="refreshKey"></BarCardDevices>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,22 +158,18 @@ const selectLabelsEvents = [
|
||||
|
||||
<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 ?
|
||||
Do you want this analytics for your website ?
|
||||
</div>
|
||||
<div class="poppins font-semibold text-text-sub">
|
||||
Start now! It's free.
|
||||
Start now and discover more.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-col md:flex-row">
|
||||
<LyxUiButton link="/" type="primary"
|
||||
class="poppins font-semibold text-[1.1rem] lg:text-[1.6rem] flex items-center !px-14">
|
||||
Get started
|
||||
class="poppins font-semibold text-[.9rem] lg:text-[1.2rem] flex items-center !px-14 py-4">
|
||||
Get started for free
|
||||
</LyxUiButton>
|
||||
<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>
|
||||
|
||||
@@ -20,9 +20,9 @@ async function loginWithoutAuth() {
|
||||
|
||||
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
||||
const loggedUser = useLoggedUser();
|
||||
loggedUser.value = user;
|
||||
loggedUser.user = user;
|
||||
|
||||
console.log('LOGIN DONE - USER', loggedUser.value);
|
||||
console.log('LOGIN DONE - USER', loggedUser.user);
|
||||
|
||||
const isFirstTime = await $fetch<boolean>('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
||||
|
||||
@@ -58,9 +58,9 @@ async function handleOnSuccess(response: any) {
|
||||
|
||||
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
||||
const loggedUser = useLoggedUser();
|
||||
loggedUser.value = user;
|
||||
loggedUser.user = user;
|
||||
|
||||
console.log('LOGIN DONE - USER', loggedUser.value);
|
||||
console.log('LOGIN DONE - USER', loggedUser.user);
|
||||
|
||||
const isFirstTime = await $fetch<boolean>('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
||||
|
||||
@@ -107,6 +107,48 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const isEmailLogin = ref<boolean>(false);
|
||||
const email = ref<string>("");
|
||||
const password = ref<string>("");
|
||||
|
||||
function goBackToEmailLogin() {
|
||||
isEmailLogin.value = false;
|
||||
email.value = '';
|
||||
password.value = '';
|
||||
}
|
||||
|
||||
async function signInWithCredentials() {
|
||||
|
||||
try {
|
||||
const result = await $fetch<{error:true, message:string} | {error: false, access_token:string}>('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: email.value, password: password.value })
|
||||
})
|
||||
|
||||
if (result.error) return alert(result.message);
|
||||
|
||||
setToken(result.access_token);
|
||||
|
||||
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
||||
const loggedUser = useLoggedUser();
|
||||
loggedUser.user = user;
|
||||
|
||||
console.log('LOGIN DONE - USER', loggedUser.user);
|
||||
|
||||
const isFirstTime = await $fetch<boolean>('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
||||
|
||||
if (isFirstTime === true) {
|
||||
router.push('/project_creation?just_logged=true');
|
||||
} else {
|
||||
router.push('/?just_logged=true');
|
||||
}
|
||||
|
||||
|
||||
} catch (ex) {
|
||||
alert('Something went wrong.');
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -117,7 +159,7 @@ onMounted(() => {
|
||||
|
||||
<div class="flex h-full">
|
||||
|
||||
<div class="flex-1 flex flex-col items-center pt-20 lg:pt-[22vh]">
|
||||
<div class="flex-1 flex flex-col items-center pt-20 xl:pt-[22vh]">
|
||||
|
||||
<div class="rotating-thing absolute top-0"></div>
|
||||
|
||||
@@ -126,12 +168,11 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="text-text text-[2.2rem] font-bold poppins">
|
||||
Sign in with
|
||||
Sign in
|
||||
</div>
|
||||
|
||||
<div class="text-text/80 text-[1.2rem] text-center w-[70%] poppins mt-2">
|
||||
<div class="text-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2">
|
||||
Track web analytics and custom events
|
||||
<br>
|
||||
with extreme simplicity in under 30 sec.
|
||||
<br>
|
||||
<!-- <div class="font-bold poppins mt-4">
|
||||
@@ -141,23 +182,52 @@ onMounted(() => {
|
||||
|
||||
<div class="mt-12">
|
||||
|
||||
<div v-if="!isNoAuth && isEmailLogin" class="flex flex-col gap-2">
|
||||
|
||||
|
||||
<div class="flex flex-col gap-4 z-[100] w-[20vw] min-w-[20rem]">
|
||||
<LyxUiInput class="px-3 py-2" placeholder="Email" v-model="email"></LyxUiInput>
|
||||
<LyxUiInput class="px-3 py-2" placeholder="Password" v-model="password" type="password">
|
||||
</LyxUiInput>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-4 z-[100]">
|
||||
<LyxUiButton @click="signInWithCredentials()" class="text-center" type="primary">
|
||||
Sign in
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
|
||||
<div @click="goBackToEmailLogin()"
|
||||
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
|
||||
Go back
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="!isNoAuth && !isEmailLogin" class="flex flex-col gap-2">
|
||||
|
||||
<div v-if="!isNoAuth" class="flex flex-col gap-2">
|
||||
<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]">
|
||||
class="hover:bg-lyx-primary 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
|
||||
class=" opacity-35 cursor-not-allowed flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]">
|
||||
<div @click="isEmailLogin = true"
|
||||
class="hover:bg-[#262626] 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-github"></i>
|
||||
<i class="far fa-envelope"></i>
|
||||
</div>
|
||||
Continue with GitHub
|
||||
Sign in with Email
|
||||
</div>
|
||||
|
||||
|
||||
<RouterLink tag="div" to="/register" class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
|
||||
You don't have an account ? Sign up
|
||||
</RouterLink>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="isNoAuth" @click="loginWithoutAuth"
|
||||
@@ -170,7 +240,7 @@ onMounted(() => {
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-[.9rem] poppins mt-12 text-text-sub text-center relative z-[2]">
|
||||
<div class="text-[.9rem] poppins mt-20 text-text-sub text-center relative z-[2]">
|
||||
By continuing you are accepting
|
||||
<br>
|
||||
our
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
definePageMeta({ layout: 'none' });
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
<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>
|
||||
@@ -7,9 +7,9 @@ const creating = ref<boolean>(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { projects, refresh } = useProjectsList();
|
||||
const { projectList, actions } = useProject();
|
||||
|
||||
const isFirstProject = computed(() => { return projects.value?.length == 0; })
|
||||
const isFirstProject = computed(() => { return projectList.value?.length == 0; })
|
||||
|
||||
import { Lit } from 'litlyx-js';
|
||||
|
||||
@@ -35,11 +35,11 @@ async function createProject() {
|
||||
body: JSON.stringify({ name: projectName.value })
|
||||
});
|
||||
|
||||
await refresh();
|
||||
await actions.refreshProjectsList();
|
||||
|
||||
const newActiveProjectId = projects.value?.[projects.value?.length - 1]._id.toString();
|
||||
const newActiveProjectId = projectList.value?.[projectList.value?.length - 1]._id.toString();
|
||||
if (newActiveProjectId) {
|
||||
await setActiveProject(newActiveProjectId);
|
||||
await actions.setActiveProject(newActiveProjectId);
|
||||
}
|
||||
|
||||
router.push('/');
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const { projects, refresh } = useProjectsList();
|
||||
const { guestProjects, refresh: refreshGuest } = useGuestProjectsList();
|
||||
const { pid } = useActiveProjectId();
|
||||
|
||||
const { data: maxProjects } = useFetch("/api/user/max_projects", signHeaders());
|
||||
|
||||
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();
|
||||
|
||||
if (pid.value == projectId) {
|
||||
const firstProjectId = projects.value?.[0]?._id.toString();
|
||||
if (firstProjectId) {
|
||||
await setActiveProject(firstProjectId);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (ex: any) {
|
||||
alert(ex.message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function leaveProject(projectId: string, projectName: string) {
|
||||
const sure = confirm(`Are you sure to leave the project ${projectName} ?`);
|
||||
if (!sure) return;
|
||||
|
||||
try {
|
||||
|
||||
await $fetch('/api/project/members/leave', signHeaders());
|
||||
|
||||
await refreshGuest();
|
||||
|
||||
if (pid.value == projectId) {
|
||||
const firstProjectId = projects.value?.[0]?._id.toString();
|
||||
if (firstProjectId) {
|
||||
await setActiveProject(firstProjectId);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (ex: any) {
|
||||
alert(ex.message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { setToken } = useAccessToken();
|
||||
|
||||
async function onProjectClick(pid: string) {
|
||||
await setActiveProject(pid)
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
const sure = confirm("Are you sure you want to delete this account ?");
|
||||
if (!sure) return;
|
||||
await $fetch("/api/user/delete_account", {
|
||||
...signHeaders(),
|
||||
method: "DELETE"
|
||||
})
|
||||
|
||||
setToken('');
|
||||
location.href = "/login"
|
||||
}
|
||||
|
||||
</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 flex-col md:flex-row">
|
||||
<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 ?? '-' }} / {{ maxProjects || 3 }}
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink v-if="(projects?.length || 0) < (maxProjects || 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="pid">
|
||||
|
||||
<div v-for="e of projects">
|
||||
<DashboardProjectSelectionCard @click="onProjectClick(e._id.toString())"
|
||||
:active="pid == e._id.toString()" :title="e.name"
|
||||
:subtitle="pid == e._id.toString() ? '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 v-for="e of guestProjects">
|
||||
<DashboardProjectSelectionCard class="outline outline-[2px] outline-yellow-200"
|
||||
@click="onProjectClick(e._id.toString())" :title="e.name" :active="pid == e._id.toString()"
|
||||
:subtitle="pid == e._id.toString() ? 'ATTIVO' : ''" :chip="''">
|
||||
</DashboardProjectSelectionCard>
|
||||
<div @click="leaveProject(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-right-from-bracket"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="px-10">
|
||||
<CardTitled title="Danger zone" :sub="''" class="p-4 mt-8">
|
||||
|
||||
<div
|
||||
class="outline rounded-lg w-fit px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-[#1e1412]">
|
||||
<div class="poppins font-semibold"> Deleting this account will also remove its projects </div>
|
||||
<div @click="deleteAccount()"
|
||||
class="text-[#e95b61] poppins font-semibold cursor-pointer hover:text-black hover:bg-red-700 outline rounded-lg w-fit px-8 py-2 outline-[1px] outline-[#532b26] bg-[#291415]">
|
||||
Delete account
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</CardTitled>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
169
dashboard/pages/register.vue
Normal file
169
dashboard/pages/register.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
definePageMeta({ layout: 'none' });
|
||||
|
||||
|
||||
const emailSended = ref<boolean>(false);
|
||||
|
||||
const email = ref<string>("");
|
||||
const password = ref<string>("");
|
||||
const passwordConfirm = ref<string>("");
|
||||
|
||||
const canRegister = computed(() => {
|
||||
if (email.value.length == 0) return false;
|
||||
if (!email.value.includes('@')) return false;
|
||||
if (!email.value.includes('.')) return false;
|
||||
if (password.value !== passwordConfirm.value) return false;
|
||||
if (password.value.length < 6) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
async function registerAccount() {
|
||||
|
||||
try {
|
||||
const res = await $fetch<any>('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email: email.value, password: password.value })
|
||||
});
|
||||
if (res.error === true) return alert(res.message);
|
||||
emailSended.value = true;
|
||||
} catch (ex) {
|
||||
alert('Something went wrong');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</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 xl: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 up
|
||||
</div>
|
||||
|
||||
<div class="text-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2">
|
||||
Track web analytics and custom events
|
||||
with extreme simplicity in under 30 sec.
|
||||
<br>
|
||||
<!-- <div class="font-bold poppins mt-4">
|
||||
Start for Free now! Up to 3k visits/events monthly.
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div v-if="!emailSended" class="mt-12">
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
|
||||
<div class="flex flex-col gap-4 z-[100] w-[20vw] min-w-[20rem]">
|
||||
<LyxUiInput class="px-3 py-2" placeholder="Email" v-model="email">
|
||||
</LyxUiInput>
|
||||
<LyxUiInput class="px-3 py-2" placeholder="Password" v-model="password" type="password">
|
||||
</LyxUiInput>
|
||||
<LyxUiInput class="px-3 py-2" placeholder="Confirm password" v-model="passwordConfirm"
|
||||
type="password">
|
||||
</LyxUiInput>
|
||||
</div>
|
||||
|
||||
<div class="text-lyx-text-darker text-end text-[.8rem]">
|
||||
Password must be at least 6 chars long
|
||||
</div>
|
||||
<div class="flex justify-center mt-4 z-[100]">
|
||||
<LyxUiButton :disabled="!canRegister" @click="registerAccount()" class="text-center"
|
||||
type="primary">
|
||||
Sign up
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
|
||||
<RouterLink to="/login"
|
||||
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
|
||||
Go back to login
|
||||
</RouterLink>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="emailSended" class="mt-12 flex flex-col text-center text-[1.1rem] z-[100]">
|
||||
<div>
|
||||
We sent you a confirm email.
|
||||
</div>
|
||||
<div>
|
||||
Please check your inbox to confirm your account and complete your registration.
|
||||
</div>
|
||||
<RouterLink tag="div" to="/login"
|
||||
class="mt-6 text-center text-lyx-text-dark underline cursor-pointer">
|
||||
Go back
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div v-if="!emailSended" class="text-[.9rem] poppins mt-5 xl:mt-20 text-text-sub text-center relative z-[2]">
|
||||
By continuing you are accepting
|
||||
<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>
|
||||
|
||||
</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>
|
||||
@@ -1,16 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
const activeProjectId = useActiveProjectId();
|
||||
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
'Authorization': authorizationHeaderComputed.value,
|
||||
'x-pid': activeProjectId.data.value || ''
|
||||
}
|
||||
});
|
||||
|
||||
const reportList = useFetch(`/api/security/list`, { headers });
|
||||
const reportList = useFetch(`/api/security/list`, { headers: useComputedHeaders({ useSnapshotDates: false }) });
|
||||
|
||||
const { createAlert } = useAlert();
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const items = [
|
||||
{ label: 'General', slot: 'general' },
|
||||
{ label: 'Members', slot: 'members' },
|
||||
@@ -15,21 +13,22 @@ const items = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 h-dvh overflow-y-auto hide-scrollbars">
|
||||
<div class="poppins font-semibold text-[1.3rem]"> Settings </div>
|
||||
<div class="lg:px-10 lg:py-8 h-dvh overflow-y-auto overflow-x-hidden hide-scrollbars">
|
||||
|
||||
<div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Settings </div>
|
||||
|
||||
<CustomTab :items="items" class="mt-8">
|
||||
<template #general>
|
||||
<SettingsGeneral :key="activeProject?._id.toString()"></SettingsGeneral>
|
||||
<SettingsGeneral :key="refreshKey"></SettingsGeneral>
|
||||
</template>
|
||||
<template #members>
|
||||
<SettingsMembers :key="activeProject?._id.toString()"></SettingsMembers>
|
||||
<SettingsMembers :key="refreshKey"></SettingsMembers>
|
||||
</template>
|
||||
<template #billing>
|
||||
<SettingsBilling :key="activeProject?._id.toString()"></SettingsBilling>
|
||||
<SettingsBilling :key="refreshKey"></SettingsBilling>
|
||||
</template>
|
||||
<template #account>
|
||||
<SettingsAccount :key="activeProject?._id.toString()"></SettingsAccount>
|
||||
<SettingsAccount :key="refreshKey"></SettingsAccount>
|
||||
</template>
|
||||
</CustomTab>
|
||||
|
||||
|
||||
77
dashboard/pnpm-lock.yaml
generated
77
dashboard/pnpm-lock.yaml
generated
@@ -33,8 +33,11 @@ importers:
|
||||
specifier: ^1.11.11
|
||||
version: 1.11.11
|
||||
google-auth-library:
|
||||
specifier: ^9.9.0
|
||||
specifier: ^9.10.0
|
||||
version: 9.10.0(encoding@0.1.13)
|
||||
googleapis:
|
||||
specifier: ^144.0.0
|
||||
version: 144.0.0(encoding@0.1.13)
|
||||
highlight.js:
|
||||
specifier: ^11.10.0
|
||||
version: 11.10.0
|
||||
@@ -1499,20 +1502,20 @@ packages:
|
||||
'@vue/reactivity@3.4.27':
|
||||
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
|
||||
|
||||
'@vue/reactivity@3.5.8':
|
||||
resolution: {integrity: sha512-mlgUyFHLCUZcAYkqvzYnlBRCh0t5ZQfLYit7nukn1GR96gc48Bp4B7OIcSfVSvlG1k3BPfD+p22gi1t2n9tsXg==}
|
||||
'@vue/reactivity@3.5.10':
|
||||
resolution: {integrity: sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==}
|
||||
|
||||
'@vue/runtime-core@3.4.27':
|
||||
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
|
||||
|
||||
'@vue/runtime-core@3.5.8':
|
||||
resolution: {integrity: sha512-fJuPelh64agZ8vKkZgp5iCkPaEqFJsYzxLk9vSC0X3G8ppknclNDr61gDc45yBGTaN5Xqc1qZWU3/NoaBMHcjQ==}
|
||||
'@vue/runtime-core@3.5.10':
|
||||
resolution: {integrity: sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==}
|
||||
|
||||
'@vue/runtime-dom@3.4.27':
|
||||
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
|
||||
|
||||
'@vue/runtime-dom@3.5.8':
|
||||
resolution: {integrity: sha512-DpAUz+PKjTZPUOB6zJgkxVI3GuYc2iWZiNeeHQUw53kdrparSTG6HeXUrYDjaam8dVsCdvQxDz6ZWxnyjccUjQ==}
|
||||
'@vue/runtime-dom@3.5.10':
|
||||
resolution: {integrity: sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==}
|
||||
|
||||
'@vue/server-renderer@3.4.27':
|
||||
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
|
||||
@@ -1522,6 +1525,9 @@ packages:
|
||||
'@vue/shared@3.4.27':
|
||||
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
|
||||
|
||||
'@vue/shared@3.5.10':
|
||||
resolution: {integrity: sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==}
|
||||
|
||||
'@vue/shared@3.5.8':
|
||||
resolution: {integrity: sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A==}
|
||||
|
||||
@@ -2734,6 +2740,14 @@ packages:
|
||||
resolution: {integrity: sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
googleapis-common@7.2.0:
|
||||
resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
googleapis@144.0.0:
|
||||
resolution: {integrity: sha512-ELcWOXtJxjPX4vsKMh+7V+jZvgPwYMlEhQFiu2sa9Qmt5veX8nwXPksOWGGN6Zk4xCiLygUyaz7xGtcMO+Onxw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
gopd@1.0.1:
|
||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||
|
||||
@@ -4984,6 +4998,9 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
url-template@2.0.8:
|
||||
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
|
||||
|
||||
urlpattern-polyfill@8.0.2:
|
||||
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
|
||||
|
||||
@@ -7241,19 +7258,19 @@ snapshots:
|
||||
dependencies:
|
||||
'@vue/shared': 3.4.27
|
||||
|
||||
'@vue/reactivity@3.5.8':
|
||||
'@vue/reactivity@3.5.10':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.8
|
||||
'@vue/shared': 3.5.10
|
||||
|
||||
'@vue/runtime-core@3.4.27':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.4.27
|
||||
'@vue/shared': 3.4.27
|
||||
|
||||
'@vue/runtime-core@3.5.8':
|
||||
'@vue/runtime-core@3.5.10':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.8
|
||||
'@vue/shared': 3.5.8
|
||||
'@vue/reactivity': 3.5.10
|
||||
'@vue/shared': 3.5.10
|
||||
|
||||
'@vue/runtime-dom@3.4.27':
|
||||
dependencies:
|
||||
@@ -7261,11 +7278,11 @@ snapshots:
|
||||
'@vue/shared': 3.4.27
|
||||
csstype: 3.1.3
|
||||
|
||||
'@vue/runtime-dom@3.5.8':
|
||||
'@vue/runtime-dom@3.5.10':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.8
|
||||
'@vue/runtime-core': 3.5.8
|
||||
'@vue/shared': 3.5.8
|
||||
'@vue/reactivity': 3.5.10
|
||||
'@vue/runtime-core': 3.5.10
|
||||
'@vue/shared': 3.5.10
|
||||
csstype: 3.1.3
|
||||
|
||||
'@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))':
|
||||
@@ -7276,6 +7293,8 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.4.27': {}
|
||||
|
||||
'@vue/shared@3.5.10': {}
|
||||
|
||||
'@vue/shared@3.5.8': {}
|
||||
|
||||
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
|
||||
@@ -8571,6 +8590,26 @@ snapshots:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
googleapis-common@7.2.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
gaxios: 6.6.0(encoding@0.1.13)
|
||||
google-auth-library: 9.10.0(encoding@0.1.13)
|
||||
qs: 6.12.1
|
||||
url-template: 2.0.8
|
||||
uuid: 9.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
googleapis@144.0.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
google-auth-library: 9.10.0(encoding@0.1.13)
|
||||
googleapis-common: 7.2.0(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
gopd@1.0.1:
|
||||
dependencies:
|
||||
get-intrinsic: 1.2.4
|
||||
@@ -11143,6 +11182,8 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
url-template@2.0.8: {}
|
||||
|
||||
urlpattern-polyfill@8.0.2: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
@@ -11347,8 +11388,8 @@ snapshots:
|
||||
|
||||
vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)):
|
||||
dependencies:
|
||||
'@vue/runtime-core': 3.5.8
|
||||
'@vue/runtime-dom': 3.5.8
|
||||
'@vue/runtime-core': 3.5.10
|
||||
'@vue/runtime-dom': 3.5.10
|
||||
chart.js: 3.9.1
|
||||
csstype: 3.1.3
|
||||
lodash-es: 4.17.21
|
||||
|
||||
@@ -3,8 +3,8 @@ import jwt from 'jsonwebtoken';
|
||||
|
||||
const { AUTH_JWT_SECRET } = useRuntimeConfig();
|
||||
|
||||
function createJwt(data: Object) {
|
||||
return jwt.sign(data, AUTH_JWT_SECRET, { expiresIn: '30d' });
|
||||
function createJwt(data: Object, expiresIn?: string) {
|
||||
return jwt.sign(data, AUTH_JWT_SECRET, { expiresIn: expiresIn ?? '30d' });
|
||||
}
|
||||
|
||||
function readJwt(data: string) {
|
||||
@@ -28,4 +28,13 @@ export function readUserJwt(raw: string) {
|
||||
|
||||
export function createUserJwt(data: TUserJwt) {
|
||||
return createJwt(data);
|
||||
}
|
||||
|
||||
export function createRegisterJwt(email: string, hashedPassword: string) {
|
||||
return createJwt({ email, password: hashedPassword }, '7d');
|
||||
}
|
||||
|
||||
export function readRegisterJwt(raw: string) {
|
||||
const data = readJwt(raw);
|
||||
return data as { email: string, password: string } | undefined;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { AdvancedTimelineAggregationOptions, executeAdvancedTimelineAggregation, executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
|
||||
import { Types } from "mongoose";
|
||||
import { AIPlugin, AIPlugin_TTool } from "../Plugin";
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const getEventsCountTool: AIPlugin_TTool<'getEventsCount'> = {
|
||||
type: 'function',
|
||||
@@ -66,7 +66,9 @@ export class AiEvents extends AIPlugin<['getEventsCount', 'getEventsTimeline']>
|
||||
const query: AdvancedTimelineAggregationOptions & { customMatch: Record<string, any> } = {
|
||||
projectId: new Types.ObjectId(data.project_id) as any,
|
||||
model: EventModel,
|
||||
from: data.from, to: data.to, slice: 'day',
|
||||
from: dayjs(data.from).startOf('day').toISOString(),
|
||||
to: dayjs(data.to).startOf('day').toISOString(),
|
||||
slice: 'day',
|
||||
customMatch: {}
|
||||
}
|
||||
if (data.metadata) query.customMatch.metadata = data.metadata;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { AdvancedTimelineAggregationOptions, executeAdvancedTimelineAggregation, executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
|
||||
import { Types } from "mongoose";
|
||||
import { AIPlugin, AIPlugin_TTool } from "../Plugin";
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const getVisitsCountsTool: AIPlugin_TTool<'getVisitsCount'> = {
|
||||
type: 'function',
|
||||
@@ -63,10 +63,13 @@ export class AiVisits extends AIPlugin<['getVisitsCount', 'getVisitsTimeline']>
|
||||
},
|
||||
'getVisitsTimeline': {
|
||||
handler: async (data: { project_id: string, from: string, to: string, website?: string, page?: string }) => {
|
||||
|
||||
const query: AdvancedTimelineAggregationOptions & { customMatch: Record<string, any> } = {
|
||||
projectId: new Types.ObjectId(data.project_id) as any,
|
||||
model: VisitModel,
|
||||
from: data.from, to: data.to, slice: 'day',
|
||||
from: dayjs(data.from).startOf('day').toISOString(),
|
||||
to: dayjs(data.to).startOf('day').toISOString(),
|
||||
slice: 'day',
|
||||
customMatch: {}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
const data = await getRequestData(event);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
if (!event.context.params) return;
|
||||
const chat_id = event.context.params['chat_id'];
|
||||
|
||||
@@ -4,13 +4,10 @@ import type OpenAI from "openai";
|
||||
import { getChartsInMessage } from "~/server/services/AiService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event);
|
||||
if (!data) return;
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
const { project_id } = data;
|
||||
|
||||
if (!event.context.params) return;
|
||||
const chat_id = event.context.params['chat_id'];
|
||||
@@ -1,28 +0,0 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { sendMessageOnChat } from "~/server/services/AiService";
|
||||
import { getAiChatRemainings } from "./chats_remaining";
|
||||
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user, false);
|
||||
if (!project) return;
|
||||
|
||||
// if (!user?.logged) return;
|
||||
// if (!user.user.roles.includes('ADMIN')) return;
|
||||
|
||||
const { text, chat_id } = await readBody(event);
|
||||
if (!text) return setResponseStatus(event, 400, 'text parameter missing');
|
||||
|
||||
const chatsRemaining = await getAiChatRemainings(project_id);
|
||||
if (chatsRemaining <= 0) return setResponseStatus(event, 400, 'CHAT_LIMIT_REACHED');
|
||||
|
||||
const response = await sendMessageOnChat(text, project._id.toString(), chat_id);
|
||||
|
||||
return response;
|
||||
});
|
||||
@@ -3,15 +3,14 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
const data = await getRequestData(event);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const chatList = await AiChatModel.find({ project_id }, { _id: 1, title: 1 }, { sort: { updated_at: 1 } });
|
||||
|
||||
|
||||
return chatList.map(e => e.toJSON());
|
||||
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ProjectLimitModel } from "@schema/ProjectsLimits";
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
|
||||
export async function getAiChatRemainings(project_id: string) {
|
||||
const limits = await ProjectLimitModel.findOne({ project_id })
|
||||
@@ -11,13 +10,11 @@ export async function getAiChatRemainings(project_id: string) {
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
const data = await getRequestData(event);
|
||||
if (!data) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
const { pid } = data;
|
||||
|
||||
const chatsRemaining = await getAiChatRemainings(project_id);
|
||||
const chatsRemaining = await getAiChatRemainings(pid);
|
||||
return chatsRemaining;
|
||||
});
|
||||
21
dashboard/server/api/ai/send_message.post.ts
Normal file
21
dashboard/server/api/ai/send_message.post.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { sendMessageOnChat } from "~/server/services/AiService";
|
||||
import { getAiChatRemainings } from "./chats_remaining";
|
||||
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event);
|
||||
if (!data) return;
|
||||
|
||||
const { pid } = data;
|
||||
|
||||
const { text, chat_id } = await readBody(event);
|
||||
if (!text) return setResponseStatus(event, 400, 'text parameter missing');
|
||||
|
||||
const chatsRemaining = await getAiChatRemainings(pid);
|
||||
if (chatsRemaining <= 0) return setResponseStatus(event, 400, 'CHAT_LIMIT_REACHED');
|
||||
|
||||
const response = await sendMessageOnChat(text, pid, chat_id);
|
||||
|
||||
return response;
|
||||
});
|
||||
24
dashboard/server/api/auth/confirm_email.ts
Normal file
24
dashboard/server/api/auth/confirm_email.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
import { createUserJwt, readRegisterJwt } from '~/server/AuthManager';
|
||||
import { UserModel } from '@schema/UserSchema';
|
||||
import { PasswordModel } from '@schema/PasswordSchema';
|
||||
import EmailService from '@services/EmailService';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const { register_code } = getQuery(event);
|
||||
|
||||
const data = readRegisterJwt(register_code as string);
|
||||
if (!data) return setResponseStatus(event, 400, 'Error decoding register_code');
|
||||
|
||||
try {
|
||||
await PasswordModel.create({ email: data.email, password: data.password })
|
||||
await UserModel.create({ email: data.email, given_name: '', name: 'EmailLogin', locale: '', picture: '', created_at: Date.now() });
|
||||
setImmediate(() => { EmailService.sendWelcomeEmail(data.email); });
|
||||
const jwt = createUserJwt({ email: data.email, name: 'EmailLogin' });
|
||||
return sendRedirect(event,`https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`);
|
||||
} catch (ex) {
|
||||
return setResponseStatus(event, 400, 'Error creating user');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -36,7 +36,14 @@ export default defineEventHandler(async event => {
|
||||
|
||||
const user = await UserModel.findOne({ email: payload.email });
|
||||
|
||||
if (user) return { error: false, access_token: createUserJwt({ email: user.email, name: user.name }) }
|
||||
if (user) {
|
||||
user.google_tokens = tokens as any;
|
||||
await user.save();
|
||||
return {
|
||||
error: false,
|
||||
access_token: createUserJwt({ email: user.email, name: user.name })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const newUser = new UserModel({
|
||||
@@ -45,6 +52,7 @@ export default defineEventHandler(async event => {
|
||||
name: payload.name,
|
||||
locale: payload.locale,
|
||||
picture: payload.picture,
|
||||
google_tokens: tokens,
|
||||
created_at: Date.now()
|
||||
});
|
||||
|
||||
|
||||
24
dashboard/server/api/auth/login.post.ts
Normal file
24
dashboard/server/api/auth/login.post.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
import { createUserJwt } from '~/server/AuthManager';
|
||||
import { UserModel } from '@schema/UserSchema';
|
||||
import crypto from 'crypto';
|
||||
import { PasswordModel } from '@schema/PasswordSchema';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const { email, password } = await readBody(event);
|
||||
|
||||
const user = await UserModel.findOne({ email });
|
||||
|
||||
if (!user) return { error: true, message: 'Email or Password wrong' }
|
||||
|
||||
const hash = crypto.createHash('sha256');
|
||||
const hashedPassword = hash.update(password + '_litlyx').digest('hex');
|
||||
|
||||
const target = await PasswordModel.findOne({ email, password: hashedPassword });
|
||||
|
||||
if (!target) return { error: true, message: 'Email or Password wrong' }
|
||||
|
||||
return { error: false, access_token: createUserJwt({ email: target.email, name: user.name }) }
|
||||
|
||||
});
|
||||
45
dashboard/server/api/auth/register.post.ts
Normal file
45
dashboard/server/api/auth/register.post.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
import { createRegisterJwt, createUserJwt } from '~/server/AuthManager';
|
||||
import { UserModel } from '@schema/UserSchema';
|
||||
import { RegisterModel } from '@schema/RegisterSchema';
|
||||
import EmailService from '@services/EmailService';
|
||||
import crypto from 'crypto';
|
||||
|
||||
function canRegister(email: string, password: string) {
|
||||
if (email.length == 0) return false;
|
||||
if (!email.includes('@')) return false;
|
||||
if (!email.includes('.')) return false;
|
||||
if (password.length < 6) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const { email, password } = await readBody(event);
|
||||
|
||||
if (!canRegister(email, password)) return setResponseStatus(event, 400, 'Email or Password not match criteria');
|
||||
|
||||
const user = await UserModel.findOne({ email });
|
||||
|
||||
if (user) return {
|
||||
error: true,
|
||||
message: 'Email already registered'
|
||||
}
|
||||
|
||||
const hash = crypto.createHash('sha256');
|
||||
const hashedPassword = hash.update(password + '_litlyx').digest('hex');
|
||||
|
||||
const jwt = createRegisterJwt(email, hashedPassword);
|
||||
|
||||
await RegisterModel.create({ email, password: hashedPassword });
|
||||
|
||||
setImmediate(() => {
|
||||
EmailService.sendConfirmEmail(email, `https://dashboard.litlyx.com/api/auth/confirm_email?register_code=${jwt}`);
|
||||
});
|
||||
|
||||
return {
|
||||
error: false,
|
||||
message: 'OK'
|
||||
}
|
||||
|
||||
});
|
||||
34
dashboard/server/api/data/browsers.ts
Normal file
34
dashboard/server/api/data/browsers.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
import { getRequestData } from "~/server/utils/getRequestData";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { pid, from, to, project_id, limit } = data;
|
||||
|
||||
const cacheKey = `browsers:${pid}:${limit}:${from}:${to}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$browser", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
32
dashboard/server/api/data/count.ts
Normal file
32
dashboard/server/api/data/count.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
import { getRequestData } from "~/server/utils/getRequestData";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: true });
|
||||
if (!data) return;
|
||||
|
||||
const { schemaName, pid, from, to, model, project_id } = data;
|
||||
|
||||
const cacheKey = `count:${schemaName}:${pid}:${from}:${to}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
|
||||
const result = await model.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
||||
}
|
||||
},
|
||||
{ $count: 'count' },
|
||||
]);
|
||||
|
||||
return result;
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
34
dashboard/server/api/data/countries.ts
Normal file
34
dashboard/server/api/data/countries.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
import { getRequestData } from "~/server/utils/getRequestData";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { pid, from, to, project_id, limit } = data;
|
||||
|
||||
const cacheKey = `countries:${pid}:${limit}:${from}:${to}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$country", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
34
dashboard/server/api/data/devices.ts
Normal file
34
dashboard/server/api/data/devices.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
import { getRequestData } from "~/server/utils/getRequestData";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { pid, from, to, project_id, limit } = data;
|
||||
|
||||
const cacheKey = `devices:${pid}:${limit}:${from}:${to}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$device", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
34
dashboard/server/api/data/events.ts
Normal file
34
dashboard/server/api/data/events.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
import { getRequestData } from "~/server/utils/getRequestData";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { pid, from, to, project_id, limit } = data;
|
||||
|
||||
const cacheKey = `events:${pid}:${limit}:${from}:${to}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
|
||||
const result = await EventModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$name", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
92
dashboard/server/api/data/events_data/flow_from_name.ts
Normal file
92
dashboard/server/api/data/events_data/flow_from_name.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id, from, to } = data;
|
||||
|
||||
const { name: eventName } = getQuery(event);
|
||||
|
||||
if (!eventName) return setResponseStatus(event, 400, 'name is required');
|
||||
|
||||
const allEvents = await EventModel.find({
|
||||
project_id: project_id,
|
||||
name: eventName,
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to),
|
||||
}
|
||||
}, { flowHash: 1 });
|
||||
|
||||
|
||||
const allFlowHashes = new Map<string, number>();
|
||||
|
||||
allEvents.forEach(e => {
|
||||
if (!e.flowHash) return;
|
||||
if (e.flowHash.length == 0) return;
|
||||
if (allFlowHashes.has(e.flowHash)) {
|
||||
const count = allFlowHashes.get(e.flowHash) as number;
|
||||
allFlowHashes.set(e.flowHash, count + 1);
|
||||
} else {
|
||||
allFlowHashes.set(e.flowHash, 1);
|
||||
}
|
||||
});
|
||||
|
||||
const flowHashIds = Array.from(allFlowHashes.keys());
|
||||
|
||||
const allReferrers: { referrer: string, flowHash: string }[] = [];
|
||||
|
||||
const promises: any[] = [];
|
||||
while (flowHashIds.length > 0) {
|
||||
promises.push(new Promise<void>(async resolve => {
|
||||
const flowHashIdsChunk = flowHashIds.splice(0, 10);
|
||||
const visits = await VisitModel.find({ project_id, flowHash: { $in: flowHashIdsChunk } }, { referrer: 1, flowHash: 1 });
|
||||
allReferrers.push(...visits.map(e => { return { referrer: e.referrer, flowHash: e.flowHash } }));
|
||||
resolve();
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const groupedFlows: Record<string, { referrers: string[] }> = {};
|
||||
|
||||
flowHashIds.forEach(flowHash => {
|
||||
if (!groupedFlows[flowHash]) groupedFlows[flowHash] = { referrers: [] };
|
||||
const target = groupedFlows[flowHash];
|
||||
if (!target) return;
|
||||
const referrers = allReferrers.filter(e => e.flowHash === flowHash).map(e => e.referrer);
|
||||
for (const referrer of referrers) {
|
||||
if (target.referrers.includes(referrer)) continue;
|
||||
target.referrers.push(referrer);
|
||||
}
|
||||
});
|
||||
|
||||
const grouped: Record<string, number> = {};
|
||||
|
||||
for (const referrerPlusHash of allReferrers) {
|
||||
const referrer = referrerPlusHash.referrer;
|
||||
if (!grouped[referrer]) grouped[referrer] = 0
|
||||
grouped[referrer]++;
|
||||
}
|
||||
|
||||
|
||||
const eventsCount = allEvents.length;
|
||||
|
||||
const allGroupedValue = Object.keys(grouped)
|
||||
.map(key => grouped[key])
|
||||
.reduce((a, e) => a + e, 0);
|
||||
|
||||
for (const key in grouped) {
|
||||
grouped[key] = 100 / allGroupedValue * grouped[key];
|
||||
}
|
||||
|
||||
return grouped;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { EVENT_METADATA_FIELDS_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
import { PipelineStage } from "mongoose";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
|
||||
const { name: eventName, field, from, to } = getQuery(event);
|
||||
|
||||
if (!from) return setResponseStatus(event, 400, 'from is required');
|
||||
if (!to) return setResponseStatus(event, 400, 'to is required');
|
||||
if (!eventName) return setResponseStatus(event, 400, 'name is required');
|
||||
if (!field) return setResponseStatus(event, 400, 'field is required');
|
||||
|
||||
|
||||
const aggregation: PipelineStage[] = [
|
||||
{
|
||||
$match: {
|
||||
project_id, name: eventName,
|
||||
created_at: {
|
||||
$gte: new Date(from.toString()),
|
||||
$lte: new Date(to.toString()),
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $group: { _id: `$metadata.${field}`, count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } }
|
||||
]
|
||||
|
||||
const metadataGrouped = await EventModel.aggregate(aggregation);
|
||||
|
||||
return metadataGrouped;
|
||||
|
||||
|
||||
});
|
||||
32
dashboard/server/api/data/events_data/metadata_fields.ts
Normal file
32
dashboard/server/api/data/events_data/metadata_fields.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const { name: eventName } = getQuery(event);
|
||||
if (!eventName) return [];
|
||||
|
||||
const fields: string[] = await Redis.useCache({
|
||||
key: `metadata_fields:${project_id}:${eventName}`,
|
||||
exp: 60
|
||||
}, async () => {
|
||||
const eventsWithName = await EventModel.find({ project_id, name: eventName }, { metadata: 1 }, { limit: 10, sort: { created_at: -1 } });
|
||||
const allMetadata = eventsWithName.map(e => e.metadata);
|
||||
const allFields = new Set<string>();
|
||||
for (const metadata of allMetadata) {
|
||||
const keys = Object.keys(metadata || {});
|
||||
keys.forEach(key => allFields.add(key));
|
||||
}
|
||||
return Array.from(allFields.values());
|
||||
});
|
||||
|
||||
return fields;
|
||||
|
||||
});
|
||||
26
dashboard/server/api/data/events_data/names.ts
Normal file
26
dashboard/server/api/data/events_data/names.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const names: string[] = await Redis.useCache({
|
||||
key: `event_names:${project_id}`,
|
||||
exp: 60
|
||||
}, async () => {
|
||||
const namesAggregation = await EventModel.aggregate([
|
||||
{ $match: { project_id } },
|
||||
{ $group: { _id: "$name" } }
|
||||
]);
|
||||
return namesAggregation.map(e => e._id);
|
||||
});
|
||||
|
||||
return names;
|
||||
|
||||
});
|
||||
25
dashboard/server/api/data/live_users.ts
Normal file
25
dashboard/server/api/data/live_users.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const online_users = await SessionModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
updated_at: { $gt: new Date(Date.now() - 1000 * 60 * 5) }
|
||||
}
|
||||
},
|
||||
{ $count: 'count' }
|
||||
]);
|
||||
|
||||
if (!online_users[0]) return 0;
|
||||
|
||||
return online_users[0].count;
|
||||
|
||||
});
|
||||
34
dashboard/server/api/data/oss.ts
Normal file
34
dashboard/server/api/data/oss.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
import { getRequestData } from "~/server/utils/getRequestData";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { pid, from, to, project_id, limit } = data;
|
||||
|
||||
const cacheKey = `oss:${pid}:${limit}:${from}:${to}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$os", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user