mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +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
|
||||
PROCESS_EVENT
|
||||
**/node_modules/
|
||||
docker
|
||||
dev
|
||||
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
|
||||
```
|
||||
|
||||
Litlyx natively works with all JavaScript / TypeScript frameworks. You can use Litlyx in all WordPress Websites by injecting JS code using a plug-in. Litlyx also works in serverless enviroments with Cloud (or Edge) Functions.
|
||||
Litlyx natively works with all JavaScript / TypeScript frameworks. You can use Litlyx in all WordPress Websites by injecting JS code using a plug-in. Litlyx also works in serverless environments with Cloud (or Edge) Functions.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/tech.png" />
|
||||
|
||||
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>
|
||||
|
||||
|
||||
<UModals />
|
||||
|
||||
<NuxtLayout>
|
||||
<NuxtPage></NuxtPage>
|
||||
</NuxtLayout>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<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 = {
|
||||
@@ -80,7 +80,7 @@ function openExternalLink(link: string) {
|
||||
</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 items-center gap-2">
|
||||
<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>
|
||||
|
||||
<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]">
|
||||
|
||||
<img v-if="iconProvider(element._id)?.[0] == 'img'" class="h-full"
|
||||
:style="customIconStyle" :src="iconProvider(element._id)?.[1]">
|
||||
<img v-if="iconProvider(element)?.[0] == 'img'" class="h-full"
|
||||
:style="customIconStyle" :src="iconProvider(element)?.[1]">
|
||||
|
||||
<i v-else :class="iconProvider(element._id)?.[1]"></i>
|
||||
<i v-else :class="iconProvider(element)?.[1]"></i>
|
||||
</div>
|
||||
<span class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-text/70">
|
||||
{{ elementTextTransformer?.(element._id) || element._id }}
|
||||
@@ -125,15 +125,14 @@ function openExternalLink(link: string) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-text font-semibold text-[.9rem] md:text-[1rem] manrope"> {{
|
||||
formatNumberK(element.count) }} </div>
|
||||
</div>
|
||||
<div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-bold text-[1.1rem]">
|
||||
<div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-light text-[1.1rem]">
|
||||
No data yet
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 ">
|
||||
<div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 items-end grow">
|
||||
<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]">
|
||||
Show more
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
<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', {
|
||||
headers: useComputedHeaders({ limit: 10, }), lazy: true
|
||||
@@ -8,7 +37,7 @@ const browsersData = useFetch('/api/data/browsers', {
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
async function showMore() {
|
||||
dialogBarData.value=[];
|
||||
dialogBarData.value = [];
|
||||
showDialog.value = true;
|
||||
isDataLoading.value = true;
|
||||
|
||||
@@ -16,7 +45,9 @@ async function showMore() {
|
||||
headers: useComputedHeaders({ limit: 1000 }).value
|
||||
});
|
||||
|
||||
dialogBarData.value = res || [];
|
||||
dialogBarData.value = res?.map(e => {
|
||||
return { ...e, icon: iconProvider(e as any) }
|
||||
}) || [];
|
||||
|
||||
isDataLoading.value = false;
|
||||
|
||||
@@ -28,8 +59,8 @@ async function showMore() {
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" :data="browsersData.data.value || []"
|
||||
desc="The browsers most used to search your website." :dataIcons="false"
|
||||
:loading="browsersData.pending.value" label="Top Browsers" sub-label="Browsers">
|
||||
desc="The browsers most used to search your website." :dataIcons="true" :iconProvider="iconProvider"
|
||||
:loading="browsersData.pending.value" label="Browsers" sub-label="Browsers">
|
||||
</BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
<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 }[]) {
|
||||
console.log(data);
|
||||
return data.map(e => ({ ...e, _id: e._id == null ? 'unknown' : e._id }))
|
||||
@@ -34,9 +46,9 @@ async function showMore() {
|
||||
|
||||
|
||||
<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 || []"
|
||||
:dataIcons="false" desc="The devices most used to access your website." :loading="devicesData.pending.value"
|
||||
label="Top Devices" sub-label="Devices"></BarCardBase>
|
||||
:iconProvider="iconProvider" :dataIcons="true" desc="The devices most used to access your website."
|
||||
:loading="devicesData.pending.value" label="Devices" sub-label="Devices"></BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,34 +2,42 @@
|
||||
|
||||
import type { IconProvider } from '../BarCard/Base.vue';
|
||||
|
||||
function iconProvider(id: string): ReturnType<IconProvider> {
|
||||
if (id === 'self') return ['icon', 'fas fa-link'];
|
||||
function iconProvider(e: { _id: string, flag: string, count: number }): ReturnType<IconProvider> {
|
||||
if (!e.flag) return ['icon', 'far fa-question']
|
||||
return [
|
||||
'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 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();
|
||||
|
||||
async function showMore() {
|
||||
dialogBarData.value=[];
|
||||
dialogBarData.value = [];
|
||||
showDialog.value = true;
|
||||
isDataLoading.value = true;
|
||||
|
||||
const res = await $fetch('/api/data/countries', {
|
||||
headers: useComputedHeaders({limit: 1000}).value
|
||||
headers: useComputedHeaders({ limit: 1000 }).value
|
||||
});
|
||||
|
||||
dialogBarData.value = res?.map(e => {
|
||||
return { ...e, icon: iconProvider(e._id) }
|
||||
dialogBarData.value = res?.map(k => {
|
||||
return { ...k, flag: k._id, _id: getCountryName(k._id) ?? k._id }
|
||||
}).map(e => {
|
||||
return { ...e, icon: iconProvider(e) }
|
||||
}) || [];
|
||||
|
||||
isDataLoading.value = false;
|
||||
@@ -43,7 +51,7 @@ async function showMore() {
|
||||
<div class="flex flex-col gap-2">
|
||||
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()"
|
||||
:data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value"
|
||||
label="Top Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"
|
||||
label="Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"
|
||||
desc=" Lists the countries where users access your website.">
|
||||
</BarCardBase>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,6 @@ async function showMore() {
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<BarCardBase @showMore="showMore()" @dataReload="ossData.refresh()" :data="ossData.data.value || []"
|
||||
desc="The operating systems most commonly used by your website's visitors." :dataIcons="false"
|
||||
:loading="ossData.pending.value" label="Top OS" sub-label="OSs"></BarCardBase>
|
||||
:loading="ossData.pending.value" label="OS" sub-label="OSs"></BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import type { IconProvider } from './Base.vue';
|
||||
|
||||
function iconProvider(id: string): ReturnType<IconProvider> {
|
||||
if (id === 'self') return ['icon', 'fas fa-link'];
|
||||
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${id}&sz=64`]
|
||||
function iconProvider(e: { _id: string, count: number }): ReturnType<IconProvider> {
|
||||
if (e._id === 'self') return ['icon', 'fas fa-link'];
|
||||
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${e._id}&sz=64`]
|
||||
}
|
||||
|
||||
function elementTextTransformer(element: string) {
|
||||
@@ -22,18 +22,18 @@ const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
async function showMore() {
|
||||
|
||||
dialogBarData.value=[];
|
||||
|
||||
dialogBarData.value = [];
|
||||
|
||||
showDialog.value = true;
|
||||
isDataLoading.value = true;
|
||||
|
||||
const res = await $fetch('/api/data/referrers', {
|
||||
headers: useComputedHeaders({limit: 1000}).value
|
||||
headers: useComputedHeaders({ limit: 1000 }).value
|
||||
});
|
||||
|
||||
|
||||
dialogBarData.value = res?.map(e => {
|
||||
return { ...e, icon: iconProvider(e._id) }
|
||||
return { ...e, icon: iconProvider(e as any) }
|
||||
}) || [];
|
||||
|
||||
isDataLoading.value = false;
|
||||
@@ -47,7 +47,7 @@ async function showMore() {
|
||||
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
|
||||
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
|
||||
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."
|
||||
:dataIcons="true" :loading="referrersData.pending.value" label="Top Referrers" sub-label="Referrers">
|
||||
:dataIcons="true" :loading="referrersData.pending.value" label="Top Sources" sub-label="Referrers">
|
||||
</BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -56,7 +56,7 @@ const { createAlert } = useAlert()
|
||||
async function deleteSnapshot(close: () => any) {
|
||||
await $fetch("/api/snapshot/delete", {
|
||||
method: 'DELETE',
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value,
|
||||
body: JSON.stringify({
|
||||
id: snapshot.value._id.toString(),
|
||||
})
|
||||
@@ -71,11 +71,7 @@ async function generatePDF() {
|
||||
|
||||
try {
|
||||
const res = await $fetch<Blob>('/api/project/generate_pdf', {
|
||||
...signHeaders({
|
||||
'x-snapshot-name': snapshot.value.name,
|
||||
'x-from': snapshot.value.from.toISOString(),
|
||||
'x-to': snapshot.value.to.toISOString(),
|
||||
}),
|
||||
headers: useComputedHeaders({ useSnapshotDates: false, custom: { 'x-snapshot-name': snapshot.value.name } }).value,
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
@@ -144,11 +140,20 @@ const pricingDrawer = usePricingDrawer();
|
||||
<LyxUiButton to="/project_creation" v-if="projectList && (projectList.length < (maxProjects || 1))"
|
||||
type="outlined" class="w-full py-1 mt-2 text-[.8rem]">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<div><i class="fas fa-plus"></i></div>
|
||||
<div> Create new project </div>
|
||||
<div><i class="fas fa-plus text-[.7rem]"></i></div>
|
||||
<div class="poppins"> New Project </div>
|
||||
</div>
|
||||
</LyxUiButton>
|
||||
|
||||
<LyxUiButton v-if="projectList && (projectList.length >= (maxProjects || 1))" type="outlined"
|
||||
class="w-full py-1 mt-2 text-[.7rem]">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<div><i class="text-lyx-text-darker far fa-lock"></i></div>
|
||||
<div class="text-lyx-text-darker"> Projects limit reached </div>
|
||||
</div>
|
||||
</LyxUiButton>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -203,12 +208,12 @@ const pricingDrawer = usePricingDrawer();
|
||||
<div v-if="snapshot" class="flex flex-col text-[.7rem] mt-2">
|
||||
<div class="flex gap-1 items-center justify-center text-lyx-text-dark">
|
||||
<div class="poppins">
|
||||
{{ new Date(snapshot.from).toLocaleString('it-IT').split(',')[0].trim().replace(/\//g, '-')
|
||||
{{ new Date(snapshot.from).toLocaleString().split(',')[0].trim()
|
||||
}}
|
||||
</div>
|
||||
<div class="poppins"> to </div>
|
||||
<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>
|
||||
|
||||
@@ -243,7 +248,7 @@ const pricingDrawer = usePricingDrawer();
|
||||
|
||||
<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="{
|
||||
'!text-lyx-text-darker pointer-events-none': entry.disabled,
|
||||
@@ -259,7 +264,7 @@ const pricingDrawer = usePricingDrawer();
|
||||
<div class="manrope grow">
|
||||
{{ entry.label }}
|
||||
</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>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
@@ -293,7 +298,8 @@ const pricingDrawer = usePricingDrawer();
|
||||
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
||||
<i class="fab fa-dev"></i>
|
||||
</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">
|
||||
<i class="fas fa-cat"></i>
|
||||
</NuxtLink>
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'highlight.js/styles/stackoverflow-dark.css';
|
||||
import hljs from 'highlight.js';
|
||||
import CardTitled from './CardTitled.vue';
|
||||
|
||||
import { Lit } from 'litlyx-js';
|
||||
|
||||
const props = defineProps<{
|
||||
firstInteraction: boolean,
|
||||
refreshInteraction: () => any
|
||||
@@ -19,6 +21,7 @@ onMounted(() => {
|
||||
function copyProjectId() {
|
||||
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
||||
navigator.clipboard.writeText(project.value?._id?.toString() || '');
|
||||
Lit.event('no_visit_copy_id');
|
||||
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
||||
}
|
||||
|
||||
@@ -36,6 +39,7 @@ function copyScript() {
|
||||
].join('')
|
||||
}
|
||||
|
||||
Lit.event('no_visit_copy_script');
|
||||
navigator.clipboard.writeText(createScriptText());
|
||||
createAlert('Success', 'Script copied successfully.', 'far fa-circle-check', 5000);
|
||||
}
|
||||
@@ -53,6 +57,7 @@ const scriptText = computed(() => {
|
||||
function reloadPage() {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -76,26 +81,32 @@ function reloadPage() {
|
||||
|
||||
<div class="flex items-center justify-center mt-10">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex gap-6">
|
||||
<div>
|
||||
<CardTitled class="h-full" title="Tutorial" sub="Coming soon. For now enjoy our launch video.">
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<iframe width="560" height="315"
|
||||
|
||||
<div class="flex gap-6 xl:flex-row flex-col">
|
||||
|
||||
<div class="h-full w-full">
|
||||
<CardTitled class="h-full w-full xl:min-w-[500px]" title="Tutorial"
|
||||
sub="Coming soon. For now enjoy our launch video.">
|
||||
|
||||
<div class="flex items-center justify-center h-full w-full">
|
||||
|
||||
<iframe class="w-full h-full min-h-[400px]"
|
||||
src="https://www.youtube.com/embed/GntyWMR7jsY?si=YGGkQwrk6-Iqmn8w" title="Litlyx"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
</CardTitled>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<CardTitled title="Quick Integration"
|
||||
sub="Start tracking web analytics in one line. (works everywhere js is supported)">
|
||||
<div class="flex flex-col items-end gap-4">
|
||||
<div class="w-full">
|
||||
<div class="w-full xl:text-[1rem] text-[.8rem]">
|
||||
<pre><code class="language-html">{{ scriptText }}</code></pre>
|
||||
</div>
|
||||
<LyxUiButton type="secondary" @click="copyScript()">
|
||||
@@ -122,7 +133,7 @@ function reloadPage() {
|
||||
<CardTitled class="w-full h-full" title="Documentation"
|
||||
sub="Learn how to use Litlyx in every tech stack">
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="flex justify-center w-full">
|
||||
<div class="justify-center w-full hidden xl:flex">
|
||||
<svg width="680" height="100" viewBox="0 0 680 100" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="path-1-inside-1_473_1361" fill="white">
|
||||
@@ -250,7 +261,9 @@ function reloadPage() {
|
||||
</defs>
|
||||
</svg>
|
||||
</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>
|
||||
</div>
|
||||
</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="poppins font-semibold"> Copy your project_id: </div>
|
||||
@@ -273,7 +286,7 @@ function reloadPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-menu p-6 rounded-xl flex flex-col gap-2 w-full lg:max-w-[40vw]">
|
||||
<div class="bg-menu p-6 rounded-xl flex flex-col gap-2 w-full xl:max-w-[40vw]">
|
||||
<div class="poppins font-semibold">
|
||||
Start logging visits in 1 click | Plug anywhere !
|
||||
</div>
|
||||
|
||||
@@ -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 class="flex flex-col grow">
|
||||
<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 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
|
||||
Plan for our first 100 users who believe in our project.
|
||||
<br>
|
||||
Redeem Code: <span class="text-white font-bold text-[1rem]">LIT25</span> at checkout to
|
||||
claim your discount.
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div>
|
||||
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
|
||||
|
||||
@@ -251,7 +251,7 @@ const legendClasses = ref<string[]>([
|
||||
</SelectButton>
|
||||
</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">
|
||||
<div class="flex items-center gap-2 px-10">
|
||||
<i class="far fa-sparkles text-yellow-400"></i>
|
||||
|
||||
@@ -21,14 +21,24 @@ const chartSlice = computed(() => {
|
||||
|
||||
|
||||
function transformResponse(input: { _id: string, count: number }[]) {
|
||||
|
||||
const data = input.map(e => e.count || 0);
|
||||
|
||||
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, chartSlice.value));
|
||||
|
||||
const pool = [...input.map(e => e.count || 0)];
|
||||
pool.pop();
|
||||
|
||||
const avg = pool.reduce((a, e) => a + e, 0) / pool.length;
|
||||
const diffPercent: number = (100 / avg * (input.at(-1)?.count || 0)) - 100;
|
||||
|
||||
const targets = input.slice(Math.floor(input.length / 4 * 3));
|
||||
const targetAvg = targets.reduce((a, e) => a + e.count, 0) / targets.length;
|
||||
|
||||
const diffPercent: number = (100 / avg * (targetAvg)) - 100;
|
||||
|
||||
const trend = Math.max(Math.min(diffPercent, 99), -99);
|
||||
|
||||
return { data, labels, trend }
|
||||
|
||||
}
|
||||
|
||||
const visitsData = useFetch('/api/timeline/visits', {
|
||||
@@ -94,7 +104,7 @@ const avgSessionDuration = computed(() => {
|
||||
<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">
|
||||
|
||||
<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) || '...')"
|
||||
:avg="formatNumberK(avgVisitDay) + '/day'" :trend="visitsData.data.value?.trend"
|
||||
:data="visitsData.data.value?.data" :labels="visitsData.data.value?.labels" color="#5655d7">
|
||||
@@ -106,7 +116,7 @@ const avgSessionDuration = computed(() => {
|
||||
</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) || '...')"
|
||||
:avg="formatNumberK(avgSessionsDay) + '/day'" :trend="sessionsData.data.value?.trend"
|
||||
: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"
|
||||
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"
|
||||
color="#f56523">
|
||||
</DashboardCountCard>
|
||||
|
||||
@@ -42,7 +42,7 @@ const { createAlert } = useAlert()
|
||||
async function confirmSnapshot() {
|
||||
await $fetch("/api/snapshot/create", {
|
||||
method: 'POST',
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value,
|
||||
body: JSON.stringify({
|
||||
name: snapshotName.value,
|
||||
color: currentColor.value,
|
||||
|
||||
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`, {
|
||||
headers: useComputedHeaders(), lazy: true, immediate: false
|
||||
headers: useComputedHeaders(), lazy: true
|
||||
});
|
||||
|
||||
const enabledEvents = ref<string[]>([]);
|
||||
@@ -140,7 +140,7 @@ async function onEventCheck(eventName: string) {
|
||||
<template>
|
||||
<CardTitled title="Funnel"
|
||||
sub="Monitor and analyze the actions your users are performing on your platform to gain insights into their behavior and optimize the user experience">
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div class="flex gap-2 justify-between lg:flex-row flex-col">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="min-w-[20rem] text-lyx-text-darker">
|
||||
Select two or more events
|
||||
|
||||
@@ -104,7 +104,7 @@ const canSearch = computed(() => {
|
||||
</USelectMenu>
|
||||
</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]">
|
||||
Search results: {{ metadataFieldGroupedFiltered.length }}
|
||||
</div>
|
||||
@@ -119,13 +119,13 @@ const canSearch = computed(() => {
|
||||
</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"
|
||||
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.count }} </div>
|
||||
<div class="px-1"> {{ item.count }} </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,10 +14,9 @@ function transformResponse(input: { _id: string, name: string, count: number }[]
|
||||
data: input,
|
||||
from: input[0]._id,
|
||||
to: safeSnapshotDates.value.to
|
||||
}, slice.value, {
|
||||
advanced: true,
|
||||
advancedGroupKey: 'name'
|
||||
});
|
||||
},
|
||||
slice.value,
|
||||
{ advanced: true, advancedGroupKey: 'name' });
|
||||
|
||||
const parsedDatasets: any[] = [];
|
||||
|
||||
@@ -62,6 +61,7 @@ function transformResponse(input: { _id: string, name: string, count: number }[]
|
||||
datasets: parsedDatasets,
|
||||
labels: fixed.labels
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const errorData = ref<{ errored: boolean, text: string }>({
|
||||
@@ -83,7 +83,7 @@ function onResponse(e: any) {
|
||||
const eventsStackedData = useFetch(`/api/timeline/events_stacked`, {
|
||||
lazy: true, immediate: false,
|
||||
transform: transformResponse,
|
||||
headers: useComputedHeaders({slice}),
|
||||
headers: useComputedHeaders({ slice }),
|
||||
onResponseError,
|
||||
onResponse
|
||||
});
|
||||
|
||||
@@ -62,7 +62,13 @@ async function analyzeEvent() {
|
||||
</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 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 v-if="data.active"
|
||||
class="absolute right-6 top-3 poppins text-[.75rem] bg-[#222A42] outline outline-[1px] outline-[#5680F8] px-3 py-[.1rem] rounded-sm">
|
||||
class="absolute right-6 top-3 poppins text-[.75rem] bg-transparent border-[#262626] border-solid border-[1px] px-3 py-[.1rem] rounded-sm">
|
||||
Active
|
||||
</div>
|
||||
<div v-if="!data.active && data.title === 'Growth'"
|
||||
|
||||
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}"
|
||||
src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></script>` }}
|
||||
</div>
|
||||
<div><i class="far fa-copy" @click="copyScript()"></i></div>
|
||||
<div class="hidden lg:flex"><i class="far fa-copy" @click="copyScript()"></i></div>
|
||||
</LyxUiCard>
|
||||
<div class="flex justify-end w-full">
|
||||
<LyxUiButton type="outline" class="flex lg:hidden mt-4">
|
||||
Copy script
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
</template>
|
||||
<template #pdelete>
|
||||
<div class="flex justify-end" v-if="!isGuest">
|
||||
<div class="flex lg:justify-end" v-if="!isGuest">
|
||||
<LyxUiButton type="danger" @click="deleteProject()">
|
||||
Delete project
|
||||
</LyxUiButton>
|
||||
|
||||
@@ -16,10 +16,10 @@ const props = defineProps<SettingsTemplateProp>();
|
||||
|
||||
|
||||
<template>
|
||||
<div class="mt-10 px-4">
|
||||
<div class="mt-10 px-4 xl:pb-0 pb-[10rem]">
|
||||
<div v-for="(entry, index) of props.entries" class="flex flex-col">
|
||||
<div class="flex">
|
||||
<div class="flex-[2]">
|
||||
<div class="flex xl:flex-row flex-col gap-4 xl:gap-0">
|
||||
<div class="xl:flex-[2]">
|
||||
<div class="poppins font-medium text-lyx-text">
|
||||
{{ entry.title }}
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@ const props = defineProps<SettingsTemplateProp>();
|
||||
{{ entry.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-[3]">
|
||||
<div class="xl:flex-[3]">
|
||||
<slot :name="entry.id"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -164,7 +164,7 @@ const { visible } = usePricingDrawer();
|
||||
{{ planData.premium ? 'Premium plan' : 'Basic plan' }}
|
||||
</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' }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,7 +190,7 @@ const { visible } = usePricingDrawer();
|
||||
</div>
|
||||
<div class="my-4 w-full bg-gray-400/30 h-[1px]">
|
||||
</div>
|
||||
<div class="flex justify-between px-8 flex-col sm:flex-row">
|
||||
<div class="flex justify-between px-8 flex-col lg:flex-row gap-2 lg:gap-0 items-center">
|
||||
<div class="flex gap-2 text-text-sub text-[.9rem]">
|
||||
<div class="poppins"> Expire date:</div>
|
||||
<div> {{ prettyExpireDate }}</div>
|
||||
|
||||
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,
|
||||
width?: string,
|
||||
height?: string,
|
||||
closable?: boolean
|
||||
closable?: boolean,
|
||||
}
|
||||
|
||||
function openDialogEx(component: Component, options?: CustomDialogOptions) {
|
||||
|
||||
@@ -15,7 +15,7 @@ const sections: Section[] = [
|
||||
entries: [
|
||||
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
||||
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
||||
{ label: 'AI Analyst', to: '/analyst', icon: 'fal fa-sparkles' },
|
||||
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
|
||||
{ label: 'Security', to: '/security', icon: 'fal fa-shield' },
|
||||
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||
|
||||
@@ -4,109 +4,85 @@ import type { AdminProjectsList } from '~/server/api/admin/projects';
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const { data: projects } = await useFetch<AdminProjectsList[]>('/api/admin/projects', signHeaders());
|
||||
const { data: counts } = await useFetch('/api/admin/counts', signHeaders());
|
||||
|
||||
|
||||
type TProjectsGrouped = {
|
||||
user: {
|
||||
name: string,
|
||||
email: string,
|
||||
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 timeRange = ref<number>(9);
|
||||
|
||||
function setTimeRange(n: number) {
|
||||
timeRange.value = n;
|
||||
}
|
||||
|
||||
const projectsGrouped = computed(() => {
|
||||
|
||||
if (!projects.value) return [];
|
||||
|
||||
const result: TProjectsGrouped[] = [];
|
||||
|
||||
for (const project of projects.value) {
|
||||
|
||||
if (!project.user) continue;
|
||||
const timeRangeTimestamp = computed(()=>{
|
||||
if (timeRange.value == 1) return Date.now() - 1000 * 60 * 60 * 24;
|
||||
if (timeRange.value == 2) return Date.now() - 1000 * 60 * 60 * 24 * 7;
|
||||
if (timeRange.value == 3) return Date.now() - 1000 * 60 * 60 * 24 * 30;
|
||||
return 0;
|
||||
})
|
||||
|
||||
|
||||
const 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() {
|
||||
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(() => {
|
||||
let premiums = 0;
|
||||
projects.value?.forEach(e => {
|
||||
if (e.premium) premiums++;
|
||||
projectsAggregated.value?.forEach(e => {
|
||||
e.projects.forEach(p => {
|
||||
if (p.premium) premiums++;
|
||||
});
|
||||
|
||||
})
|
||||
return premiums;
|
||||
})
|
||||
|
||||
|
||||
const activeProjects = computed(() => {
|
||||
let actives = 0;
|
||||
|
||||
projectsAggregated.value?.forEach(e => {
|
||||
e.projects.forEach(p => {
|
||||
if (!p.counts) return;
|
||||
if (!p.counts.updated_at) return;
|
||||
const updated_at = new Date(p.counts.updated_at).getTime();
|
||||
if (updated_at < Date.now() - 1000 * 60 * 60 * 24) return;
|
||||
actives++;
|
||||
});
|
||||
})
|
||||
return actives;
|
||||
});
|
||||
|
||||
|
||||
|
||||
const totalVisits = computed(() => {
|
||||
return 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(() => {
|
||||
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 showDetails = ref<boolean>(false);
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -138,13 +138,21 @@ async function resetCount(project_id: string) {
|
||||
<div @click="onHideClicked()" v-if="!isAdminHidden"
|
||||
class="bg-menu hover:bg-menu/70 cursor-pointer flex gap-2 rounded-lg w-fit px-6 py-4 text-text-sub">
|
||||
<div class="text-text-sub/90"> <i class="far fa-eye"></i> </div>
|
||||
<div> Nascondi dalla barra </div>
|
||||
<div> Hide from the bar </div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<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">
|
||||
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<div>
|
||||
Users: {{ counts?.users }}
|
||||
</div>
|
||||
@@ -154,6 +162,10 @@ async function resetCount(project_id: string) {
|
||||
<div>
|
||||
Total visits: {{ formatNumberK(totalVisits) }}
|
||||
</div>
|
||||
<div>
|
||||
Active: {{ activeProjects }} |
|
||||
Dead: {{ (counts?.projects || 0) - activeProjects }}
|
||||
</div>
|
||||
<div>
|
||||
Total events: {{ formatNumberK(totalEvents) }}
|
||||
</div>
|
||||
@@ -162,17 +174,25 @@ async function resetCount(project_id: string) {
|
||||
</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-1">
|
||||
<div> {{ item.user.email }} </div>
|
||||
<div> {{ item.user.name }} </div>
|
||||
<div> {{ item.email }} </div>
|
||||
<div> {{ item.name }} </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"
|
||||
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="font-bold"> {{ project.premium ? 'PREMIUM' : 'FREE' }} </div>
|
||||
<div class="text-text-sub/90">
|
||||
@@ -181,14 +201,14 @@ async function resetCount(project_id: string) {
|
||||
</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> Visits: </div>
|
||||
<div> {{ project.total_visits }} </div>
|
||||
<div> {{ formatNumberK(project.counts?.visits || 0) }} </div>
|
||||
<div> Events: </div>
|
||||
<div> {{ project.total_events }} </div>
|
||||
<div> {{ formatNumberK(project.counts?.events || 0) }} </div>
|
||||
<div> Sessions: </div>
|
||||
<div> {{ project.total_sessions }} </div>
|
||||
<div> {{ formatNumberK(project.counts?.sessions || 0) }} </div>
|
||||
</div>
|
||||
|
||||
<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 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">
|
||||
<div class="w-[7rem] lg:w-[10rem]">
|
||||
<div class="w-[7rem] xl:w-[10rem]">
|
||||
<img :src="'analyst.png'" class="w-full h-full">
|
||||
</div>
|
||||
<div class="poppins text-[1.2rem] text-center">
|
||||
Ask me anything about your data
|
||||
</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"
|
||||
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 }}
|
||||
@@ -216,7 +216,7 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
||||
<i class="far fa-arrow-up"></i>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,12 +225,12 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
||||
|
||||
|
||||
<div :class="{
|
||||
'absolute': menuOpen,
|
||||
'hidden lg:flex': !menuOpen
|
||||
'absolute top-0 left-0 w-full': menuOpen,
|
||||
'hidden xl:flex': !menuOpen
|
||||
}" class="flex-[2] bg-lyx-background-light p-6 flex flex-col gap-4 h-full overflow-hidden">
|
||||
|
||||
<div class="gap-2 flex flex-col">
|
||||
<div class="lg:hidden absolute right-4 top-4 text-[1.5rem]">
|
||||
<div class="xl:hidden absolute right-6 top-2 text-[1.5rem]">
|
||||
<i @click="menuOpen = false" class="fas fa-close cursor-pointer"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
<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>
|
||||
Total events: {{ eventsData.data.value?.[0]?.count || '0' }}
|
||||
@@ -29,7 +29,7 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
||||
</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
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
@@ -40,9 +40,9 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
||||
<BarCardEvents :key="refreshKey"></BarCardEvents>
|
||||
</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.">
|
||||
<template #header>
|
||||
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
|
||||
@@ -55,7 +55,7 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
||||
</div>
|
||||
</CardTitled>
|
||||
|
||||
<CardTitled :key="refreshKey" class="p-4 flex-[2] w-full h-full" title="Top events"
|
||||
<CardTitled :key="refreshKey" class="p-4 xl:flex-[2] w-full h-full" title="Top events"
|
||||
sub="Displays key events.">
|
||||
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
|
||||
</CardTitled>
|
||||
|
||||
@@ -35,7 +35,7 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
|
||||
|
||||
<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">
|
||||
|
||||
@@ -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 gap-6 flex-col xl:flex-row">
|
||||
<div class="flex-1">
|
||||
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-center mt-6 px-6">
|
||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||
<div class="flex-1">
|
||||
<BarCardBrowsers :key="refreshKey"></BarCardBrowsers>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
||||
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,6 +75,17 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
|
||||
</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>
|
||||
|
||||
|
||||
@@ -97,8 +97,11 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
|
||||
Create your first project...
|
||||
</div>
|
||||
|
||||
<div v-if="justLogged" class="text-[2rem]">
|
||||
The page will refresh soon
|
||||
<div v-if="justLogged" class="text-[2rem] w-full h-full flex items-center justify-center">
|
||||
<div
|
||||
class="backdrop-blur-[1px] z-[20] left-0 top-0 w-full h-full flex items-center justify-center font-bold rockmann absolute">
|
||||
<i class="fas fa-spinner text-[2rem] text-[#727272] animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -89,7 +89,7 @@ const selectLabelsEvents = [
|
||||
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
definePageMeta({ layout: 'none' });
|
||||
|
||||
import { Lit } from 'litlyx-js';
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
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 })
|
||||
})
|
||||
|
||||
Lit.event('google_login_signup');
|
||||
|
||||
if (result.error) return alert('Error during login, please try again');
|
||||
|
||||
setToken(result.access_token);
|
||||
@@ -120,7 +124,7 @@ function goBackToEmailLogin() {
|
||||
async function signInWithCredentials() {
|
||||
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: email.value, password: password.value })
|
||||
@@ -159,7 +163,7 @@ async function signInWithCredentials() {
|
||||
|
||||
<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>
|
||||
|
||||
@@ -171,9 +175,8 @@ async function signInWithCredentials() {
|
||||
Sign in
|
||||
</div>
|
||||
|
||||
<div class="text-text/80 text-[1.2rem] text-center w-[70%] poppins mt-2">
|
||||
<div class="text-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2">
|
||||
Track web analytics and custom events
|
||||
<br>
|
||||
with extreme simplicity in under 30 sec.
|
||||
<br>
|
||||
<!-- <div class="font-bold poppins mt-4">
|
||||
@@ -221,11 +224,12 @@ async function signInWithCredentials() {
|
||||
<div class="flex items-center">
|
||||
<i class="far fa-envelope"></i>
|
||||
</div>
|
||||
Continue with Email
|
||||
Sign in with Email
|
||||
</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
|
||||
</RouterLink>
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
definePageMeta({ layout: 'none' });
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
definePageMeta({ layout: 'none' });
|
||||
|
||||
import { Lit } from 'litlyx-js';
|
||||
|
||||
const emailSended = ref<boolean>(false);
|
||||
|
||||
@@ -29,6 +30,9 @@ async function registerAccount() {
|
||||
body: JSON.stringify({ email: email.value, password: password.value })
|
||||
});
|
||||
if (res.error === true) return alert(res.message);
|
||||
|
||||
Lit.event('email_signup');
|
||||
|
||||
emailSended.value = true;
|
||||
} catch (ex) {
|
||||
alert('Something went wrong');
|
||||
@@ -45,7 +49,7 @@ async function registerAccount() {
|
||||
|
||||
<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>
|
||||
|
||||
@@ -57,9 +61,8 @@ async function registerAccount() {
|
||||
Sign up
|
||||
</div>
|
||||
|
||||
<div class="text-text/80 text-[1.2rem] text-center w-[70%] poppins mt-2">
|
||||
<div class="text-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2">
|
||||
Track web analytics and custom events
|
||||
<br>
|
||||
with extreme simplicity in under 30 sec.
|
||||
<br>
|
||||
<!-- <div class="font-bold poppins mt-4">
|
||||
@@ -114,7 +117,8 @@ async function registerAccount() {
|
||||
</RouterLink>
|
||||
</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
|
||||
<br>
|
||||
our
|
||||
|
||||
@@ -5,27 +5,36 @@ definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const items = [
|
||||
{ label: 'General', slot: 'general' },
|
||||
{ label: 'Data', slot: 'data' },
|
||||
{ label: 'Members', slot: 'members' },
|
||||
{ label: 'Billing', slot: 'billing' },
|
||||
{ label: 'Codes', slot: 'codes' },
|
||||
{ label: 'Account', slot: 'account' }
|
||||
]
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 h-dvh overflow-y-auto hide-scrollbars">
|
||||
<div class="poppins font-semibold text-[1.3rem]"> Settings </div>
|
||||
<div class="lg:px-10 lg:py-8 h-dvh overflow-y-auto overflow-x-hidden hide-scrollbars">
|
||||
|
||||
<div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Settings </div>
|
||||
|
||||
<CustomTab :items="items" class="mt-8">
|
||||
<template #general>
|
||||
<SettingsGeneral :key="refreshKey"></SettingsGeneral>
|
||||
</template>
|
||||
<template #data>
|
||||
<SettingsData :key="refreshKey"></SettingsData>
|
||||
</template>
|
||||
<template #members>
|
||||
<SettingsMembers :key="refreshKey"></SettingsMembers>
|
||||
</template>
|
||||
<template #billing>
|
||||
<SettingsBilling :key="refreshKey"></SettingsBilling>
|
||||
</template>
|
||||
<template #codes>
|
||||
<SettingsCodes :key="refreshKey"></SettingsCodes>
|
||||
</template>
|
||||
<template #account>
|
||||
<SettingsAccount :key="refreshKey"></SettingsAccount>
|
||||
</template>
|
||||
|
||||
@@ -8,9 +8,16 @@ export default defineEventHandler(async event => {
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
|
||||
const { from } = getQuery(event);
|
||||
|
||||
const projectsCount = await ProjectModel.countDocuments({});
|
||||
const usersCount = await UserModel.countDocuments({});
|
||||
const date = new Date(parseInt(from as any));
|
||||
|
||||
const projectsCount = await ProjectModel.countDocuments({
|
||||
created_at: { $gte: date }
|
||||
});
|
||||
const usersCount = await UserModel.countDocuments({
|
||||
created_at: { $gte: date }
|
||||
});
|
||||
|
||||
return { users: usersCount, projects: projectsCount }
|
||||
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
|
||||
export type AdminProjectsList = {
|
||||
premium: boolean,
|
||||
created_at: Date,
|
||||
project_name: string,
|
||||
premium_type: number,
|
||||
_id: string,
|
||||
user: {
|
||||
name: string,
|
||||
given_name: string,
|
||||
created_at: string,
|
||||
email: string,
|
||||
projects: {
|
||||
_id: string,
|
||||
owner: string,
|
||||
name: string,
|
||||
email: string,
|
||||
given_name: string,
|
||||
picture: string,
|
||||
created_at: Date
|
||||
},
|
||||
total_visits: number,
|
||||
total_events: number,
|
||||
total_sessions: number
|
||||
premium: boolean,
|
||||
premium_type: number,
|
||||
customer_id: string,
|
||||
subscription_id: string,
|
||||
premium_expire_at: string,
|
||||
created_at: string,
|
||||
__v: number,
|
||||
counts: { _id: string, project_id: string, events: number, visits: number, sessions: number, updated_at?: string }
|
||||
}[],
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
@@ -24,40 +27,53 @@ export default defineEventHandler(async event => {
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
|
||||
const data: AdminProjectsList[] = await ProjectModel.aggregate([
|
||||
const data: AdminProjectsList[] = await UserModel.aggregate([
|
||||
{
|
||||
$lookup: {
|
||||
from: "users",
|
||||
localField: "owner",
|
||||
foreignField: "_id",
|
||||
as: "user"
|
||||
from: "projects",
|
||||
localField: "_id",
|
||||
foreignField: "owner",
|
||||
as: "projects"
|
||||
}
|
||||
},
|
||||
{
|
||||
$unwind: {
|
||||
path: "$projects",
|
||||
preserveNullAndEmptyArrays: true
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "project_counts",
|
||||
localField: "_id",
|
||||
localField: "projects._id",
|
||||
foreignField: "project_id",
|
||||
as: "counts"
|
||||
as: "projects.counts"
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
project_name: "$name",
|
||||
premium: 1,
|
||||
premium_type: 1,
|
||||
created_at: 1,
|
||||
user: {
|
||||
$first: "$user"
|
||||
$addFields: {
|
||||
"projects.counts": {
|
||||
$arrayElemAt: ["$projects.counts", 0]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$_id",
|
||||
name: {
|
||||
$first: "$name"
|
||||
},
|
||||
total_visits: {
|
||||
$arrayElemAt: ["$counts.visits", 0]
|
||||
given_name: {
|
||||
$first: "$given_name"
|
||||
},
|
||||
total_events: {
|
||||
$arrayElemAt: ["$counts.events", 0]
|
||||
created_at: {
|
||||
$first: "$created_at"
|
||||
},
|
||||
total_sessions: {
|
||||
$arrayElemAt: ["$counts.sessions", 0]
|
||||
email: {
|
||||
$first: "$email"
|
||||
},
|
||||
projects: {
|
||||
$push: "$projects"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { ApiSettingsModel, TApiSettings } from "@schema/ApiSettingsSchema";
|
||||
import { UserSettingsModel } from "@schema/UserSettings";
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
|
||||
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' }
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -16,9 +16,7 @@ export default defineEventHandler(async event => {
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
||||
|
||||
const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { max_projects: true });
|
||||
|
||||
const maxProjects = userSettings?.max_projects || 3;
|
||||
const maxProjects = 20;
|
||||
|
||||
const existingUserProjects = await ProjectModel.countDocuments({ owner: userData.id });
|
||||
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 { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { UserSettingsModel } from "@schema/UserSettings";
|
||||
import { VisitModel } from '@schema/metrics/VisitSchema';
|
||||
import { EventModel } from '@schema/metrics/EventSchema';
|
||||
|
||||
@@ -82,15 +81,13 @@ function createPdf(data: PDFGenerationData) {
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false, allowGuests: true, requireRange: false });
|
||||
if (!data) return;
|
||||
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
||||
|
||||
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.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);
|
||||
const project = await ProjectModel.findById(data.project_id);
|
||||
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
||||
|
||||
const snapshotHeader = getHeader(event, 'x-snapshot-name');
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { UserSettingsModel } from "@schema/UserSettings";
|
||||
import StripeService from '~/server/services/StripeService';
|
||||
|
||||
|
||||
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 { ProjectSnapshotModel } from "@schema/ProjectSnapshot";
|
||||
import { UserSettingsModel } from "@schema/UserSettings";
|
||||
|
||||
|
||||
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 { name: newSnapshotName, from, to, color: snapshotColor } = body;
|
||||
@@ -19,13 +21,8 @@ export default defineEventHandler(async event => {
|
||||
const userData = getRequestUser(event);
|
||||
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 currentProjectId = userSettings.active_project_id;
|
||||
|
||||
const project = await ProjectModel.findById(currentProjectId);
|
||||
const project = await ProjectModel.findById(data.project_id);
|
||||
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
||||
|
||||
|
||||
@@ -34,7 +31,7 @@ export default defineEventHandler(async event => {
|
||||
from: new Date(from),
|
||||
to: new Date(to),
|
||||
color: snapshotColor,
|
||||
project_id: currentProjectId
|
||||
project_id: data.project_id
|
||||
});
|
||||
|
||||
return newSnapshot.id;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { ProjectSnapshotModel } from "@schema/ProjectSnapshot";
|
||||
import { UserSettingsModel } from "@schema/UserSettings";
|
||||
|
||||
|
||||
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 { id: snapshotId } = body;
|
||||
@@ -14,18 +16,11 @@ export default defineEventHandler(async event => {
|
||||
const userData = getRequestUser(event);
|
||||
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 currentProjectId = userSettings.active_project_id;
|
||||
|
||||
const project = await ProjectModel.findById(currentProjectId);
|
||||
const project = await ProjectModel.findById(data.project_id);
|
||||
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
||||
|
||||
|
||||
const deletation = await ProjectSnapshotModel.deleteOne({
|
||||
project_id: currentProjectId,
|
||||
project_id: data.project_id,
|
||||
_id: snapshotId
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AuthContext } from "~/server/middleware/01-authorization";
|
||||
export default defineEventHandler(async event => {
|
||||
const userData: AuthContext = getRequestUser(event) as any;
|
||||
if (!userData.logged) return;
|
||||
const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { max_projects: 1 });
|
||||
return userSettings?.max_projects || 3;
|
||||
// const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { max_projects: 1 });
|
||||
// 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (this.disabledMode) return;
|
||||
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());
|
||||
|
||||
console.log({allKeys})
|
||||
console.log({ allKeys, allDates })
|
||||
|
||||
const fixed: any[] = allDates.map(matchDate => {
|
||||
|
||||
@@ -102,9 +102,9 @@ console.log({allKeys})
|
||||
if (slice == 'hour') {
|
||||
return `${e._id.getHours().toString().padStart(2, '0')}:00`
|
||||
} 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') {
|
||||
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') {
|
||||
return `${e._id.getFullYear().toString()}`
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,10 @@ export const PREMIUM_TAGS = [
|
||||
'SCALING',
|
||||
'UNICORN',
|
||||
'LIFETIME_GROWTH_ONETIME',
|
||||
'GROWTH_DUMMY'
|
||||
'GROWTH_DUMMY',
|
||||
'APPSUMO_INCUBATION',
|
||||
'APPSUMO_ACCELERATION',
|
||||
'APPSUMO_GROWTH',
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -123,7 +126,31 @@ export const PREMIUM_PLAN: Record<PREMIUM_TAG, PREMIUM_DATA> = {
|
||||
PRICE: 'price_1PvgoRB2lPUiVs9VC51YBT7J',
|
||||
PRICE_TEST: 'price_1PvgRTB2lPUiVs9V3kFSNC3G',
|
||||
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 {
|
||||
|
||||
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,
|
||||
visits: number,
|
||||
sessions: number,
|
||||
lastRecheck?: Date
|
||||
lastRecheck?: Date,
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
const ProjectCountSchema = new Schema<TProjectCount>({
|
||||
@@ -14,7 +15,8 @@ const ProjectCountSchema = new Schema<TProjectCount>({
|
||||
events: { type: Number, required: true, default: 0 },
|
||||
visits: { 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);
|
||||
@@ -13,7 +13,7 @@ const EventSchema = new Schema<TEvent>({
|
||||
project_id: { type: Types.ObjectId, index: 1 },
|
||||
name: { type: String, required: true, index: 1 },
|
||||
metadata: Schema.Types.Mixed,
|
||||
session: { type: String },
|
||||
session: { type: String, index: 1 },
|
||||
flowHash: { type: String },
|
||||
created_at: { type: Date, default: () => Date.now(), index: true },
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ export type TSession = {
|
||||
|
||||
const SessionSchema = new Schema<TSession>({
|
||||
project_id: { type: Types.ObjectId, index: 1 },
|
||||
session: { type: String, required: true },
|
||||
session: { type: String, required: true, index: 1 },
|
||||
flowHash: { type: String },
|
||||
duration: { type: Number, required: true, default: 0 },
|
||||
updated_at: { type: Date, default: () => Date.now() },
|
||||
|
||||
Reference in New Issue
Block a user