mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-11 00:08:37 +01:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9de299d841 | ||
|
|
2929b229c4 | ||
|
|
f06d7d78fc | ||
|
|
4d7cfbb7b9 | ||
|
|
b4c0620f17 | ||
|
|
b8c2e40f7a | ||
|
|
e866a1c22b | ||
|
|
f86a399840 | ||
|
|
36c4406af2 | ||
|
|
b2afd585bb | ||
|
|
24ae9d0e0d | ||
|
|
b479ca1bbf | ||
|
|
0a748346c5 | ||
|
|
fa7880552a | ||
|
|
06fb8bfab0 | ||
|
|
a876d77d42 | ||
|
|
e6bb58693f | ||
|
|
00e63cc80b | ||
|
|
e43f138945 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
steps
|
steps
|
||||||
PROCESS_EVENT
|
PROCESS_EVENT
|
||||||
|
**/node_modules/
|
||||||
docker
|
docker
|
||||||
dev
|
dev
|
||||||
docker-compose.admin.yml
|
docker-compose.admin.yml
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ You can install Litlyx using `npm`, `pnpm`, `yarn` or any modern package manager
|
|||||||
npm i litlyx-js
|
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">
|
<p align="center">
|
||||||
<img src="assets/tech.png" />
|
<img src="assets/tech.png" />
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 123 KiB |
@@ -67,6 +67,9 @@ const { visible } = usePricingDrawer();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<UModals />
|
||||||
|
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage></NuxtPage>
|
<NuxtPage></NuxtPage>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
|
||||||
export type IconProvider = (id: string) => ['img' | 'icon', string] | undefined;
|
export type IconProvider = (e: { _id: string, count: string } & any) => ['img' | 'icon', string] | undefined;
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -80,7 +80,7 @@ function openExternalLink(link: string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="h-full flex flex-col">
|
||||||
<div class="flex justify-between font-bold text-text-sub/80 text-[1.1rem] mb-4">
|
<div class="flex justify-between font-bold text-text-sub/80 text-[1.1rem] mb-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div v-if="isDetailView" class="flex items-center justify-center">
|
<div v-if="isDetailView" class="flex items-center justify-center">
|
||||||
@@ -111,13 +111,13 @@ function openExternalLink(link: string) {
|
|||||||
:style="'width:' + 100 / maxData * element.count + '%;'"></div>
|
:style="'width:' + 100 / maxData * element.count + '%;'"></div>
|
||||||
|
|
||||||
<div class="flex px-2 py-1 relative items-center gap-4">
|
<div class="flex px-2 py-1 relative items-center gap-4">
|
||||||
<div v-if="iconProvider && iconProvider(element._id) != undefined"
|
<div v-if="iconProvider && iconProvider(element) != undefined"
|
||||||
class="flex items-center h-[1.3rem]">
|
class="flex items-center h-[1.3rem]">
|
||||||
|
|
||||||
<img v-if="iconProvider(element._id)?.[0] == 'img'" class="h-full"
|
<img v-if="iconProvider(element)?.[0] == 'img'" class="h-full"
|
||||||
:style="customIconStyle" :src="iconProvider(element._id)?.[1]">
|
:style="customIconStyle" :src="iconProvider(element)?.[1]">
|
||||||
|
|
||||||
<i v-else :class="iconProvider(element._id)?.[1]"></i>
|
<i v-else :class="iconProvider(element)?.[1]"></i>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-text/70">
|
<span class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-text/70">
|
||||||
{{ elementTextTransformer?.(element._id) || element._id }}
|
{{ elementTextTransformer?.(element._id) || element._id }}
|
||||||
@@ -125,15 +125,14 @@ function openExternalLink(link: string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-text font-semibold text-[.9rem] md:text-[1rem] manrope"> {{
|
<div class="text-text font-semibold text-[.9rem] md:text-[1rem] manrope"> {{
|
||||||
formatNumberK(element.count) }} </div>
|
formatNumberK(element.count) }} </div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-bold text-[1.1rem]">
|
<div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-light text-[1.1rem]">
|
||||||
No data yet
|
No data yet
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 ">
|
<div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 items-end grow">
|
||||||
<div @click="$emit('showMore')"
|
<div @click="$emit('showMore')"
|
||||||
class="poppins hover:bg-black cursor-pointer w-fit px-6 py-1 rounded-lg border-[1px] border-text-sub text-[.9rem]">
|
class="poppins hover:bg-black cursor-pointer w-fit px-6 py-1 rounded-lg border-[1px] border-text-sub text-[.9rem]">
|
||||||
Show more
|
Show more
|
||||||
|
|||||||
@@ -1,5 +1,34 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import type { IconProvider } from './Base.vue';
|
||||||
|
|
||||||
|
function iconProvider(e: { _id: string, flag: string, count: number }): ReturnType<IconProvider> {
|
||||||
|
let name = e._id.toLowerCase().replace(/ /g, '-');
|
||||||
|
|
||||||
|
if (name === 'mobile-safari') name = 'safari';
|
||||||
|
if (name === 'chrome-headless') name = 'chrome'
|
||||||
|
if (name === 'chrome-webview') name = 'chrome'
|
||||||
|
|
||||||
|
if (name === 'duckduckgo') return ['icon', 'far fa-duck']
|
||||||
|
if (name === 'avast-secure-browser') return ['icon', 'far fa-bug']
|
||||||
|
if (name === 'avg-secure-browser') return ['icon', 'far fa-bug']
|
||||||
|
|
||||||
|
if (name === 'no_browser') return ['icon', 'far fa-question']
|
||||||
|
if (name === 'gsa') return ['icon', 'far fa-question']
|
||||||
|
if (name === 'miui-browser') return ['icon', 'far fa-question']
|
||||||
|
|
||||||
|
if (name === 'vivo-browser') return ['icon', 'far fa-question']
|
||||||
|
if (name === 'whale') return ['icon', 'far fa-question']
|
||||||
|
|
||||||
|
if (name === 'twitter') return ['icon', 'fab fa-twitter']
|
||||||
|
if (name === 'linkedin') return ['icon', 'fab fa-linkedin']
|
||||||
|
if (name === 'facebook') return ['icon', 'fab fa-facebook']
|
||||||
|
|
||||||
|
return [
|
||||||
|
'img',
|
||||||
|
`https://github.com/alrra/browser-logos/blob/main/src/${name}/${name}_256x256.png?raw=true`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const browsersData = useFetch('/api/data/browsers', {
|
const browsersData = useFetch('/api/data/browsers', {
|
||||||
headers: useComputedHeaders({ limit: 10, }), lazy: true
|
headers: useComputedHeaders({ limit: 10, }), lazy: true
|
||||||
@@ -8,7 +37,7 @@ const browsersData = useFetch('/api/data/browsers', {
|
|||||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||||
|
|
||||||
async function showMore() {
|
async function showMore() {
|
||||||
dialogBarData.value=[];
|
dialogBarData.value = [];
|
||||||
showDialog.value = true;
|
showDialog.value = true;
|
||||||
isDataLoading.value = true;
|
isDataLoading.value = true;
|
||||||
|
|
||||||
@@ -16,7 +45,9 @@ async function showMore() {
|
|||||||
headers: useComputedHeaders({ limit: 1000 }).value
|
headers: useComputedHeaders({ limit: 1000 }).value
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogBarData.value = res || [];
|
dialogBarData.value = res?.map(e => {
|
||||||
|
return { ...e, icon: iconProvider(e as any) }
|
||||||
|
}) || [];
|
||||||
|
|
||||||
isDataLoading.value = false;
|
isDataLoading.value = false;
|
||||||
|
|
||||||
@@ -28,8 +59,8 @@ async function showMore() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" :data="browsersData.data.value || []"
|
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" :data="browsersData.data.value || []"
|
||||||
desc="The browsers most used to search your website." :dataIcons="false"
|
desc="The browsers most used to search your website." :dataIcons="true" :iconProvider="iconProvider"
|
||||||
:loading="browsersData.pending.value" label="Top Browsers" sub-label="Browsers">
|
:loading="browsersData.pending.value" label="Browsers" sub-label="Browsers">
|
||||||
</BarCardBase>
|
</BarCardBase>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
|
||||||
|
import type { IconProvider } from './Base.vue';
|
||||||
|
|
||||||
|
function iconProvider(e: { _id: string, count: number }): ReturnType<IconProvider> {
|
||||||
|
if (e._id === 'desktop') return ['icon','far fa-desktop'];
|
||||||
|
if (e._id === 'tablet') return ['icon','far fa-tablet'];
|
||||||
|
if (e._id === 'mobile') return ['icon','far fa-mobile'];
|
||||||
|
if (e._id === 'smarttv') return ['icon','far fa-tv'];
|
||||||
|
if (e._id === 'console') return ['icon','far fa-game-console-handheld'];
|
||||||
|
return ['icon', 'far fa-question']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function transform(data: { _id: string, count: number }[]) {
|
function transform(data: { _id: string, count: number }[]) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
return data.map(e => ({ ...e, _id: e._id == null ? 'unknown' : e._id }))
|
return data.map(e => ({ ...e, _id: e._id == null ? 'unknown' : e._id }))
|
||||||
@@ -34,9 +46,9 @@ async function showMore() {
|
|||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2 h-full">
|
||||||
<BarCardBase @showMore="showMore()" @dataReload="devicesData.refresh()" :data="devicesData.data.value || []"
|
<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"
|
:iconProvider="iconProvider" :dataIcons="true" desc="The devices most used to access your website."
|
||||||
label="Top Devices" sub-label="Devices"></BarCardBase>
|
:loading="devicesData.pending.value" label="Devices" sub-label="Devices"></BarCardBase>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,34 +2,42 @@
|
|||||||
|
|
||||||
import type { IconProvider } from '../BarCard/Base.vue';
|
import type { IconProvider } from '../BarCard/Base.vue';
|
||||||
|
|
||||||
function iconProvider(id: string): ReturnType<IconProvider> {
|
function iconProvider(e: { _id: string, flag: string, count: number }): ReturnType<IconProvider> {
|
||||||
if (id === 'self') return ['icon', 'fas fa-link'];
|
if (!e.flag) return ['icon', 'far fa-question']
|
||||||
return [
|
return [
|
||||||
'img',
|
'img',
|
||||||
`https://raw.githubusercontent.com/hampusborgos/country-flags/main/png250px/${id.toLowerCase()}.png`
|
`https://raw.githubusercontent.com/hampusborgos/country-flags/main/png250px/${e.flag.toLowerCase()}.png`
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const customIconStyle = `width: 2rem; padding: 1px;`
|
const customIconStyle = `width: 2rem; padding: 1px;`
|
||||||
|
|
||||||
const geolocationData = useFetch('/api/data/countries', {
|
const geolocationData = useFetch('/api/data/countries', {
|
||||||
headers: useComputedHeaders({ limit: 10, }), lazy: true
|
headers: useComputedHeaders({ limit: 10, }), lazy: true,
|
||||||
|
transform: (e) => {
|
||||||
|
if (!e) return e;
|
||||||
|
return e.map(k => {
|
||||||
|
return { ...k, flag: k._id, _id: getCountryName(k._id) ?? k._id }
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||||
|
|
||||||
async function showMore() {
|
async function showMore() {
|
||||||
dialogBarData.value=[];
|
dialogBarData.value = [];
|
||||||
showDialog.value = true;
|
showDialog.value = true;
|
||||||
isDataLoading.value = true;
|
isDataLoading.value = true;
|
||||||
|
|
||||||
const res = await $fetch('/api/data/countries', {
|
const res = await $fetch('/api/data/countries', {
|
||||||
headers: useComputedHeaders({limit: 1000}).value
|
headers: useComputedHeaders({ limit: 1000 }).value
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogBarData.value = res?.map(e => {
|
dialogBarData.value = res?.map(k => {
|
||||||
return { ...e, icon: iconProvider(e._id) }
|
return { ...k, flag: k._id, _id: getCountryName(k._id) ?? k._id }
|
||||||
|
}).map(e => {
|
||||||
|
return { ...e, icon: iconProvider(e) }
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
isDataLoading.value = false;
|
isDataLoading.value = false;
|
||||||
@@ -43,7 +51,7 @@ async function showMore() {
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()"
|
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()"
|
||||||
:data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value"
|
:data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value"
|
||||||
label="Top Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"
|
label="Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"
|
||||||
desc=" Lists the countries where users access your website.">
|
desc=" Lists the countries where users access your website.">
|
||||||
</BarCardBase>
|
</BarCardBase>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ async function showMore() {
|
|||||||
<div class="flex flex-col gap-2 h-full">
|
<div class="flex flex-col gap-2 h-full">
|
||||||
<BarCardBase @showMore="showMore()" @dataReload="ossData.refresh()" :data="ossData.data.value || []"
|
<BarCardBase @showMore="showMore()" @dataReload="ossData.refresh()" :data="ossData.data.value || []"
|
||||||
desc="The operating systems most commonly used by your website's visitors." :dataIcons="false"
|
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>
|
:loading="ossData.pending.value" label="OS" sub-label="OSs"></BarCardBase>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import type { IconProvider } from './Base.vue';
|
import type { IconProvider } from './Base.vue';
|
||||||
|
|
||||||
function iconProvider(id: string): ReturnType<IconProvider> {
|
function iconProvider(e: { _id: string, count: number }): ReturnType<IconProvider> {
|
||||||
if (id === 'self') return ['icon', 'fas fa-link'];
|
if (e._id === 'self') return ['icon', 'fas fa-link'];
|
||||||
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${id}&sz=64`]
|
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${e._id}&sz=64`]
|
||||||
}
|
}
|
||||||
|
|
||||||
function elementTextTransformer(element: string) {
|
function elementTextTransformer(element: string) {
|
||||||
@@ -22,18 +22,18 @@ const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
|||||||
|
|
||||||
async function showMore() {
|
async function showMore() {
|
||||||
|
|
||||||
dialogBarData.value=[];
|
dialogBarData.value = [];
|
||||||
|
|
||||||
showDialog.value = true;
|
showDialog.value = true;
|
||||||
isDataLoading.value = true;
|
isDataLoading.value = true;
|
||||||
|
|
||||||
const res = await $fetch('/api/data/referrers', {
|
const res = await $fetch('/api/data/referrers', {
|
||||||
headers: useComputedHeaders({limit: 1000}).value
|
headers: useComputedHeaders({ limit: 1000 }).value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
dialogBarData.value = res?.map(e => {
|
dialogBarData.value = res?.map(e => {
|
||||||
return { ...e, icon: iconProvider(e._id) }
|
return { ...e, icon: iconProvider(e as any) }
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
isDataLoading.value = false;
|
isDataLoading.value = false;
|
||||||
@@ -47,7 +47,7 @@ async function showMore() {
|
|||||||
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
|
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
|
||||||
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
|
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
|
||||||
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."
|
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."
|
||||||
:dataIcons="true" :loading="referrersData.pending.value" label="Top Referrers" sub-label="Referrers">
|
:dataIcons="true" :loading="referrersData.pending.value" label="Top Sources" sub-label="Referrers">
|
||||||
</BarCardBase>
|
</BarCardBase>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const { createAlert } = useAlert()
|
|||||||
async function deleteSnapshot(close: () => any) {
|
async function deleteSnapshot(close: () => any) {
|
||||||
await $fetch("/api/snapshot/delete", {
|
await $fetch("/api/snapshot/delete", {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
headers: useComputedHeaders({ useSnapshotDates: false }).value,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: snapshot.value._id.toString(),
|
id: snapshot.value._id.toString(),
|
||||||
})
|
})
|
||||||
@@ -71,11 +71,7 @@ async function generatePDF() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await $fetch<Blob>('/api/project/generate_pdf', {
|
const res = await $fetch<Blob>('/api/project/generate_pdf', {
|
||||||
...signHeaders({
|
headers: useComputedHeaders({ useSnapshotDates: false, custom: { 'x-snapshot-name': snapshot.value.name } }).value,
|
||||||
'x-snapshot-name': snapshot.value.name,
|
|
||||||
'x-from': snapshot.value.from.toISOString(),
|
|
||||||
'x-to': snapshot.value.to.toISOString(),
|
|
||||||
}),
|
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,11 +140,20 @@ const pricingDrawer = usePricingDrawer();
|
|||||||
<LyxUiButton to="/project_creation" v-if="projectList && (projectList.length < (maxProjects || 1))"
|
<LyxUiButton to="/project_creation" v-if="projectList && (projectList.length < (maxProjects || 1))"
|
||||||
type="outlined" class="w-full py-1 mt-2 text-[.8rem]">
|
type="outlined" class="w-full py-1 mt-2 text-[.8rem]">
|
||||||
<div class="flex items-center gap-2 justify-center">
|
<div class="flex items-center gap-2 justify-center">
|
||||||
<div><i class="fas fa-plus"></i></div>
|
<div><i class="fas fa-plus text-[.7rem]"></i></div>
|
||||||
<div> Create new project </div>
|
<div class="poppins"> New Project </div>
|
||||||
</div>
|
</div>
|
||||||
</LyxUiButton>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -203,12 +208,12 @@ const pricingDrawer = usePricingDrawer();
|
|||||||
<div v-if="snapshot" class="flex flex-col text-[.7rem] mt-2">
|
<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="flex gap-1 items-center justify-center text-lyx-text-dark">
|
||||||
<div class="poppins">
|
<div class="poppins">
|
||||||
{{ new Date(snapshot.from).toLocaleString('it-IT').split(',')[0].trim().replace(/\//g, '-')
|
{{ new Date(snapshot.from).toLocaleString().split(',')[0].trim()
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="poppins"> to </div>
|
<div class="poppins"> to </div>
|
||||||
<div class="poppins">
|
<div class="poppins">
|
||||||
{{ new Date(snapshot.to).toLocaleString('it-IT').split(',')[0].trim().replace(/\//g, '-') }}
|
{{ new Date(snapshot.to).toLocaleString().split(',')[0].trim() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -243,7 +248,7 @@ const pricingDrawer = usePricingDrawer();
|
|||||||
|
|
||||||
<div v-for="entry of section.entries" :class="{ 'grow flex items-end': entry.grow }">
|
<div v-for="entry of section.entries" :class="{ 'grow flex items-end': entry.grow }">
|
||||||
|
|
||||||
<div v-if="(!entry.adminOnly || (userRoles.isAdmin && !isAdminHidden))"
|
<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="bg-lyx-background w-full cursor-pointer text-lyx-text-dark py-[.35rem] px-2 rounded-lg text-[.95rem] flex items-center"
|
||||||
:class="{
|
:class="{
|
||||||
'!text-lyx-text-darker pointer-events-none': entry.disabled,
|
'!text-lyx-text-darker pointer-events-none': entry.disabled,
|
||||||
@@ -259,7 +264,7 @@ const pricingDrawer = usePricingDrawer();
|
|||||||
<div class="manrope grow">
|
<div class="manrope grow">
|
||||||
{{ entry.label }}
|
{{ entry.label }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="entry.premiumOnly && !userRoles.isPremium" class="flex items-center">
|
<div v-if="entry.premiumOnly && !userRoles.isPremium.value" class="flex items-center">
|
||||||
<i class="fal fa-lock"></i>
|
<i class="fal fa-lock"></i>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
@@ -293,7 +298,8 @@ const pricingDrawer = usePricingDrawer();
|
|||||||
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
||||||
<i class="fab fa-dev"></i>
|
<i class="fab fa-dev"></i>
|
||||||
</NuxtLink> -->
|
</NuxtLink> -->
|
||||||
<NuxtLink to="/admin" v-if="userRoles.isAdmin"
|
|
||||||
|
<NuxtLink to="/admin" v-if="userRoles.isAdmin.value"
|
||||||
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
||||||
<i class="fas fa-cat"></i>
|
<i class="fas fa-cat"></i>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import 'highlight.js/styles/stackoverflow-dark.css';
|
|||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
import CardTitled from './CardTitled.vue';
|
import CardTitled from './CardTitled.vue';
|
||||||
|
|
||||||
|
import { Lit } from 'litlyx-js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
firstInteraction: boolean,
|
firstInteraction: boolean,
|
||||||
refreshInteraction: () => any
|
refreshInteraction: () => any
|
||||||
@@ -19,6 +21,7 @@ onMounted(() => {
|
|||||||
function copyProjectId() {
|
function copyProjectId() {
|
||||||
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
||||||
navigator.clipboard.writeText(project.value?._id?.toString() || '');
|
navigator.clipboard.writeText(project.value?._id?.toString() || '');
|
||||||
|
Lit.event('no_visit_copy_id');
|
||||||
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +39,7 @@ function copyScript() {
|
|||||||
].join('')
|
].join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Lit.event('no_visit_copy_script');
|
||||||
navigator.clipboard.writeText(createScriptText());
|
navigator.clipboard.writeText(createScriptText());
|
||||||
createAlert('Success', 'Script copied successfully.', 'far fa-circle-check', 5000);
|
createAlert('Success', 'Script copied successfully.', 'far fa-circle-check', 5000);
|
||||||
}
|
}
|
||||||
@@ -53,6 +57,7 @@ const scriptText = computed(() => {
|
|||||||
function reloadPage() {
|
function reloadPage() {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -76,26 +81,32 @@ function reloadPage() {
|
|||||||
|
|
||||||
<div class="flex items-center justify-center mt-10">
|
<div class="flex items-center justify-center mt-10">
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
<div class="flex gap-6">
|
|
||||||
<div>
|
<div class="flex gap-6 xl:flex-row flex-col">
|
||||||
<CardTitled class="h-full" title="Tutorial" sub="Coming soon. For now enjoy our launch video.">
|
|
||||||
<div class="flex items-center justify-center h-full">
|
<div class="h-full w-full">
|
||||||
<iframe width="560" height="315"
|
<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"
|
src="https://www.youtube.com/embed/GntyWMR7jsY?si=YGGkQwrk6-Iqmn8w" title="Litlyx"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</CardTitled>
|
</CardTitled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
|
|
||||||
<div>
|
<div class="w-full">
|
||||||
<CardTitled title="Quick Integration"
|
<CardTitled title="Quick Integration"
|
||||||
sub="Start tracking web analytics in one line. (works everywhere js is supported)">
|
sub="Start tracking web analytics in one line. (works everywhere js is supported)">
|
||||||
<div class="flex flex-col items-end gap-4">
|
<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>
|
<pre><code class="language-html">{{ scriptText }}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<LyxUiButton type="secondary" @click="copyScript()">
|
<LyxUiButton type="secondary" @click="copyScript()">
|
||||||
@@ -122,7 +133,7 @@ function reloadPage() {
|
|||||||
<CardTitled class="w-full h-full" title="Documentation"
|
<CardTitled class="w-full h-full" title="Documentation"
|
||||||
sub="Learn how to use Litlyx in every tech stack">
|
sub="Learn how to use Litlyx in every tech stack">
|
||||||
<div class="flex flex-col items-end">
|
<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"
|
<svg width="680" height="100" viewBox="0 0 680 100" fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<mask id="path-1-inside-1_473_1361" fill="white">
|
<mask id="path-1-inside-1_473_1361" fill="white">
|
||||||
@@ -250,7 +261,9 @@ function reloadPage() {
|
|||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<LyxUiButton type="secondary" to="https://docs.litlyx.com"> Visit documentation
|
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
|
||||||
|
to="https://docs.litlyx.com">
|
||||||
|
Visit documentation
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
</CardTitled>
|
</CardTitled>
|
||||||
@@ -263,7 +276,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="bg-menu p-6 rounded-xl flex flex-col gap-2 w-full">
|
||||||
<div class="poppins font-semibold"> Copy your project_id: </div>
|
<div class="poppins font-semibold"> Copy your project_id: </div>
|
||||||
@@ -273,7 +286,7 @@ function reloadPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="poppins font-semibold">
|
||||||
Start logging visits in 1 click | Plug anywhere !
|
Start logging visits in 1 click | Plug anywhere !
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,15 +20,16 @@ const isPremium = computed(() => {
|
|||||||
<div v-if="!isPremium" class="w-full bg-[#5680f822] p-4 rounded-lg text-[.9rem] flex items-center">
|
<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="flex flex-col grow">
|
||||||
<div class="poppins font-semibold text-lyx-primary">
|
<div class="poppins font-semibold text-lyx-primary">
|
||||||
Launch offer: 25% off
|
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>
|
||||||
<div class="poppins text-lyx-primary">
|
<!-- <div class="poppins text-lyx-primary">
|
||||||
We're offering an exclusive 25% discount forever on all plans starting from the Acceleration
|
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.
|
Plan for our first 100 users who believe in our project.
|
||||||
<br>
|
<br>
|
||||||
Redeem Code: <span class="text-white font-bold text-[1rem]">LIT25</span> at checkout to
|
Redeem Code: <span class="text-white font-bold text-[1rem]">LIT25</span> at checkout to
|
||||||
claim your discount.
|
claim your discount.
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
|
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ const legendClasses = ref<string[]>([
|
|||||||
</SelectButton>
|
</SelectButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex gap-6 w-full justify-between">
|
<div class="flex gap-6 w-full justify-between lg:flex-row flex-col">
|
||||||
<LyxUiButton type="secondary" :to="isLiveDemo ? '#' : '/analyst'" :disabled="isLiveDemo">
|
<LyxUiButton type="secondary" :to="isLiveDemo ? '#' : '/analyst'" :disabled="isLiveDemo">
|
||||||
<div class="flex items-center gap-2 px-10">
|
<div class="flex items-center gap-2 px-10">
|
||||||
<i class="far fa-sparkles text-yellow-400"></i>
|
<i class="far fa-sparkles text-yellow-400"></i>
|
||||||
|
|||||||
@@ -21,14 +21,24 @@ const chartSlice = computed(() => {
|
|||||||
|
|
||||||
|
|
||||||
function transformResponse(input: { _id: string, count: number }[]) {
|
function transformResponse(input: { _id: string, count: number }[]) {
|
||||||
|
|
||||||
const data = input.map(e => e.count || 0);
|
const data = input.map(e => e.count || 0);
|
||||||
|
|
||||||
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, chartSlice.value));
|
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, chartSlice.value));
|
||||||
|
|
||||||
const pool = [...input.map(e => e.count || 0)];
|
const pool = [...input.map(e => e.count || 0)];
|
||||||
pool.pop();
|
|
||||||
const avg = pool.reduce((a, e) => a + e, 0) / pool.length;
|
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);
|
const trend = Math.max(Math.min(diffPercent, 99), -99);
|
||||||
|
|
||||||
return { data, labels, trend }
|
return { data, labels, trend }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const visitsData = useFetch('/api/timeline/visits', {
|
const visitsData = useFetch('/api/timeline/visits', {
|
||||||
@@ -94,7 +104,7 @@ const avgSessionDuration = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="gap-6 px-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 m-cards-wrap:grid-cols-4">
|
<div class="gap-6 px-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 m-cards-wrap:grid-cols-4">
|
||||||
|
|
||||||
<DashboardCountCard :ready="!visitsData.pending.value" icon="far fa-earth" text="Total page visits"
|
<DashboardCountCard :ready="!visitsData.pending.value" icon="far fa-earth" text="Total visits"
|
||||||
:value="formatNumberK(visitsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
|
:value="formatNumberK(visitsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
|
||||||
:avg="formatNumberK(avgVisitDay) + '/day'" :trend="visitsData.data.value?.trend"
|
:avg="formatNumberK(avgVisitDay) + '/day'" :trend="visitsData.data.value?.trend"
|
||||||
:data="visitsData.data.value?.data" :labels="visitsData.data.value?.labels" color="#5655d7">
|
:data="visitsData.data.value?.data" :labels="visitsData.data.value?.labels" color="#5655d7">
|
||||||
@@ -106,7 +116,7 @@ const avgSessionDuration = computed(() => {
|
|||||||
</DashboardCountCard>
|
</DashboardCountCard>
|
||||||
|
|
||||||
|
|
||||||
<DashboardCountCard :ready="!sessionsData.pending.value" icon="far fa-user" text="Unique visits sessions"
|
<DashboardCountCard :ready="!sessionsData.pending.value" icon="far fa-user" text="Unique visitors"
|
||||||
:value="formatNumberK(sessionsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
|
:value="formatNumberK(sessionsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
|
||||||
:avg="formatNumberK(avgSessionsDay) + '/day'" :trend="sessionsData.data.value?.trend"
|
:avg="formatNumberK(avgSessionsDay) + '/day'" :trend="sessionsData.data.value?.trend"
|
||||||
:data="sessionsData.data.value?.data" :labels="sessionsData.data.value?.labels" color="#4abde8">
|
:data="sessionsData.data.value?.data" :labels="sessionsData.data.value?.labels" color="#4abde8">
|
||||||
@@ -114,7 +124,7 @@ const avgSessionDuration = computed(() => {
|
|||||||
|
|
||||||
|
|
||||||
<DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer"
|
<DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer"
|
||||||
text="Total avg session time" :value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
|
text="Visit duration" :value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
|
||||||
:data="sessionsDurationData.data.value?.data" :labels="sessionsDurationData.data.value?.labels"
|
:data="sessionsDurationData.data.value?.data" :labels="sessionsDurationData.data.value?.labels"
|
||||||
color="#f56523">
|
color="#f56523">
|
||||||
</DashboardCountCard>
|
</DashboardCountCard>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const { createAlert } = useAlert()
|
|||||||
async function confirmSnapshot() {
|
async function confirmSnapshot() {
|
||||||
await $fetch("/api/snapshot/create", {
|
await $fetch("/api/snapshot/create", {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
headers: useComputedHeaders({ useSnapshotDates: false }).value,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: snapshotName.value,
|
name: snapshotName.value,
|
||||||
color: currentColor.value,
|
color: currentColor.value,
|
||||||
|
|||||||
82
dashboard/components/dialog/DeleteDomainData.vue
Normal file
82
dashboard/components/dialog/DeleteDomainData.vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const emit = defineEmits(['success', 'cancel'])
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
buttonType: string,
|
||||||
|
message: string,
|
||||||
|
deleteData: { isAll: boolean, visits: boolean, sessions: boolean, events: boolean, domain: string }
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isDone = ref<boolean>(false);
|
||||||
|
const canDelete = ref<boolean>(false);
|
||||||
|
|
||||||
|
async function deleteData() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (props.deleteData.isAll) {
|
||||||
|
await $fetch('/api/settings/delete_all', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: useComputedHeaders({ useSnapshotDates: false }).value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await $fetch('/api/settings/delete_domain', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: useComputedHeaders({ useSnapshotDates: false, custom: { 'Content-Type': 'application/json' } }).value,
|
||||||
|
body: JSON.stringify({
|
||||||
|
domain: props.deleteData.domain,
|
||||||
|
visits: props.deleteData.visits,
|
||||||
|
sessions: props.deleteData.sessions,
|
||||||
|
events: props.deleteData.events,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
alert('Something went wrong');
|
||||||
|
console.error(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDone.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UModal :ui="{
|
||||||
|
strategy: 'override',
|
||||||
|
overlay: {
|
||||||
|
background: 'bg-lyx-background/85'
|
||||||
|
},
|
||||||
|
background: 'bg-lyx-widget',
|
||||||
|
ring: 'border-solid border-[1px] border-[#262626]'
|
||||||
|
}">
|
||||||
|
<div class="h-full flex flex-col gap-2 p-4">
|
||||||
|
|
||||||
|
<div class="font-semibold text-[1.2rem]"> {{ isDone ? "Data Deletion Scheduled" : "Are you sure ?" }}</div>
|
||||||
|
|
||||||
|
<div v-if="!isDone">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isDone">
|
||||||
|
Your data deletion request is being processed and will be reflected in your project dashboard within a
|
||||||
|
few minutes.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grow"></div>
|
||||||
|
|
||||||
|
<div v-if="!isDone">
|
||||||
|
<UCheckbox v-model="canDelete" label="Confirm data delete"></UCheckbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isDone" class="flex justify-end gap-2">
|
||||||
|
<LyxUiButton type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton>
|
||||||
|
<LyxUiButton :disabled="!canDelete" @click="canDelete ? deleteData() : () => { }" :type="buttonType"> Confirm </LyxUiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isDone" class="flex justify-end w-full">
|
||||||
|
<LyxUiButton type="secondary" @click="emit('success')"> Dismiss </LyxUiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
@@ -111,7 +111,7 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const eventsData = useFetch(`/api/data/events`, {
|
const eventsData = useFetch(`/api/data/events`, {
|
||||||
headers: useComputedHeaders(), lazy: true, immediate: false
|
headers: useComputedHeaders(), lazy: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const enabledEvents = ref<string[]>([]);
|
const enabledEvents = ref<string[]>([]);
|
||||||
@@ -140,7 +140,7 @@ async function onEventCheck(eventName: string) {
|
|||||||
<template>
|
<template>
|
||||||
<CardTitled title="Funnel"
|
<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">
|
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">
|
<div class="flex gap-2 justify-between lg:flex-row flex-col">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div class="min-w-[20rem] text-lyx-text-darker">
|
<div class="min-w-[20rem] text-lyx-text-darker">
|
||||||
Select two or more events
|
Select two or more events
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const canSearch = computed(() => {
|
|||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-lyx-text-darker poppins mt-4 flex items-center gap-4">
|
<div class="text-lyx-text-darker poppins mt-4 flex items-center gap-4 lg:flex-row flex-col">
|
||||||
<div class="w-[10rem]">
|
<div class="w-[10rem]">
|
||||||
Search results: {{ metadataFieldGroupedFiltered.length }}
|
Search results: {{ metadataFieldGroupedFiltered.length }}
|
||||||
</div>
|
</div>
|
||||||
@@ -119,13 +119,13 @@ const canSearch = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 mt-4">
|
<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"
|
<div class="bg-lyx-widget-light text-lyx-text-dark px-3 py-2 rounded-md w-fit"
|
||||||
v-for="item of metadataFieldGroupedFiltered">
|
v-for="item of metadataFieldGroupedFiltered">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2 items-center">
|
||||||
<div> {{ item._id || 'OLD_EVENTS' }} </div>
|
<div> {{ item._id || 'OLD_EVENTS' }} </div>
|
||||||
<div> {{ item.count }} </div>
|
<div class="px-1"> {{ item.count }} </div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ function transformResponse(input: { _id: string, name: string, count: number }[]
|
|||||||
data: input,
|
data: input,
|
||||||
from: input[0]._id,
|
from: input[0]._id,
|
||||||
to: safeSnapshotDates.value.to
|
to: safeSnapshotDates.value.to
|
||||||
}, slice.value, {
|
},
|
||||||
advanced: true,
|
slice.value,
|
||||||
advancedGroupKey: 'name'
|
{ advanced: true, advancedGroupKey: 'name' });
|
||||||
});
|
|
||||||
|
|
||||||
const parsedDatasets: any[] = [];
|
const parsedDatasets: any[] = [];
|
||||||
|
|
||||||
@@ -62,6 +61,7 @@ function transformResponse(input: { _id: string, name: string, count: number }[]
|
|||||||
datasets: parsedDatasets,
|
datasets: parsedDatasets,
|
||||||
labels: fixed.labels
|
labels: fixed.labels
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorData = ref<{ errored: boolean, text: string }>({
|
const errorData = ref<{ errored: boolean, text: string }>({
|
||||||
@@ -83,7 +83,7 @@ function onResponse(e: any) {
|
|||||||
const eventsStackedData = useFetch(`/api/timeline/events_stacked`, {
|
const eventsStackedData = useFetch(`/api/timeline/events_stacked`, {
|
||||||
lazy: true, immediate: false,
|
lazy: true, immediate: false,
|
||||||
transform: transformResponse,
|
transform: transformResponse,
|
||||||
headers: useComputedHeaders({slice}),
|
headers: useComputedHeaders({ slice }),
|
||||||
onResponseError,
|
onResponseError,
|
||||||
onResponse
|
onResponse
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,7 +62,13 @@ async function analyzeEvent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="analyzing"> Analyzing... </div>
|
<div v-if="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 flex-col gap-2" v-if="userFlowData">
|
||||||
<div class="flex gap-4 items-center bg-bg py-2 px-2 bg-lyx-widget-light rounded-lg"
|
<div class="flex gap-4 items-center bg-bg py-2 px-2 bg-lyx-widget-light rounded-lg"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ async function onUpgradeClick() {
|
|||||||
|
|
||||||
<div class="flex flex-col gap-3 text-center pt-3">
|
<div class="flex flex-col gap-3 text-center pt-3">
|
||||||
<div v-if="data.active"
|
<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
|
Active
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!data.active && data.title === 'Growth'"
|
<div v-if="!data.active && data.title === 'Growth'"
|
||||||
|
|||||||
58
dashboard/components/settings/Codes.vue
Normal file
58
dashboard/components/settings/Codes.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { TApiSettings } from '@schema/ApiSettingsSchema';
|
||||||
|
import type { SettingsTemplateEntry } from './Template.vue';
|
||||||
|
|
||||||
|
const { project } = useProject();
|
||||||
|
|
||||||
|
const entries: SettingsTemplateEntry[] = [
|
||||||
|
{ id: 'acodes', title: 'Appsumo codes', text: 'Redeem appsumo codes' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const { createAlert } = useAlert()
|
||||||
|
|
||||||
|
const currentCode = ref<string>("");
|
||||||
|
const redeeming = ref<boolean>(false);
|
||||||
|
|
||||||
|
const valid_codes = useFetch('/api/pay/valid_codes', signHeaders({ 'x-pid': project.value?._id.toString() ?? '' }));
|
||||||
|
|
||||||
|
async function redeemCode() {
|
||||||
|
redeeming.value = true;
|
||||||
|
try {
|
||||||
|
const res = await $fetch<TApiSettings>('/api/pay/redeem_appsumo_code', {
|
||||||
|
method: 'POST', ...signHeaders({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-pid': project.value?._id.toString() ?? ''
|
||||||
|
}),
|
||||||
|
body: JSON.stringify({ code: currentCode.value })
|
||||||
|
});
|
||||||
|
createAlert('Success', 'Code redeem success.', 'far fa-check', 5000);
|
||||||
|
valid_codes.refresh();
|
||||||
|
} catch (ex: any) {
|
||||||
|
createAlert('Error', ex?.response?.statusText || 'Unexpected error. Contact support.', 'far fa-error', 5000);
|
||||||
|
} finally {
|
||||||
|
currentCode.value = '';
|
||||||
|
}
|
||||||
|
redeeming.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SettingsTemplate :entries="entries" :key="project?.name || 'NONE'">
|
||||||
|
<template #acodes>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<LyxUiInput class="w-full px-4 py-2" placeholder="Appsumo code" v-model="currentCode"></LyxUiInput>
|
||||||
|
<LyxUiButton v-if="!redeeming" :disabled="currentCode.length == 0" @click="redeemCode()" type="primary">
|
||||||
|
Redeem
|
||||||
|
</LyxUiButton>
|
||||||
|
<div v-if="redeeming">
|
||||||
|
Redeeming...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-lyx-text-darker mt-1 text-[.9rem] poppins">
|
||||||
|
Redeemed codes: {{ valid_codes.data.value?.count || '0' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SettingsTemplate>
|
||||||
|
</template>
|
||||||
154
dashboard/components/settings/Data.vue
Normal file
154
dashboard/components/settings/Data.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import DeleteDomainData from '../dialog/DeleteDomainData.vue';
|
||||||
|
import type { SettingsTemplateEntry } from './Template.vue';
|
||||||
|
|
||||||
|
const entries: SettingsTemplateEntry[] = [
|
||||||
|
{ id: 'delete_dns', title: 'Delete domain data', text: 'Delete data of a specific domain from this project' },
|
||||||
|
{ id: 'delete_data', title: 'Delete project data', text: 'Delete all data from this project' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const domains = useFetch('/api/settings/domains', {
|
||||||
|
headers: useComputedHeaders({ useSnapshotDates: false }),
|
||||||
|
transform: (e) => {
|
||||||
|
if (!e) return [];
|
||||||
|
return e.sort((a, b) => {
|
||||||
|
return a.count - b.count;
|
||||||
|
}).map(e => {
|
||||||
|
return { id: e._id, label: `${e._id} - ${e.count} visits` }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedDomain = ref<{ id: string, label: string }>();
|
||||||
|
const selectedVisits = ref<boolean>(true);
|
||||||
|
const selectedSessions = ref<boolean>(true);
|
||||||
|
const selectedEvents = ref<boolean>(true);
|
||||||
|
|
||||||
|
|
||||||
|
const domainCounts = useFetch(() => `/api/settings/domain_counts?domain=${selectedDomain.value?.id}`, {
|
||||||
|
headers: useComputedHeaders({ useSnapshotDates: false }),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const { setToken } = useAccessToken();
|
||||||
|
|
||||||
|
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
|
function openDeleteDomainDataDialog() {
|
||||||
|
modal.open(DeleteDomainData, {
|
||||||
|
preventClose: true,
|
||||||
|
deleteData: {
|
||||||
|
isAll: false,
|
||||||
|
domain: selectedDomain.value?.id as string,
|
||||||
|
visits: selectedVisits.value,
|
||||||
|
sessions: selectedSessions.value,
|
||||||
|
events: selectedEvents.value,
|
||||||
|
},
|
||||||
|
buttonType: 'primary',
|
||||||
|
message: 'This action is irreversable and will wipe all the data from the selected domain.',
|
||||||
|
onSuccess: () => {
|
||||||
|
modal.close()
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
modal.close()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteAllDomainDataDialog() {
|
||||||
|
modal.open(DeleteDomainData, {
|
||||||
|
preventClose: true,
|
||||||
|
deleteData: {
|
||||||
|
isAll: true,
|
||||||
|
domain: '',
|
||||||
|
visits: false,
|
||||||
|
sessions: false,
|
||||||
|
events: false,
|
||||||
|
},
|
||||||
|
buttonType: 'danger',
|
||||||
|
message: 'This action is irreversable and will wipe all the data from the entire project.',
|
||||||
|
onSuccess: () => {
|
||||||
|
modal.close()
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
modal.close()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const visitsLabel = computed(() => {
|
||||||
|
if (domainCounts.pending.value === true) return 'Visits loading...';
|
||||||
|
if (domainCounts.data.value?.error === true) return 'Visits (too many to compute)';
|
||||||
|
return 'Visits ' + (domainCounts.data.value?.visits ?? '');
|
||||||
|
})
|
||||||
|
|
||||||
|
const eventsLabel = computed(() => {
|
||||||
|
if (domainCounts.pending.value === true) return 'Events loading...';
|
||||||
|
if (domainCounts.data.value?.error === true) return 'Events (too many to compute)';
|
||||||
|
return 'Events ' + (domainCounts.data.value?.events ?? '');
|
||||||
|
})
|
||||||
|
|
||||||
|
const sessionsLabel = computed(() => {
|
||||||
|
if (domainCounts.pending.value === true) return 'Sessions loading...';
|
||||||
|
if (domainCounts.data.value?.error === true) return 'Sessions (too many to compute)';
|
||||||
|
return 'Sessions ' + (domainCounts.data.value?.sessions ?? '');
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SettingsTemplate :entries="entries">
|
||||||
|
<template #delete_dns>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
|
||||||
|
<!-- <div class="text-[.9rem] text-lyx-text-darker"> Select a domain </div> -->
|
||||||
|
<USelectMenu placeholder="Select a domain" :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'
|
||||||
|
}
|
||||||
|
}" :options="domains.data.value ?? []" v-model="selectedDomain"></USelectMenu>
|
||||||
|
|
||||||
|
<div v-if="selectedDomain" class="flex flex-col gap-2 mt-4">
|
||||||
|
<div class="text-[.9rem] text-lyx-text-dark"> Select data to delete </div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
|
||||||
|
|
||||||
|
<UCheckbox :ui="{ color: 'actionable-visits-color-checkbox' }" v-model="selectedVisits"
|
||||||
|
:label="visitsLabel" />
|
||||||
|
<UCheckbox :ui="{ color: 'actionable-sessions-color-checkbox' }" v-model="selectedSessions"
|
||||||
|
:label="sessionsLabel" />
|
||||||
|
<UCheckbox :ui="{ color: 'actionable-events-color-checkbox' }" v-model="selectedEvents"
|
||||||
|
:label="eventsLabel" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LyxUiButton class="mt-2" v-if="selectedVisits || selectedSessions || selectedEvents"
|
||||||
|
@click="openDeleteDomainDataDialog()" type="outline">
|
||||||
|
Delete data
|
||||||
|
</LyxUiButton>
|
||||||
|
<div class="text-lyx-text-dark">
|
||||||
|
This action will delete all data from the project creation date.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #delete_data>
|
||||||
|
<div
|
||||||
|
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-[#1e1412]">
|
||||||
|
<div class="poppins font-semibold"> This operation will reset this project to it's initial state (0
|
||||||
|
visits 0 events 0 sessions)</div>
|
||||||
|
<div @click="openDeleteAllDomainDataDialog()"
|
||||||
|
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 all data
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SettingsTemplate>
|
||||||
|
</template>
|
||||||
@@ -198,11 +198,16 @@ function copyProjectId() {
|
|||||||
<script defer data-project="${project?._id}"
|
<script defer data-project="${project?._id}"
|
||||||
src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></script>` }}
|
src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></script>` }}
|
||||||
</div>
|
</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>
|
</LyxUiCard>
|
||||||
|
<div class="flex justify-end w-full">
|
||||||
|
<LyxUiButton type="outline" class="flex lg:hidden mt-4">
|
||||||
|
Copy script
|
||||||
|
</LyxUiButton>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #pdelete>
|
<template #pdelete>
|
||||||
<div class="flex justify-end" v-if="!isGuest">
|
<div class="flex lg:justify-end" v-if="!isGuest">
|
||||||
<LyxUiButton type="danger" @click="deleteProject()">
|
<LyxUiButton type="danger" @click="deleteProject()">
|
||||||
Delete project
|
Delete project
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ const props = defineProps<SettingsTemplateProp>();
|
|||||||
|
|
||||||
|
|
||||||
<template>
|
<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 v-for="(entry, index) of props.entries" class="flex flex-col">
|
||||||
<div class="flex">
|
<div class="flex xl:flex-row flex-col gap-4 xl:gap-0">
|
||||||
<div class="flex-[2]">
|
<div class="xl:flex-[2]">
|
||||||
<div class="poppins font-medium text-lyx-text">
|
<div class="poppins font-medium text-lyx-text">
|
||||||
{{ entry.title }}
|
{{ entry.title }}
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +27,7 @@ const props = defineProps<SettingsTemplateProp>();
|
|||||||
{{ entry.text }}
|
{{ entry.text }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-[3]">
|
<div class="xl:flex-[3]">
|
||||||
<slot :name="entry.id"></slot>
|
<slot :name="entry.id"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ const { visible } = usePricingDrawer();
|
|||||||
{{ planData.premium ? 'Premium plan' : 'Basic plan' }}
|
{{ planData.premium ? 'Premium plan' : 'Basic plan' }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex lato text-[.7rem] bg-accent/25 border-accent/40 border-[1px] px-[.6rem] rounded-sm">
|
class="flex lato text-[.7rem] bg-transparent border-[#262626] border-[1px] px-[.6rem] rounded-sm">
|
||||||
{{ planData.premium ? getPremiumName(planData.premium_type) : 'FREE' }}
|
{{ planData.premium ? getPremiumName(planData.premium_type) : 'FREE' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,7 +190,7 @@ const { visible } = usePricingDrawer();
|
|||||||
</div>
|
</div>
|
||||||
<div class="my-4 w-full bg-gray-400/30 h-[1px]">
|
<div class="my-4 w-full bg-gray-400/30 h-[1px]">
|
||||||
</div>
|
</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="flex gap-2 text-text-sub text-[.9rem]">
|
||||||
<div class="poppins"> Expire date:</div>
|
<div class="poppins"> Expire date:</div>
|
||||||
<div> {{ prettyExpireDate }}</div>
|
<div> {{ prettyExpireDate }}</div>
|
||||||
|
|||||||
42
dashboard/composables/useCountryName.ts
Normal file
42
dashboard/composables/useCountryName.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const countryMap: Record<string, string> = {
|
||||||
|
RW: "Rwanda", SO: "Somalia", YE: "Yemen", IQ: "Iraq", SA: "Saudi Arabia", IR: "Iran", CY: "Cyprus", TZ: "Tanzania",
|
||||||
|
SY: "Syria", AM: "Armenia", KE: "Kenya", CD: "Congo", DJ: "Djibouti", UG: "Uganda", CF: "Central African Republic",
|
||||||
|
SC: "Seychelles", JO: "Jordan", LB: "Lebanon", KW: "Kuwait", OM: "Oman", QA: "Qatar", BH: "Bahrain", AE: "United Arab Emirates",
|
||||||
|
IL: "Israel", TR: "Türkiye", ET: "Ethiopia", ER: "Eritrea", EG: "Egypt", SD: "Sudan", GR: "Greece", BI: "Burundi",
|
||||||
|
EE: "Estonia", LV: "Latvia", AZ: "Azerbaijan", LT: "Lithuania", SJ: "Svalbard and Jan Mayen", GE: "Georgia", MD: "Moldova",
|
||||||
|
BY: "Belarus", FI: "Finland", AX: "Åland Islands", UA: "Ukraine", MK: "North Macedonia", HU: "Hungary", BG: "Bulgaria",
|
||||||
|
AL: "Albania", PL: "Poland", RO: "Romania", XK: "Kosovo", ZW: "Zimbabwe", ZM: "Zambia", KM: "Comoros", MW: "Malawi",
|
||||||
|
LS: "Lesotho", BW: "Botswana", MU: "Mauritius", SZ: "Eswatini", RE: "Réunion", ZA: "South Africa", YT: "Mayotte",
|
||||||
|
MZ: "Mozambique", MG: "Madagascar", AF: "Afghanistan", PK: "Pakistan", BD: "Bangladesh", TM: "Turkmenistan", TJ: "Tajikistan",
|
||||||
|
LK: "Sri Lanka", BT: "Bhutan", IN: "India", MV: "Maldives", IO: "British Indian Ocean Territory", NP: "Nepal", MM: "Myanmar",
|
||||||
|
UZ: "Uzbekistan", KZ: "Kazakhstan", KG: "Kyrgyzstan", TF: "French Southern Territories", HM: "Heard and McDonald Islands",
|
||||||
|
CC: "Cocos (Keeling) Islands", PW: "Palau", VN: "Vietnam", TH: "Thailand", ID: "Indonesia", LA: "Laos", TW: "Taiwan",
|
||||||
|
PH: "Philippines", MY: "Malaysia", CN: "China", HK: "Hong Kong", BN: "Brunei", MO: "Macao", KH: "Cambodia", KR: "South Korea",
|
||||||
|
JP: "Japan", KP: "North Korea", SG: "Singapore", CK: "Cook Islands", TL: "Timor-Leste", RU: "Russia", MN: "Mongolia",
|
||||||
|
AU: "Australia", CX: "Christmas Island", MH: "Marshall Islands", FM: "Federated States of Micronesia", PG: "Papua New Guinea",
|
||||||
|
SB: "Solomon Islands", TV: "Tuvalu", NR: "Nauru", VU: "Vanuatu", NC: "New Caledonia", NF: "Norfolk Island", NZ: "New Zealand",
|
||||||
|
FJ: "Fiji", LY: "Libya", CM: "Cameroon", SN: "Senegal", CG: "Congo Republic", PT: "Portugal", LR: "Liberia", CI: "Ivory Coast", GH: "Ghana",
|
||||||
|
GQ: "Equatorial Guinea", NG: "Nigeria", BF: "Burkina Faso", TG: "Togo", GW: "Guinea-Bissau", MR: "Mauritania", BJ: "Benin", GA: "Gabon",
|
||||||
|
SL: "Sierra Leone", ST: "São Tomé and Príncipe", GI: "Gibraltar", GM: "Gambia", GN: "Guinea", TD: "Chad", NE: "Niger", ML: "Mali",
|
||||||
|
EH: "Western Sahara", TN: "Tunisia", ES: "Spain", MA: "Morocco", MT: "Malta", DZ: "Algeria", FO: "Faroe Islands", DK: "Denmark",
|
||||||
|
IS: "Iceland", GB: "United Kingdom", CH: "Switzerland", SE: "Sweden", NL: "The Netherlands", AT: "Austria", BE: "Belgium",
|
||||||
|
DE: "Germany", LU: "Luxembourg", IE: "Ireland", MC: "Monaco", FR: "France", AD: "Andorra", LI: "Liechtenstein", JE: "Jersey",
|
||||||
|
IM: "Isle of Man", GG: "Guernsey", SK: "Slovakia", CZ: "Czechia", NO: "Norway", VA: "Vatican City", SM: "San Marino",
|
||||||
|
IT: "Italy", SI: "Slovenia", ME: "Montenegro", HR: "Croatia", BA: "Bosnia and Herzegovina", AO: "Angola", NA: "Namibia",
|
||||||
|
SH: "Saint Helena", BV: "Bouvet Island", BB: "Barbados", CV: "Cabo Verde", GY: "Guyana", GF: "French Guiana", SR: "Suriname",
|
||||||
|
PM: "Saint Pierre and Miquelon", GL: "Greenland", PY: "Paraguay", UY: "Uruguay", BR: "Brazil", FK: "Falkland Islands",
|
||||||
|
GS: "South Georgia and the South Sandwich Islands", JM: "Jamaica", DO: "Dominican Republic", CU: "Cuba", MQ: "Martinique",
|
||||||
|
BS: "Bahamas", BM: "Bermuda", AI: "Anguilla", TT: "Trinidad and Tobago", KN: "St Kitts and Nevis", DM: "Dominica",
|
||||||
|
AG: "Antigua and Barbuda", LC: "Saint Lucia", TC: "Turks and Caicos Islands", AW: "Aruba", VG: "British Virgin Islands",
|
||||||
|
VC: "St Vincent and Grenadines", MS: "Montserrat", MF: "Saint Martin", BL: "Saint Barthélemy", GP: "Guadeloupe",
|
||||||
|
GD: "Grenada", KY: "Cayman Islands", BZ: "Belize", SV: "El Salvador", GT: "Guatemala", HN: "Honduras", NI: "Nicaragua",
|
||||||
|
CR: "Costa Rica", VE: "Venezuela", EC: "Ecuador", CO: "Colombia", PA: "Panama", HT: "Haiti", AR: "Argentina", CL: "Chile",
|
||||||
|
BO: "Bolivia", PE: "Peru", MX: "Mexico", PF: "French Polynesia", PN: "Pitcairn Islands", KI: "Kiribati", TK: "Tokelau",
|
||||||
|
TO: "Tonga", WF: "Wallis and Futuna", WS: "Samoa", NU: "Niue", MP: "Northern Mariana Islands", GU: "Guam", PR: "Puerto Rico",
|
||||||
|
VI: "U.S. Virgin Islands", UM: "U.S. Outlying Islands", AS: "American Samoa", CA: "Canada", US: "United States",
|
||||||
|
PS: "Palestine", RS: "Serbia", AQ: "Antarctica", SX: "Sint Maarten", CW: "Curaçao", BQ: "Bonaire", SS: "South Sudan"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCountryName(iso: string) {
|
||||||
|
return countryMap[iso] as string | undefined;
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ export type CustomDialogOptions = {
|
|||||||
params?: any,
|
params?: any,
|
||||||
width?: string,
|
width?: string,
|
||||||
height?: string,
|
height?: string,
|
||||||
closable?: boolean
|
closable?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDialogEx(component: Component, options?: CustomDialogOptions) {
|
function openDialogEx(component: Component, options?: CustomDialogOptions) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const sections: Section[] = [
|
|||||||
entries: [
|
entries: [
|
||||||
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
||||||
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
||||||
{ label: 'AI Analyst', to: '/analyst', icon: 'fal fa-sparkles' },
|
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
|
||||||
{ label: 'Security', to: '/security', icon: 'fal fa-shield' },
|
{ label: 'Security', to: '/security', icon: 'fal fa-shield' },
|
||||||
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||||
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||||
|
|||||||
@@ -4,109 +4,85 @@ import type { AdminProjectsList } from '~/server/api/admin/projects';
|
|||||||
|
|
||||||
definePageMeta({ layout: 'dashboard' });
|
definePageMeta({ layout: 'dashboard' });
|
||||||
|
|
||||||
const { data: projects } = await useFetch<AdminProjectsList[]>('/api/admin/projects', signHeaders());
|
|
||||||
const { data: counts } = await useFetch('/api/admin/counts', signHeaders());
|
|
||||||
|
|
||||||
|
|
||||||
type TProjectsGrouped = {
|
const timeRange = ref<number>(9);
|
||||||
user: {
|
|
||||||
name: string,
|
function setTimeRange(n: number) {
|
||||||
email: string,
|
timeRange.value = n;
|
||||||
given_name: string,
|
|
||||||
picture: string,
|
|
||||||
created_at: Date
|
|
||||||
},
|
|
||||||
projects: {
|
|
||||||
_id: string,
|
|
||||||
premium: boolean,
|
|
||||||
premium_type: number,
|
|
||||||
created_at: Date,
|
|
||||||
project_name: string,
|
|
||||||
total_visits: number,
|
|
||||||
total_events: number,
|
|
||||||
total_sessions: number
|
|
||||||
}[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectsGrouped = computed(() => {
|
const timeRangeTimestamp = computed(()=>{
|
||||||
|
if (timeRange.value == 1) return Date.now() - 1000 * 60 * 60 * 24;
|
||||||
if (!projects.value) return [];
|
if (timeRange.value == 2) return Date.now() - 1000 * 60 * 60 * 24 * 7;
|
||||||
|
if (timeRange.value == 3) return Date.now() - 1000 * 60 * 60 * 24 * 30;
|
||||||
const result: TProjectsGrouped[] = [];
|
return 0;
|
||||||
|
})
|
||||||
for (const project of projects.value) {
|
|
||||||
|
|
||||||
if (!project.user) continue;
|
|
||||||
|
|
||||||
|
|
||||||
const target = result.find(e => e.user.email == project.user.email);
|
const { data: projectsAggregatedResponseData } = await useFetch<AdminProjectsList[]>('/api/admin/projects', signHeaders());
|
||||||
|
const { data: counts } = await useFetch(()=> `/api/admin/counts?from=${timeRangeTimestamp.value}`, signHeaders());
|
||||||
|
|
||||||
if (target) {
|
|
||||||
|
|
||||||
target.projects.push({
|
|
||||||
_id: project._id,
|
|
||||||
created_at: project.created_at,
|
|
||||||
premium_type: project.premium_type,
|
|
||||||
premium: project.premium,
|
|
||||||
project_name: project.project_name,
|
|
||||||
total_events: project.total_events,
|
|
||||||
total_visits: project.total_visits,
|
|
||||||
total_sessions: project.total_sessions
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
const item: TProjectsGrouped = {
|
|
||||||
user: project.user,
|
|
||||||
projects: [{
|
|
||||||
_id: project._id,
|
|
||||||
created_at: project.created_at,
|
|
||||||
premium: project.premium,
|
|
||||||
premium_type: project.premium_type,
|
|
||||||
project_name: project.project_name,
|
|
||||||
total_events: project.total_events,
|
|
||||||
total_visits: project.total_visits,
|
|
||||||
total_sessions: project.total_sessions
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(item);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
result.sort((sa, sb) => {
|
|
||||||
const ca = sa.projects.reduce((a, e) => a + (e.total_visits + e.total_events), 0);
|
|
||||||
const cb = sb.projects.reduce((a, e) => a + (e.total_visits + e.total_events), 0);
|
|
||||||
return cb - ca;
|
|
||||||
})
|
|
||||||
|
|
||||||
return result;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
function onHideClicked() {
|
function onHideClicked() {
|
||||||
isAdminHidden.value = true;
|
isAdminHidden.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const projectsAggregated = computed(() => {
|
||||||
|
return projectsAggregatedResponseData.value?.sort((a, b) => {
|
||||||
|
const sumVisitsA = a.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0) + (pe.counts?.events || 0), 0);
|
||||||
|
const sumVisitsB = b.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0) + (pe.counts?.events || 0), 0);
|
||||||
|
return sumVisitsB - sumVisitsA;
|
||||||
|
}).filter(e=>{
|
||||||
|
return new Date(e.created_at).getTime() >= timeRangeTimestamp.value
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
const premiumCount = computed(() => {
|
const premiumCount = computed(() => {
|
||||||
let premiums = 0;
|
let premiums = 0;
|
||||||
projects.value?.forEach(e => {
|
projectsAggregated.value?.forEach(e => {
|
||||||
if (e.premium) premiums++;
|
e.projects.forEach(p => {
|
||||||
|
if (p.premium) premiums++;
|
||||||
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
return premiums;
|
return premiums;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const activeProjects = computed(() => {
|
||||||
|
let actives = 0;
|
||||||
|
|
||||||
|
projectsAggregated.value?.forEach(e => {
|
||||||
|
e.projects.forEach(p => {
|
||||||
|
if (!p.counts) return;
|
||||||
|
if (!p.counts.updated_at) return;
|
||||||
|
const updated_at = new Date(p.counts.updated_at).getTime();
|
||||||
|
if (updated_at < Date.now() - 1000 * 60 * 60 * 24) return;
|
||||||
|
actives++;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
return actives;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const totalVisits = computed(() => {
|
const totalVisits = computed(() => {
|
||||||
return projects.value?.reduce((a, e) => a + e.total_visits, 0) || 0;
|
return projectsAggregated.value?.reduce((a, e) => {
|
||||||
|
return a + e.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0), 0);
|
||||||
|
}, 0) || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalEvents = computed(() => {
|
const totalEvents = computed(() => {
|
||||||
return projects.value?.reduce((a, e) => a + e.total_events, 0) || 0;
|
return projectsAggregated.value?.reduce((a, e) => {
|
||||||
|
return a + e.projects.reduce((pa, pe) => pa + (pe.counts?.events || 0), 0);
|
||||||
|
}, 0) || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const details = ref<any>();
|
const details = ref<any>();
|
||||||
const showDetails = ref<boolean>(false);
|
const showDetails = ref<boolean>(false);
|
||||||
async function getProjectDetails(project_id: string) {
|
async function getProjectDetails(project_id: string) {
|
||||||
@@ -118,6 +94,30 @@ async function resetCount(project_id: string) {
|
|||||||
await $fetch(`/api/admin/reset_count?project_id=${project_id}`, signHeaders());
|
await $fetch(`/api/admin/reset_count?project_id=${project_id}`, signHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function dateDiffDays(a: string) {
|
||||||
|
return (Date.now() - new Date(a).getTime()) / (1000 * 60 * 60 * 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogBg(last_logged_at?: string) {
|
||||||
|
|
||||||
|
const day = 1000 * 60 * 60 * 24;
|
||||||
|
const week = 1000 * 60 * 60 * 24 * 7;
|
||||||
|
|
||||||
|
const lastLoggedAtDate = new Date(last_logged_at || 0);
|
||||||
|
|
||||||
|
if (lastLoggedAtDate.getTime() > Date.now() - day) {
|
||||||
|
return 'bg-green-500'
|
||||||
|
} else if (lastLoggedAtDate.getTime() > Date.now() - week) {
|
||||||
|
return 'bg-yellow-500'
|
||||||
|
} else {
|
||||||
|
return 'bg-red-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@@ -138,13 +138,21 @@ async function resetCount(project_id: string) {
|
|||||||
<div @click="onHideClicked()" v-if="!isAdminHidden"
|
<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">
|
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 class="text-text-sub/90"> <i class="far fa-eye"></i> </div>
|
||||||
<div> Nascondi dalla barra </div>
|
<div> Hide from the bar </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Card class="p-2 flex gap-10 items-center justify-center">
|
||||||
|
<div :class="{ 'text-red-200': timeRange == 1 }" @click="setTimeRange(1)"> Last day </div>
|
||||||
|
<div :class="{ 'text-red-200': timeRange == 2 }" @click="setTimeRange(2)"> Last week </div>
|
||||||
|
<div :class="{ 'text-red-200': timeRange == 3 }" @click="setTimeRange(3)"> Last month </div>
|
||||||
|
<div :class="{ 'text-red-200': timeRange == 9 }" @click="setTimeRange(9)"> All </div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card class="p-4">
|
<Card class="p-4">
|
||||||
|
|
||||||
<div class="grid grid-cols-2">
|
<div class="grid grid-cols-2 gap-1">
|
||||||
<div>
|
<div>
|
||||||
Users: {{ counts?.users }}
|
Users: {{ counts?.users }}
|
||||||
</div>
|
</div>
|
||||||
@@ -154,6 +162,10 @@ async function resetCount(project_id: string) {
|
|||||||
<div>
|
<div>
|
||||||
Total visits: {{ formatNumberK(totalVisits) }}
|
Total visits: {{ formatNumberK(totalVisits) }}
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
Active: {{ activeProjects }} |
|
||||||
|
Dead: {{ (counts?.projects || 0) - activeProjects }}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Total events: {{ formatNumberK(totalEvents) }}
|
Total events: {{ formatNumberK(totalEvents) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -162,17 +174,25 @@ async function resetCount(project_id: string) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
<div v-for="item of projectsGrouped" class="bg-menu p-4 rounded-xl flex flex-col gap-2 w-full relative">
|
<div v-for="item of projectsAggregated || []"
|
||||||
|
class="bg-menu p-4 rounded-xl flex flex-col gap-2 w-full relative">
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div> {{ item.user.email }} </div>
|
<div> {{ item.email }} </div>
|
||||||
<div> {{ item.user.name }} </div>
|
<div> {{ item.name }} </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-evenly flex-col lg:flex-row gap-2 lg:gap-0">
|
<div class="flex justify-evenly flex-col lg:grid lg:grid-cols-3 gap-2 lg:gap-4">
|
||||||
|
|
||||||
<div v-for="project of item.projects"
|
<div v-for="project of item.projects"
|
||||||
class="lg:w-[30%] flex flex-col items-center bg-bg p-6 rounded-xl">
|
class="flex relative flex-col items-center bg-bg p-6 rounded-xl">
|
||||||
|
|
||||||
|
<div class="absolute left-2 top-2 flex items-center gap-2">
|
||||||
|
<div :class="getLogBg(project?.counts?.updated_at)" class="h-3 w-3 rounded-full"> </div>
|
||||||
|
<div> {{ dateDiffDays(project?.counts?.updated_at || '0').toFixed(0) }} days </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div class="font-bold"> {{ project.premium ? 'PREMIUM' : 'FREE' }} </div>
|
<div class="font-bold"> {{ project.premium ? 'PREMIUM' : 'FREE' }} </div>
|
||||||
<div class="text-text-sub/90">
|
<div class="text-text-sub/90">
|
||||||
@@ -181,14 +201,14 @@ async function resetCount(project_id: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="text-ellipsis line-clamp-1"> {{ project.project_name }} </div>
|
<div class="text-ellipsis line-clamp-1"> {{ project.name }} </div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div> Visits: </div>
|
<div> Visits: </div>
|
||||||
<div> {{ project.total_visits }} </div>
|
<div> {{ formatNumberK(project.counts?.visits || 0) }} </div>
|
||||||
<div> Events: </div>
|
<div> Events: </div>
|
||||||
<div> {{ project.total_events }} </div>
|
<div> {{ formatNumberK(project.counts?.events || 0) }} </div>
|
||||||
<div> Sessions: </div>
|
<div> Sessions: </div>
|
||||||
<div> {{ project.total_sessions }} </div>
|
<div> {{ formatNumberK(project.counts?.sessions || 0) }} </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4 items-center mt-4">
|
<div class="flex gap-4 items-center mt-4">
|
||||||
|
|||||||
@@ -148,15 +148,15 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
|||||||
|
|
||||||
<div class="flex-[5] py-8 flex h-full flex-col items-center relative overflow-y-hidden">
|
<div class="flex-[5] py-8 flex h-full flex-col items-center relative overflow-y-hidden">
|
||||||
|
|
||||||
<div class="flex flex-col items-center lg:mt-[20vh] px-8 lg:px-28"
|
<div class="flex flex-col items-center xl:mt-[20vh] px-8 xl:px-28"
|
||||||
v-if="currentChatMessages.length == 0">
|
v-if="currentChatMessages.length == 0">
|
||||||
<div class="w-[7rem] lg:w-[10rem]">
|
<div class="w-[7rem] xl:w-[10rem]">
|
||||||
<img :src="'analyst.png'" class="w-full h-full">
|
<img :src="'analyst.png'" class="w-full h-full">
|
||||||
</div>
|
</div>
|
||||||
<div class="poppins text-[1.2rem] text-center">
|
<div class="poppins text-[1.2rem] text-center">
|
||||||
Ask me anything about your data
|
Ask me anything about your data
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col lg:grid lg:grid-cols-2 gap-4 mt-6">
|
<div class="flex flex-col xl:grid xl:grid-cols-2 gap-4 mt-6">
|
||||||
<div v-for="prompt of defaultPrompts" @click="currentText = prompt"
|
<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]">
|
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 }}
|
{{ prompt }}
|
||||||
@@ -216,7 +216,7 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
|||||||
<i class="far fa-arrow-up"></i>
|
<i class="far fa-arrow-up"></i>
|
||||||
</div>
|
</div>
|
||||||
<div @click="menuOpen = !menuOpen"
|
<div @click="menuOpen = !menuOpen"
|
||||||
class="bg-lyx-widget-light lg:hidden hhover:bg-lyx-widget-light 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>
|
<i class="far fa-message"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,12 +225,12 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
|||||||
|
|
||||||
|
|
||||||
<div :class="{
|
<div :class="{
|
||||||
'absolute': menuOpen,
|
'absolute top-0 left-0 w-full': menuOpen,
|
||||||
'hidden lg:flex': !menuOpen
|
'hidden xl:flex': !menuOpen
|
||||||
}" class="flex-[2] bg-lyx-background-light p-6 flex flex-col gap-4 h-full overflow-hidden">
|
}" 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="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>
|
<i @click="menuOpen = false" class="fas fa-close cursor-pointer"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
|||||||
<div class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
|
<div class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
|
||||||
|
|
||||||
|
|
||||||
<LyxUiCard class="w-full flex justify-between items-center">
|
<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 class="flex flex-col gap-1">
|
||||||
<div>
|
<div>
|
||||||
Total events: {{ eventsData.data.value?.[0]?.count || '0' }}
|
Total events: {{ eventsData.data.value?.[0]?.count || '0' }}
|
||||||
@@ -29,7 +29,7 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<LyxUiButton type="secondary" to="https://docs.litlyx.com/custom-events">
|
<LyxUiButton type="secondary" target="_blank" to="https://docs.litlyx.com/custom-events">
|
||||||
Trigger your first event
|
Trigger your first event
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,9 +40,9 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
|||||||
<BarCardEvents :key="refreshKey"></BarCardEvents>
|
<BarCardEvents :key="refreshKey"></BarCardEvents>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-6 flex-col xl:flex-row h-full">
|
<div class="flex gap-6 flex-col xl:flex-row xl:h-full">
|
||||||
|
|
||||||
<CardTitled :key="refreshKey" class="p-4 flex-[4] w-full h-full" title="Events"
|
<CardTitled :key="refreshKey" class="p-4 xl:flex-[4] w-full h-full" title="Events"
|
||||||
sub="Events stacked bar chart.">
|
sub="Events stacked bar chart.">
|
||||||
<template #header>
|
<template #header>
|
||||||
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
|
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
|
||||||
@@ -55,7 +55,7 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
|||||||
</div>
|
</div>
|
||||||
</CardTitled>
|
</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.">
|
sub="Displays key events.">
|
||||||
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
|
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
|
||||||
</CardTitled>
|
</CardTitled>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
|
|||||||
|
|
||||||
<template>
|
<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 v-if="showDashboard">
|
<div v-if="showDashboard">
|
||||||
|
|
||||||
@@ -55,22 +55,11 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
|
|||||||
|
|
||||||
<div class="flex w-full justify-center mt-6 px-6">
|
<div class="flex w-full justify-center mt-6 px-6">
|
||||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||||
<div class="flex-1">
|
|
||||||
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
|
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex w-full justify-center mt-6 px-6">
|
|
||||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<BarCardBrowsers :key="refreshKey"></BarCardBrowsers>
|
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,6 +75,17 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full justify-center mt-6 px-6">
|
||||||
|
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||||
|
<div class="flex-1">
|
||||||
|
<BarCardBrowsers :key="refreshKey"></BarCardBrowsers>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -97,8 +97,11 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
|
|||||||
Create your first project...
|
Create your first project...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="justLogged" class="text-[2rem]">
|
<div v-if="justLogged" class="text-[2rem] w-full h-full flex items-center justify-center">
|
||||||
The page will refresh soon
|
<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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const selectLabelsEvents = [
|
|||||||
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
<div class="mt-6 px-6 hidden lg:flex gap-6 flex-col 2xl:flex-row w-full">
|
||||||
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
definePageMeta({ layout: 'none' });
|
definePageMeta({ layout: 'none' });
|
||||||
|
|
||||||
|
import { Lit } from 'litlyx-js';
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const isNoAuth = ref<boolean>(config.public.AUTH_MODE == 'NO_AUTH');
|
const isNoAuth = ref<boolean>(config.public.AUTH_MODE == 'NO_AUTH');
|
||||||
|
|
||||||
@@ -52,6 +54,8 @@ async function handleOnSuccess(response: any) {
|
|||||||
body: JSON.stringify({ code: response.code })
|
body: JSON.stringify({ code: response.code })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Lit.event('google_login_signup');
|
||||||
|
|
||||||
if (result.error) return alert('Error during login, please try again');
|
if (result.error) return alert('Error during login, please try again');
|
||||||
|
|
||||||
setToken(result.access_token);
|
setToken(result.access_token);
|
||||||
@@ -120,7 +124,7 @@ function goBackToEmailLogin() {
|
|||||||
async function signInWithCredentials() {
|
async function signInWithCredentials() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await $fetch<{error:true, message:string} | {error: false, access_token:string}>('/api/auth/login', {
|
const result = await $fetch<{ error: true, message: string } | { error: false, access_token: string }>('/api/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email: email.value, password: password.value })
|
body: JSON.stringify({ email: email.value, password: password.value })
|
||||||
@@ -159,7 +163,7 @@ async function signInWithCredentials() {
|
|||||||
|
|
||||||
<div class="flex h-full">
|
<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>
|
<div class="rotating-thing absolute top-0"></div>
|
||||||
|
|
||||||
@@ -171,9 +175,8 @@ async function signInWithCredentials() {
|
|||||||
Sign in
|
Sign in
|
||||||
</div>
|
</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
|
Track web analytics and custom events
|
||||||
<br>
|
|
||||||
with extreme simplicity in under 30 sec.
|
with extreme simplicity in under 30 sec.
|
||||||
<br>
|
<br>
|
||||||
<!-- <div class="font-bold poppins mt-4">
|
<!-- <div class="font-bold poppins mt-4">
|
||||||
@@ -221,11 +224,12 @@ async function signInWithCredentials() {
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<i class="far fa-envelope"></i>
|
<i class="far fa-envelope"></i>
|
||||||
</div>
|
</div>
|
||||||
Continue with Email
|
Sign in with Email
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<RouterLink tag="div" to="/register" class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
|
<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
|
You don't have an account ? Sign up
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
definePageMeta({ layout: 'none' });
|
definePageMeta({ layout: 'none' });
|
||||||
|
|
||||||
const activeProject = useActiveProject();
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
definePageMeta({ layout: 'none' });
|
definePageMeta({ layout: 'none' });
|
||||||
|
|
||||||
|
import { Lit } from 'litlyx-js';
|
||||||
|
|
||||||
const emailSended = ref<boolean>(false);
|
const emailSended = ref<boolean>(false);
|
||||||
|
|
||||||
@@ -29,6 +30,9 @@ async function registerAccount() {
|
|||||||
body: JSON.stringify({ email: email.value, password: password.value })
|
body: JSON.stringify({ email: email.value, password: password.value })
|
||||||
});
|
});
|
||||||
if (res.error === true) return alert(res.message);
|
if (res.error === true) return alert(res.message);
|
||||||
|
|
||||||
|
Lit.event('email_signup');
|
||||||
|
|
||||||
emailSended.value = true;
|
emailSended.value = true;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
alert('Something went wrong');
|
alert('Something went wrong');
|
||||||
@@ -45,7 +49,7 @@ async function registerAccount() {
|
|||||||
|
|
||||||
<div class="flex h-full">
|
<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>
|
<div class="rotating-thing absolute top-0"></div>
|
||||||
|
|
||||||
@@ -57,9 +61,8 @@ async function registerAccount() {
|
|||||||
Sign up
|
Sign up
|
||||||
</div>
|
</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
|
Track web analytics and custom events
|
||||||
<br>
|
|
||||||
with extreme simplicity in under 30 sec.
|
with extreme simplicity in under 30 sec.
|
||||||
<br>
|
<br>
|
||||||
<!-- <div class="font-bold poppins mt-4">
|
<!-- <div class="font-bold poppins mt-4">
|
||||||
@@ -114,7 +117,8 @@ async function registerAccount() {
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!emailSended" class="text-[.9rem] poppins mt-20 text-text-sub text-center relative z-[2]">
|
<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
|
By continuing you are accepting
|
||||||
<br>
|
<br>
|
||||||
our
|
our
|
||||||
|
|||||||
@@ -5,27 +5,36 @@ definePageMeta({ layout: 'dashboard' });
|
|||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{ label: 'General', slot: 'general' },
|
{ label: 'General', slot: 'general' },
|
||||||
|
{ label: 'Data', slot: 'data' },
|
||||||
{ label: 'Members', slot: 'members' },
|
{ label: 'Members', slot: 'members' },
|
||||||
{ label: 'Billing', slot: 'billing' },
|
{ label: 'Billing', slot: 'billing' },
|
||||||
|
{ label: 'Codes', slot: 'codes' },
|
||||||
{ label: 'Account', slot: 'account' }
|
{ label: 'Account', slot: 'account' }
|
||||||
]
|
]
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="px-10 py-8 h-dvh overflow-y-auto hide-scrollbars">
|
<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]"> Settings </div>
|
|
||||||
|
<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">
|
<CustomTab :items="items" class="mt-8">
|
||||||
<template #general>
|
<template #general>
|
||||||
<SettingsGeneral :key="refreshKey"></SettingsGeneral>
|
<SettingsGeneral :key="refreshKey"></SettingsGeneral>
|
||||||
</template>
|
</template>
|
||||||
|
<template #data>
|
||||||
|
<SettingsData :key="refreshKey"></SettingsData>
|
||||||
|
</template>
|
||||||
<template #members>
|
<template #members>
|
||||||
<SettingsMembers :key="refreshKey"></SettingsMembers>
|
<SettingsMembers :key="refreshKey"></SettingsMembers>
|
||||||
</template>
|
</template>
|
||||||
<template #billing>
|
<template #billing>
|
||||||
<SettingsBilling :key="refreshKey"></SettingsBilling>
|
<SettingsBilling :key="refreshKey"></SettingsBilling>
|
||||||
</template>
|
</template>
|
||||||
|
<template #codes>
|
||||||
|
<SettingsCodes :key="refreshKey"></SettingsCodes>
|
||||||
|
</template>
|
||||||
<template #account>
|
<template #account>
|
||||||
<SettingsAccount :key="refreshKey"></SettingsAccount>
|
<SettingsAccount :key="refreshKey"></SettingsAccount>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,9 +8,16 @@ export default defineEventHandler(async event => {
|
|||||||
if (!userData?.logged) return;
|
if (!userData?.logged) return;
|
||||||
if (!userData.user.roles.includes('ADMIN')) return;
|
if (!userData.user.roles.includes('ADMIN')) return;
|
||||||
|
|
||||||
|
const { from } = getQuery(event);
|
||||||
|
|
||||||
const projectsCount = await ProjectModel.countDocuments({});
|
const date = new Date(parseInt(from as any));
|
||||||
const usersCount = await UserModel.countDocuments({});
|
|
||||||
|
const projectsCount = await ProjectModel.countDocuments({
|
||||||
|
created_at: { $gte: date }
|
||||||
|
});
|
||||||
|
const usersCount = await UserModel.countDocuments({
|
||||||
|
created_at: { $gte: date }
|
||||||
|
});
|
||||||
|
|
||||||
return { users: usersCount, projects: projectsCount }
|
return { users: usersCount, projects: projectsCount }
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import { ProjectModel } from "@schema/ProjectSchema";
|
import { UserModel } from "@schema/UserSchema";
|
||||||
|
|
||||||
export type AdminProjectsList = {
|
export type AdminProjectsList = {
|
||||||
premium: boolean,
|
|
||||||
created_at: Date,
|
|
||||||
project_name: string,
|
|
||||||
premium_type: number,
|
|
||||||
_id: string,
|
_id: string,
|
||||||
user: {
|
name: string,
|
||||||
|
given_name: string,
|
||||||
|
created_at: string,
|
||||||
|
email: string,
|
||||||
|
projects: {
|
||||||
|
_id: string,
|
||||||
|
owner: string,
|
||||||
name: string,
|
name: string,
|
||||||
email: string,
|
premium: boolean,
|
||||||
given_name: string,
|
premium_type: number,
|
||||||
picture: string,
|
customer_id: string,
|
||||||
created_at: Date
|
subscription_id: string,
|
||||||
},
|
premium_expire_at: string,
|
||||||
total_visits: number,
|
created_at: string,
|
||||||
total_events: number,
|
__v: number,
|
||||||
total_sessions: number
|
counts: { _id: string, project_id: string, events: number, visits: number, sessions: number, updated_at?: string }
|
||||||
|
}[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
@@ -24,40 +27,53 @@ export default defineEventHandler(async event => {
|
|||||||
if (!userData?.logged) return;
|
if (!userData?.logged) return;
|
||||||
if (!userData.user.roles.includes('ADMIN')) return;
|
if (!userData.user.roles.includes('ADMIN')) return;
|
||||||
|
|
||||||
const data: AdminProjectsList[] = await ProjectModel.aggregate([
|
const data: AdminProjectsList[] = await UserModel.aggregate([
|
||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
from: "users",
|
from: "projects",
|
||||||
localField: "owner",
|
localField: "_id",
|
||||||
foreignField: "_id",
|
foreignField: "owner",
|
||||||
as: "user"
|
as: "projects"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$unwind: {
|
||||||
|
path: "$projects",
|
||||||
|
preserveNullAndEmptyArrays: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
from: "project_counts",
|
from: "project_counts",
|
||||||
localField: "_id",
|
localField: "projects._id",
|
||||||
foreignField: "project_id",
|
foreignField: "project_id",
|
||||||
as: "counts"
|
as: "projects.counts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$project: {
|
$addFields: {
|
||||||
project_name: "$name",
|
"projects.counts": {
|
||||||
premium: 1,
|
$arrayElemAt: ["$projects.counts", 0]
|
||||||
premium_type: 1,
|
}
|
||||||
created_at: 1,
|
}
|
||||||
user: {
|
},
|
||||||
$first: "$user"
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$_id",
|
||||||
|
name: {
|
||||||
|
$first: "$name"
|
||||||
},
|
},
|
||||||
total_visits: {
|
given_name: {
|
||||||
$arrayElemAt: ["$counts.visits", 0]
|
$first: "$given_name"
|
||||||
},
|
},
|
||||||
total_events: {
|
created_at: {
|
||||||
$arrayElemAt: ["$counts.events", 0]
|
$first: "$created_at"
|
||||||
},
|
},
|
||||||
total_sessions: {
|
email: {
|
||||||
$arrayElemAt: ["$counts.sessions", 0]
|
$first: "$email"
|
||||||
|
},
|
||||||
|
projects: {
|
||||||
|
$push: "$projects"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import { ApiSettingsModel, TApiSettings } from "@schema/ApiSettingsSchema";
|
import { ApiSettingsModel, TApiSettings } from "@schema/ApiSettingsSchema";
|
||||||
import { UserSettingsModel } from "@schema/UserSettings";
|
|
||||||
import { ProjectModel } from "@schema/ProjectSchema";
|
import { ProjectModel } from "@schema/ProjectSchema";
|
||||||
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|||||||
51
dashboard/server/api/pay/redeem_appsumo_code.post.ts
Normal file
51
dashboard/server/api/pay/redeem_appsumo_code.post.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { getPlanFromId } from "@data/PREMIUM";
|
||||||
|
import { PREMIUM_PLAN } from "../../../../shared/data/PREMIUM";
|
||||||
|
import { canTryAppsumoCode, checkAppsumoCode, useAppsumoCode, useTryAppsumoCode } from "~/server/services/AppsumoService";
|
||||||
|
import StripeService from '~/server/services/StripeService';
|
||||||
|
|
||||||
|
function getPlanToActivate(current_plan_id: number) {
|
||||||
|
if (current_plan_id === PREMIUM_PLAN.FREE.ID) {
|
||||||
|
return PREMIUM_PLAN.APPSUMO_INCUBATION;
|
||||||
|
}
|
||||||
|
// if (current_plan_id === PREMIUM_PLAN.INCUBATION.ID) {
|
||||||
|
// return PREMIUM_PLAN.APPSUMO_ACCELERATION;
|
||||||
|
// }
|
||||||
|
// if (current_plan_id === PREMIUM_PLAN.ACCELERATION.ID) {
|
||||||
|
// return PREMIUM_PLAN.APPSUMO_GROWTH;
|
||||||
|
// }
|
||||||
|
if (current_plan_id === PREMIUM_PLAN.APPSUMO_INCUBATION.ID) {
|
||||||
|
return PREMIUM_PLAN.APPSUMO_ACCELERATION;
|
||||||
|
}
|
||||||
|
if (current_plan_id === PREMIUM_PLAN.APPSUMO_ACCELERATION.ID) {
|
||||||
|
return PREMIUM_PLAN.APPSUMO_GROWTH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const { project, pid, user } = data;
|
||||||
|
|
||||||
|
const body = await readBody(event);
|
||||||
|
|
||||||
|
const { code } = body;
|
||||||
|
|
||||||
|
const canTry = await canTryAppsumoCode(pid);
|
||||||
|
if (!canTry) return setResponseStatus(event, 400, 'You tried too much codes. Please contact support.');
|
||||||
|
await useTryAppsumoCode(pid, code);
|
||||||
|
|
||||||
|
const valid = await checkAppsumoCode(code);
|
||||||
|
if (!valid) return setResponseStatus(event, 400, 'Code not valid');
|
||||||
|
|
||||||
|
const currentPlan = getPlanFromId(project.premium_type);
|
||||||
|
if (!currentPlan) return setResponseStatus(event, 400, 'Current plan not found');
|
||||||
|
const planToActivate = getPlanToActivate(currentPlan.ID);
|
||||||
|
if (!planToActivate) return setResponseStatus(event, 400, 'Cannot use code on current plan');
|
||||||
|
|
||||||
|
await StripeService.createSubscription(project.customer_id, planToActivate.ID);
|
||||||
|
|
||||||
|
await useAppsumoCode(pid, code);
|
||||||
|
|
||||||
|
});
|
||||||
14
dashboard/server/api/pay/valid_codes.ts
Normal file
14
dashboard/server/api/pay/valid_codes.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { AppsumoCodeTryModel } from "@schema/AppsumoCodeTrySchema";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const { pid } = data;
|
||||||
|
|
||||||
|
const tryRes = await AppsumoCodeTryModel.findOne({ project_id: pid }, { valid_codes: 1 });
|
||||||
|
if (!tryRes) return { count: 0 }
|
||||||
|
return { count: tryRes.valid_codes.length }
|
||||||
|
|
||||||
|
});
|
||||||
@@ -133,7 +133,7 @@ async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
|
|||||||
if (!price) return { error: 'Price not found' }
|
if (!price) return { error: 'Price not found' }
|
||||||
|
|
||||||
const PLAN = getPlanFromPrice(price, StripeService.testMode || false);
|
const PLAN = getPlanFromPrice(price, StripeService.testMode || false);
|
||||||
if (!PLAN) return { error: 'Plan not found' }
|
if (!PLAN) return { error: `Plan not found. Price: ${price}. TestMode: ${StripeService.testMode}` }
|
||||||
|
|
||||||
await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, currentSubscription.current_period_start, currentSubscription.current_period_end)
|
await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, currentSubscription.current_period_start, currentSubscription.current_period_end)
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ export default defineEventHandler(async event => {
|
|||||||
const userData = getRequestUser(event);
|
const userData = getRequestUser(event);
|
||||||
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
||||||
|
|
||||||
const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { max_projects: true });
|
const maxProjects = 20;
|
||||||
|
|
||||||
const maxProjects = userSettings?.max_projects || 3;
|
|
||||||
|
|
||||||
const existingUserProjects = await ProjectModel.countDocuments({ owner: userData.id });
|
const existingUserProjects = await ProjectModel.countDocuments({ owner: userData.id });
|
||||||
if (existingUserProjects >= maxProjects) return setResponseStatus(event, 400, 'Already have max number of projects');
|
if (existingUserProjects >= maxProjects) return setResponseStatus(event, 400, 'Already have max number of projects');
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import pdfkit from 'pdfkit';
|
|||||||
import { PassThrough } from 'node:stream';
|
import { PassThrough } from 'node:stream';
|
||||||
|
|
||||||
import { ProjectModel } from "@schema/ProjectSchema";
|
import { ProjectModel } from "@schema/ProjectSchema";
|
||||||
import { UserSettingsModel } from "@schema/UserSettings";
|
|
||||||
import { VisitModel } from '@schema/metrics/VisitSchema';
|
import { VisitModel } from '@schema/metrics/VisitSchema';
|
||||||
import { EventModel } from '@schema/metrics/EventSchema';
|
import { EventModel } from '@schema/metrics/EventSchema';
|
||||||
|
|
||||||
@@ -82,15 +81,13 @@ function createPdf(data: PDFGenerationData) {
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, { requireSchema: false, allowGuests: true, requireRange: false });
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
const userData = getRequestUser(event);
|
const userData = getRequestUser(event);
|
||||||
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
||||||
|
|
||||||
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
|
const project = await ProjectModel.findById(data.project_id);
|
||||||
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
|
|
||||||
|
|
||||||
const project_id = currentActiveProject.active_project_id;
|
|
||||||
|
|
||||||
const project = await ProjectModel.findById(project_id);
|
|
||||||
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
||||||
|
|
||||||
const snapshotHeader = getHeader(event, 'x-snapshot-name');
|
const snapshotHeader = getHeader(event, 'x-snapshot-name');
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { ProjectModel } from "@schema/ProjectSchema";
|
import { ProjectModel } from "@schema/ProjectSchema";
|
||||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||||
import { UserModel } from "@schema/UserSchema";
|
import { UserModel } from "@schema/UserSchema";
|
||||||
import { UserSettingsModel } from "@schema/UserSettings";
|
|
||||||
import StripeService from '~/server/services/StripeService';
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
|||||||
36
dashboard/server/api/settings/delete_all.delete.ts
Normal file
36
dashboard/server/api/settings/delete_all.delete.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
import { EventModel } from "@schema/metrics/EventSchema";
|
||||||
|
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||||
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { getRequestData } from "~/server/utils/getRequestData";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, { requireSchema: false });
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const { project_id } = data;
|
||||||
|
|
||||||
|
taskDeleteAll(project_id);
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
async function taskDeleteAll(project_id: Types.ObjectId) {
|
||||||
|
|
||||||
|
console.log('Deletation all started');
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
await VisitModel.deleteMany({ project_id });
|
||||||
|
await SessionModel.deleteMany({ project_id });
|
||||||
|
await EventModel.deleteMany({ project_id });
|
||||||
|
|
||||||
|
const s = (Date.now() - start) / 1000;
|
||||||
|
|
||||||
|
console.log(`Deletation all done in ${s.toFixed(2)} seconds`);
|
||||||
|
|
||||||
|
}
|
||||||
104
dashboard/server/api/settings/delete_domain.delete.ts
Normal file
104
dashboard/server/api/settings/delete_domain.delete.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
|
||||||
|
import { EventModel } from "@schema/metrics/EventSchema";
|
||||||
|
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||||
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { getRequestData } from "~/server/utils/getRequestData";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, { requireSchema: false });
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const { project_id } = data;
|
||||||
|
|
||||||
|
const { domain, visits, events, sessions } = await readBody(event);
|
||||||
|
|
||||||
|
taskDeleteDomain(project_id, domain, visits, events, sessions);
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
async function taskDeleteDomain(project_id: Types.ObjectId, domain: string, deleteVisits: boolean, deleteEvents: boolean, deleteSessions: boolean) {
|
||||||
|
|
||||||
|
console.log('Deletation started');
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
const data = await VisitModel.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
project_id,
|
||||||
|
website: domain
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$session",
|
||||||
|
count: { $sum: 1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "events",
|
||||||
|
let: { sessionId: "$_id" },
|
||||||
|
pipeline: [
|
||||||
|
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
|
||||||
|
{ $match: { project_id } },
|
||||||
|
{ $project: { _id: 1 } }
|
||||||
|
],
|
||||||
|
as: "events"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "sessions",
|
||||||
|
let: { sessionId: "$_id" },
|
||||||
|
pipeline: [
|
||||||
|
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
|
||||||
|
{ $match: { project_id } },
|
||||||
|
{ $project: { _id: 1 } }
|
||||||
|
],
|
||||||
|
as: "sessions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 1,
|
||||||
|
count: 1,
|
||||||
|
"events._id": 1,
|
||||||
|
"sessions._id": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]) as { _id: string, events: { _id: string }[], sessions: { _id: string }[] }[]
|
||||||
|
|
||||||
|
|
||||||
|
if (deleteSessions === true) {
|
||||||
|
const sessions = data.flatMap(e => e.sessions).map(e => e._id.toString());
|
||||||
|
const batchSize = 1000;
|
||||||
|
for (let i = 0; i < sessions.length; i += batchSize) {
|
||||||
|
const batch = sessions.slice(i, i + batchSize);
|
||||||
|
await SessionModel.deleteMany({ _id: { $in: batch } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteEvents === true) {
|
||||||
|
const events = data.flatMap(e => e.events).map(e => e._id.toString());
|
||||||
|
const batchSize = 1000;
|
||||||
|
for (let i = 0; i < events.length; i += batchSize) {
|
||||||
|
const batch = events.slice(i, i + batchSize);
|
||||||
|
await EventModel.deleteMany({ _id: { $in: batch } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteVisits === true) {
|
||||||
|
await VisitModel.deleteMany({ project_id, website: domain })
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = (Date.now() - start) / 1000;
|
||||||
|
|
||||||
|
console.log(`Deletation done in ${s.toFixed(2)} seconds`);
|
||||||
|
|
||||||
|
}
|
||||||
79
dashboard/server/api/settings/domain_counts.ts
Normal file
79
dashboard/server/api/settings/domain_counts.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
|
||||||
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
import { getRequestData } from "~/server/utils/getRequestData";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, { requireSchema: false });
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const { project_id } = data;
|
||||||
|
|
||||||
|
const { domain } = getQuery(event);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resultData = await VisitModel.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
project_id,
|
||||||
|
website: domain
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$session",
|
||||||
|
count: { $sum: 1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "events",
|
||||||
|
let: { sessionId: "$_id" },
|
||||||
|
pipeline: [
|
||||||
|
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
|
||||||
|
{ $match: { project_id } },
|
||||||
|
{ $project: { _id: 1 } }
|
||||||
|
],
|
||||||
|
as: "events"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "sessions",
|
||||||
|
let: { sessionId: "$_id" },
|
||||||
|
pipeline: [
|
||||||
|
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
|
||||||
|
{ $match: { project_id } },
|
||||||
|
{ $project: { _id: 1 } }
|
||||||
|
],
|
||||||
|
as: "sessions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 1,
|
||||||
|
count: 1,
|
||||||
|
"events._id": 1,
|
||||||
|
"sessions._id": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
], { maxTimeMS: 5000 }) as { _id: string, count: number, events: { _id: string }[], sessions: { _id: string }[] }[]
|
||||||
|
|
||||||
|
|
||||||
|
const visits = resultData.reduce((a, e) => a + e.count, 0);
|
||||||
|
|
||||||
|
const sessions = resultData.reduce((a, e) => {
|
||||||
|
const count = e.sessions.length;
|
||||||
|
return a + count;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const events = resultData.reduce((a, e) => {
|
||||||
|
const count = e.events.length;
|
||||||
|
return a + count;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return { visits, sessions, events, error: false, message: '' };
|
||||||
|
} catch (ex: any) {
|
||||||
|
return { error: true, message: ex.message.toString(), visits: -1, sessions: -1, events: -1 }
|
||||||
|
}
|
||||||
|
});
|
||||||
18
dashboard/server/api/settings/domains.ts
Normal file
18
dashboard/server/api/settings/domains.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
import { getRequestData } from "~/server/utils/getRequestData";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, { requireSchema: false });
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const { project_id } = data;
|
||||||
|
|
||||||
|
const result = await VisitModel.aggregate([
|
||||||
|
{ $match: { project_id } },
|
||||||
|
{ $group: { _id: "$website", count: { $sum: 1 } } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return result as { _id: string, count: number }[];
|
||||||
|
});
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ProjectModel } from "@schema/ProjectSchema";
|
import { ProjectModel } from "@schema/ProjectSchema";
|
||||||
import { ProjectSnapshotModel } from "@schema/ProjectSnapshot";
|
import { ProjectSnapshotModel } from "@schema/ProjectSnapshot";
|
||||||
import { UserSettingsModel } from "@schema/UserSettings";
|
|
||||||
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, { requireSchema: false, allowGuests: true, requireRange: false });
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const { name: newSnapshotName, from, to, color: snapshotColor } = body;
|
const { name: newSnapshotName, from, to, color: snapshotColor } = body;
|
||||||
@@ -19,13 +21,8 @@ export default defineEventHandler(async event => {
|
|||||||
const userData = getRequestUser(event);
|
const userData = getRequestUser(event);
|
||||||
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
||||||
|
|
||||||
const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { active_project_id: 1 });
|
|
||||||
|
|
||||||
if (!userSettings) return setResponseStatus(event, 500, 'Unkwnown error');
|
const project = await ProjectModel.findById(data.project_id);
|
||||||
|
|
||||||
const currentProjectId = userSettings.active_project_id;
|
|
||||||
|
|
||||||
const project = await ProjectModel.findById(currentProjectId);
|
|
||||||
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +31,7 @@ export default defineEventHandler(async event => {
|
|||||||
from: new Date(from),
|
from: new Date(from),
|
||||||
to: new Date(to),
|
to: new Date(to),
|
||||||
color: snapshotColor,
|
color: snapshotColor,
|
||||||
project_id: currentProjectId
|
project_id: data.project_id
|
||||||
});
|
});
|
||||||
|
|
||||||
return newSnapshot.id;
|
return newSnapshot.id;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ProjectModel } from "@schema/ProjectSchema";
|
import { ProjectModel } from "@schema/ProjectSchema";
|
||||||
import { ProjectSnapshotModel } from "@schema/ProjectSnapshot";
|
import { ProjectSnapshotModel } from "@schema/ProjectSnapshot";
|
||||||
import { UserSettingsModel } from "@schema/UserSettings";
|
|
||||||
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, requireRange: false });
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const { id: snapshotId } = body;
|
const { id: snapshotId } = body;
|
||||||
@@ -14,18 +16,11 @@ export default defineEventHandler(async event => {
|
|||||||
const userData = getRequestUser(event);
|
const userData = getRequestUser(event);
|
||||||
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
||||||
|
|
||||||
const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { active_project_id: 1 });
|
const project = await ProjectModel.findById(data.project_id);
|
||||||
|
|
||||||
if (!userSettings) return setResponseStatus(event, 500, 'Unkwnown error');
|
|
||||||
|
|
||||||
const currentProjectId = userSettings.active_project_id;
|
|
||||||
|
|
||||||
const project = await ProjectModel.findById(currentProjectId);
|
|
||||||
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
||||||
|
|
||||||
|
|
||||||
const deletation = await ProjectSnapshotModel.deleteOne({
|
const deletation = await ProjectSnapshotModel.deleteOne({
|
||||||
project_id: currentProjectId,
|
project_id: data.project_id,
|
||||||
_id: snapshotId
|
_id: snapshotId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AuthContext } from "~/server/middleware/01-authorization";
|
|||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const userData: AuthContext = getRequestUser(event) as any;
|
const userData: AuthContext = getRequestUser(event) as any;
|
||||||
if (!userData.logged) return;
|
if (!userData.logged) return;
|
||||||
const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { max_projects: 1 });
|
// const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { max_projects: 1 });
|
||||||
return userSettings?.max_projects || 3;
|
// return userSettings?.max_projects || 3;
|
||||||
|
return 20;
|
||||||
});
|
});
|
||||||
26
dashboard/server/services/AppsumoService.ts
Normal file
26
dashboard/server/services/AppsumoService.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import { AppsumoCodeModel } from '@schema/AppsumoCodeSchema';
|
||||||
|
import { AppsumoCodeTryModel } from '@schema/AppsumoCodeTrySchema';
|
||||||
|
|
||||||
|
|
||||||
|
export async function canTryAppsumoCode(project_id: string) {
|
||||||
|
const tries = await AppsumoCodeTryModel.findOne({ project_id });
|
||||||
|
if (!tries) return true;
|
||||||
|
if (tries.codes.length >= 30) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function useTryAppsumoCode(project_id: string, code: string) {
|
||||||
|
await AppsumoCodeTryModel.updateOne({ project_id }, { $push: { codes: code } }, { upsert: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkAppsumoCode(code: string) {
|
||||||
|
const target = await AppsumoCodeModel.exists({ code, used_at: { $exists: false } });
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function useAppsumoCode(project_id: string, code: string) {
|
||||||
|
await AppsumoCodeTryModel.updateOne({ project_id }, { $push: { valid_codes: code } }, { upsert: true });
|
||||||
|
await AppsumoCodeModel.updateOne({ code }, { used_at: Date.now() });
|
||||||
|
}
|
||||||
@@ -168,6 +168,23 @@ class StripeService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createSubscription(customer_id: string, planId: number) {
|
||||||
|
if (this.disabledMode) return;
|
||||||
|
if (!this.stripe) throw Error('Stripe not initialized');
|
||||||
|
|
||||||
|
const PLAN = getPlanFromId(planId);
|
||||||
|
if (!PLAN) throw Error('Plan not found');
|
||||||
|
|
||||||
|
const subscription = await this.stripe.subscriptions.create({
|
||||||
|
customer: customer_id,
|
||||||
|
items: [
|
||||||
|
{ price: this.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, quantity: 1 }
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {
|
async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {
|
||||||
if (this.disabledMode) return;
|
if (this.disabledMode) return;
|
||||||
if (!this.stripe) throw Error('Stripe not initialized');
|
if (!this.stripe) throw Error('Stripe not initialized');
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function fixMetrics(result: { data: MetricsTimeline[], from: string, to:
|
|||||||
|
|
||||||
const allKeys = !options.advanced ? [] : Array.from(new Set(result.data.map((e: any) => e[options.advancedGroupKey])).values());
|
const allKeys = !options.advanced ? [] : Array.from(new Set(result.data.map((e: any) => e[options.advancedGroupKey])).values());
|
||||||
|
|
||||||
console.log({allKeys})
|
console.log({ allKeys, allDates })
|
||||||
|
|
||||||
const fixed: any[] = allDates.map(matchDate => {
|
const fixed: any[] = allDates.map(matchDate => {
|
||||||
|
|
||||||
@@ -102,9 +102,9 @@ console.log({allKeys})
|
|||||||
if (slice == 'hour') {
|
if (slice == 'hour') {
|
||||||
return `${e._id.getHours().toString().padStart(2, '0')}:00`
|
return `${e._id.getHours().toString().padStart(2, '0')}:00`
|
||||||
} else if (slice == 'day') {
|
} else if (slice == 'day') {
|
||||||
return `${e._id.getDate().toString().padStart(2, '0')}/${e._id.getMonth().toString().padStart(2, '0')}`
|
return `${e._id.getDate().toString().padStart(2, '0')}/${(e._id.getMonth() + 1).toString().padStart(2, '0')}`
|
||||||
} else if (slice == 'month') {
|
} else if (slice == 'month') {
|
||||||
return `${e._id.getMonth().toString().padStart(2, '0')}/${e._id.getFullYear().toString()}`
|
return `${(e._id.getMonth() + 1).toString().padStart(2, '0')}/${e._id.getFullYear().toString()}`
|
||||||
} else if (slice == 'year') {
|
} else if (slice == 'year') {
|
||||||
return `${e._id.getFullYear().toString()}`
|
return `${e._id.getFullYear().toString()}`
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ export const PREMIUM_TAGS = [
|
|||||||
'SCALING',
|
'SCALING',
|
||||||
'UNICORN',
|
'UNICORN',
|
||||||
'LIFETIME_GROWTH_ONETIME',
|
'LIFETIME_GROWTH_ONETIME',
|
||||||
'GROWTH_DUMMY'
|
'GROWTH_DUMMY',
|
||||||
|
'APPSUMO_INCUBATION',
|
||||||
|
'APPSUMO_ACCELERATION',
|
||||||
|
'APPSUMO_GROWTH',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|
||||||
@@ -123,7 +126,31 @@ export const PREMIUM_PLAN: Record<PREMIUM_TAG, PREMIUM_DATA> = {
|
|||||||
PRICE: 'price_1PvgoRB2lPUiVs9VC51YBT7J',
|
PRICE: 'price_1PvgoRB2lPUiVs9VC51YBT7J',
|
||||||
PRICE_TEST: 'price_1PvgRTB2lPUiVs9V3kFSNC3G',
|
PRICE_TEST: 'price_1PvgRTB2lPUiVs9V3kFSNC3G',
|
||||||
COST: 0
|
COST: 0
|
||||||
}
|
},
|
||||||
|
APPSUMO_INCUBATION: {
|
||||||
|
ID: 6001,
|
||||||
|
COUNT_LIMIT: 50_000,
|
||||||
|
AI_MESSAGE_LIMIT: 30,
|
||||||
|
PRICE: 'price_1QIXwbB2lPUiVs9VKSsoksaU',
|
||||||
|
PRICE_TEST: '',
|
||||||
|
COST: 0
|
||||||
|
},
|
||||||
|
APPSUMO_ACCELERATION: {
|
||||||
|
ID: 6002,
|
||||||
|
COUNT_LIMIT: 150_000,
|
||||||
|
AI_MESSAGE_LIMIT: 100,
|
||||||
|
PRICE: 'price_1QIXxRB2lPUiVs9VrjaVRoOl',
|
||||||
|
PRICE_TEST: '',
|
||||||
|
COST: 0
|
||||||
|
},
|
||||||
|
APPSUMO_GROWTH: {
|
||||||
|
ID: 6003,
|
||||||
|
COUNT_LIMIT: 500_000,
|
||||||
|
AI_MESSAGE_LIMIT: 3_000,
|
||||||
|
PRICE: 'price_1QIXy8B2lPUiVs9VQBOUPAoE',
|
||||||
|
PRICE_TEST: '',
|
||||||
|
COST: 0
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
16
shared/schema/AppsumoCodeSchema.ts
Normal file
16
shared/schema/AppsumoCodeSchema.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { model, Schema, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export type TAppsumoCode = {
|
||||||
|
_id: Schema.Types.ObjectId,
|
||||||
|
code: string,
|
||||||
|
used_at: Date,
|
||||||
|
created_at?: Date,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppsumoCodeSchema = new Schema<TAppsumoCode>({
|
||||||
|
code: { type: String, index: 1 },
|
||||||
|
created_at: { type: Date, default: () => Date.now() },
|
||||||
|
used_at: { type: Date, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AppsumoCodeModel = model<TAppsumoCode>('appsumo_codes', AppsumoCodeSchema);
|
||||||
15
shared/schema/AppsumoCodeTrySchema.ts
Normal file
15
shared/schema/AppsumoCodeTrySchema.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { model, Schema, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export type TAppsumoCodeTry = {
|
||||||
|
project_id: Types.ObjectId,
|
||||||
|
codes: string[],
|
||||||
|
valid_codes: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppsumoCodeTrySchema = new Schema<TAppsumoCodeTry>({
|
||||||
|
project_id: { type: Schema.Types.ObjectId, required: true, unique: true, index: 1 },
|
||||||
|
codes: [{ type: String }],
|
||||||
|
valid_codes: [{ type: String }]
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AppsumoCodeTryModel = model<TAppsumoCodeTry>('appsumo_codes_tries', AppsumoCodeTrySchema);
|
||||||
@@ -6,7 +6,8 @@ export type TProjectCount = {
|
|||||||
events: number,
|
events: number,
|
||||||
visits: number,
|
visits: number,
|
||||||
sessions: number,
|
sessions: number,
|
||||||
lastRecheck?: Date
|
lastRecheck?: Date,
|
||||||
|
updated_at: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectCountSchema = new Schema<TProjectCount>({
|
const ProjectCountSchema = new Schema<TProjectCount>({
|
||||||
@@ -14,7 +15,8 @@ const ProjectCountSchema = new Schema<TProjectCount>({
|
|||||||
events: { type: Number, required: true, default: 0 },
|
events: { type: Number, required: true, default: 0 },
|
||||||
visits: { type: Number, required: true, default: 0 },
|
visits: { type: Number, required: true, default: 0 },
|
||||||
sessions: { type: Number, required: true, default: 0 },
|
sessions: { type: Number, required: true, default: 0 },
|
||||||
lastRecheck: { type: Date }
|
lastRecheck: { type: Date },
|
||||||
});
|
updated_at: { type: Date }
|
||||||
|
}, { timestamps: { updatedAt: 'updated_at' } });
|
||||||
|
|
||||||
export const ProjectCountModel = model<TProjectCount>('project_counts', ProjectCountSchema);
|
export const ProjectCountModel = model<TProjectCount>('project_counts', ProjectCountSchema);
|
||||||
@@ -13,7 +13,7 @@ const EventSchema = new Schema<TEvent>({
|
|||||||
project_id: { type: Types.ObjectId, index: 1 },
|
project_id: { type: Types.ObjectId, index: 1 },
|
||||||
name: { type: String, required: true, index: 1 },
|
name: { type: String, required: true, index: 1 },
|
||||||
metadata: Schema.Types.Mixed,
|
metadata: Schema.Types.Mixed,
|
||||||
session: { type: String },
|
session: { type: String, index: 1 },
|
||||||
flowHash: { type: String },
|
flowHash: { type: String },
|
||||||
created_at: { type: Date, default: () => Date.now(), index: true },
|
created_at: { type: Date, default: () => Date.now(), index: true },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export type TSession = {
|
|||||||
|
|
||||||
const SessionSchema = new Schema<TSession>({
|
const SessionSchema = new Schema<TSession>({
|
||||||
project_id: { type: Types.ObjectId, index: 1 },
|
project_id: { type: Types.ObjectId, index: 1 },
|
||||||
session: { type: String, required: true },
|
session: { type: String, required: true, index: 1 },
|
||||||
flowHash: { type: String },
|
flowHash: { type: String },
|
||||||
duration: { type: Number, required: true, default: 0 },
|
duration: { type: Number, required: true, default: 0 },
|
||||||
updated_at: { type: Date, default: () => Date.now() },
|
updated_at: { type: Date, default: () => Date.now() },
|
||||||
|
|||||||
Reference in New Issue
Block a user