adjust dashboard

This commit is contained in:
Emily
2024-11-13 15:44:20 +01:00
parent 2929b229c4
commit 9de299d841
19 changed files with 254 additions and 65 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
steps steps
PROCESS_EVENT PROCESS_EVENT
**/node_modules/
docker docker
dev dev
docker-compose.admin.yml docker-compose.admin.yml

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
export type IconProvider = (id: string) => ['img' | 'icon', string] | undefined; export type IconProvider = (e: { _id: string, count: string } & any) => ['img' | 'icon', string] | undefined;
type Props = { type Props = {
@@ -80,7 +80,7 @@ function openExternalLink(link: string) {
</div> </div>
</div> </div>
<div> <div class="h-full flex flex-col">
<div class="flex justify-between font-bold text-text-sub/80 text-[1.1rem] mb-4"> <div class="flex justify-between font-bold text-text-sub/80 text-[1.1rem] mb-4">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div v-if="isDetailView" class="flex items-center justify-center"> <div v-if="isDetailView" class="flex items-center justify-center">
@@ -111,13 +111,13 @@ function openExternalLink(link: string) {
:style="'width:' + 100 / maxData * element.count + '%;'"></div> :style="'width:' + 100 / maxData * element.count + '%;'"></div>
<div class="flex px-2 py-1 relative items-center gap-4"> <div class="flex px-2 py-1 relative items-center gap-4">
<div v-if="iconProvider && iconProvider(element._id) != undefined" <div v-if="iconProvider && iconProvider(element) != undefined"
class="flex items-center h-[1.3rem]"> class="flex items-center h-[1.3rem]">
<img v-if="iconProvider(element._id)?.[0] == 'img'" class="h-full" <img v-if="iconProvider(element)?.[0] == 'img'" class="h-full"
:style="customIconStyle" :src="iconProvider(element._id)?.[1]"> :style="customIconStyle" :src="iconProvider(element)?.[1]">
<i v-else :class="iconProvider(element._id)?.[1]"></i> <i v-else :class="iconProvider(element)?.[1]"></i>
</div> </div>
<span class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-text/70"> <span class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-text/70">
{{ elementTextTransformer?.(element._id) || element._id }} {{ elementTextTransformer?.(element._id) || element._id }}
@@ -132,7 +132,7 @@ function openExternalLink(link: string) {
No data yet No data yet
</div> </div>
</div> </div>
<div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 "> <div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 items-end grow">
<div @click="$emit('showMore')" <div @click="$emit('showMore')"
class="poppins hover:bg-black cursor-pointer w-fit px-6 py-1 rounded-lg border-[1px] border-text-sub text-[.9rem]"> class="poppins hover:bg-black cursor-pointer w-fit px-6 py-1 rounded-lg border-[1px] border-text-sub text-[.9rem]">
Show more Show more

View File

@@ -1,5 +1,34 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { IconProvider } from './Base.vue';
function iconProvider(e: { _id: string, flag: string, count: number }): ReturnType<IconProvider> {
let name = e._id.toLowerCase().replace(/ /g, '-');
if (name === 'mobile-safari') name = 'safari';
if (name === 'chrome-headless') name = 'chrome'
if (name === 'chrome-webview') name = 'chrome'
if (name === 'duckduckgo') return ['icon', 'far fa-duck']
if (name === 'avast-secure-browser') return ['icon', 'far fa-bug']
if (name === 'avg-secure-browser') return ['icon', 'far fa-bug']
if (name === 'no_browser') return ['icon', 'far fa-question']
if (name === 'gsa') return ['icon', 'far fa-question']
if (name === 'miui-browser') return ['icon', 'far fa-question']
if (name === 'vivo-browser') return ['icon', 'far fa-question']
if (name === 'whale') return ['icon', 'far fa-question']
if (name === 'twitter') return ['icon', 'fab fa-twitter']
if (name === 'linkedin') return ['icon', 'fab fa-linkedin']
if (name === 'facebook') return ['icon', 'fab fa-facebook']
return [
'img',
`https://github.com/alrra/browser-logos/blob/main/src/${name}/${name}_256x256.png?raw=true`
]
}
const browsersData = useFetch('/api/data/browsers', { const browsersData = useFetch('/api/data/browsers', {
headers: useComputedHeaders({ limit: 10, }), lazy: true headers: useComputedHeaders({ limit: 10, }), lazy: true
@@ -8,7 +37,7 @@ const browsersData = useFetch('/api/data/browsers', {
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog(); const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
async function showMore() { async function showMore() {
dialogBarData.value=[]; dialogBarData.value = [];
showDialog.value = true; showDialog.value = true;
isDataLoading.value = true; isDataLoading.value = true;
@@ -16,7 +45,9 @@ async function showMore() {
headers: useComputedHeaders({ limit: 1000 }).value headers: useComputedHeaders({ limit: 1000 }).value
}); });
dialogBarData.value = res || []; dialogBarData.value = res?.map(e => {
return { ...e, icon: iconProvider(e as any) }
}) || [];
isDataLoading.value = false; isDataLoading.value = false;
@@ -28,8 +59,8 @@ async function showMore() {
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" :data="browsersData.data.value || []" <BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" :data="browsersData.data.value || []"
desc="The browsers most used to search your website." :dataIcons="false" desc="The browsers most used to search your website." :dataIcons="true" :iconProvider="iconProvider"
:loading="browsersData.pending.value" label="Top Browsers" sub-label="Browsers"> :loading="browsersData.pending.value" label="Browsers" sub-label="Browsers">
</BarCardBase> </BarCardBase>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { IconProvider } from './Base.vue';
function iconProvider(e: { _id: string, count: number }): ReturnType<IconProvider> {
if (e._id === 'desktop') return ['icon','far fa-desktop'];
if (e._id === 'tablet') return ['icon','far fa-tablet'];
if (e._id === 'mobile') return ['icon','far fa-mobile'];
if (e._id === 'smarttv') return ['icon','far fa-tv'];
if (e._id === 'console') return ['icon','far fa-game-console-handheld'];
return ['icon', 'far fa-question']
}
function transform(data: { _id: string, count: number }[]) { function transform(data: { _id: string, count: number }[]) {
console.log(data); console.log(data);
return data.map(e => ({ ...e, _id: e._id == null ? 'unknown' : e._id })) return data.map(e => ({ ...e, _id: e._id == null ? 'unknown' : e._id }))
@@ -34,9 +46,9 @@ async function showMore() {
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2 h-full">
<BarCardBase @showMore="showMore()" @dataReload="devicesData.refresh()" :data="devicesData.data.value || []" <BarCardBase @showMore="showMore()" @dataReload="devicesData.refresh()" :data="devicesData.data.value || []"
:dataIcons="false" desc="The devices most used to access your website." :loading="devicesData.pending.value" :iconProvider="iconProvider" :dataIcons="true" desc="The devices most used to access your website."
label="Top Devices" sub-label="Devices"></BarCardBase> :loading="devicesData.pending.value" label="Devices" sub-label="Devices"></BarCardBase>
</div> </div>
</template> </template>

View File

@@ -2,34 +2,42 @@
import type { IconProvider } from '../BarCard/Base.vue'; import type { IconProvider } from '../BarCard/Base.vue';
function iconProvider(id: string): ReturnType<IconProvider> { function iconProvider(e: { _id: string, flag: string, count: number }): ReturnType<IconProvider> {
if (id === 'self') return ['icon', 'fas fa-link']; if (!e.flag) return ['icon', 'far fa-question']
return [ return [
'img', 'img',
`https://raw.githubusercontent.com/hampusborgos/country-flags/main/png250px/${id.toLowerCase()}.png` `https://raw.githubusercontent.com/hampusborgos/country-flags/main/png250px/${e.flag.toLowerCase()}.png`
] ]
} }
const customIconStyle = `width: 2rem; padding: 1px;` const customIconStyle = `width: 2rem; padding: 1px;`
const geolocationData = useFetch('/api/data/countries', { const geolocationData = useFetch('/api/data/countries', {
headers: useComputedHeaders({ limit: 10, }), lazy: true headers: useComputedHeaders({ limit: 10, }), lazy: true,
transform: (e) => {
if (!e) return e;
return e.map(k => {
return { ...k, flag: k._id, _id: getCountryName(k._id) ?? k._id }
})
}
}); });
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog(); const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
async function showMore() { async function showMore() {
dialogBarData.value=[]; dialogBarData.value = [];
showDialog.value = true; showDialog.value = true;
isDataLoading.value = true; isDataLoading.value = true;
const res = await $fetch('/api/data/countries', { const res = await $fetch('/api/data/countries', {
headers: useComputedHeaders({limit: 1000}).value headers: useComputedHeaders({ limit: 1000 }).value
}); });
dialogBarData.value = res?.map(e => { dialogBarData.value = res?.map(k => {
return { ...e, icon: iconProvider(e._id) } return { ...k, flag: k._id, _id: getCountryName(k._id) ?? k._id }
}).map(e => {
return { ...e, icon: iconProvider(e) }
}) || []; }) || [];
isDataLoading.value = false; isDataLoading.value = false;
@@ -43,7 +51,7 @@ async function showMore() {
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()" <BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()"
:data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value" :data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value"
label="Top Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle" label="Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"
desc=" Lists the countries where users access your website."> desc=" Lists the countries where users access your website.">
</BarCardBase> </BarCardBase>
</div> </div>

View File

@@ -31,6 +31,6 @@ async function showMore() {
<div class="flex flex-col gap-2 h-full"> <div class="flex flex-col gap-2 h-full">
<BarCardBase @showMore="showMore()" @dataReload="ossData.refresh()" :data="ossData.data.value || []" <BarCardBase @showMore="showMore()" @dataReload="ossData.refresh()" :data="ossData.data.value || []"
desc="The operating systems most commonly used by your website's visitors." :dataIcons="false" desc="The operating systems most commonly used by your website's visitors." :dataIcons="false"
:loading="ossData.pending.value" label="Top OS" sub-label="OSs"></BarCardBase> :loading="ossData.pending.value" label="OS" sub-label="OSs"></BarCardBase>
</div> </div>
</template> </template>

View File

@@ -2,9 +2,9 @@
import type { IconProvider } from './Base.vue'; import type { IconProvider } from './Base.vue';
function iconProvider(id: string): ReturnType<IconProvider> { function iconProvider(e: { _id: string, count: number }): ReturnType<IconProvider> {
if (id === 'self') return ['icon', 'fas fa-link']; if (e._id === 'self') return ['icon', 'fas fa-link'];
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${id}&sz=64`] return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${e._id}&sz=64`]
} }
function elementTextTransformer(element: string) { function elementTextTransformer(element: string) {
@@ -22,18 +22,18 @@ const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
async function showMore() { async function showMore() {
dialogBarData.value=[]; dialogBarData.value = [];
showDialog.value = true; showDialog.value = true;
isDataLoading.value = true; isDataLoading.value = true;
const res = await $fetch('/api/data/referrers', { const res = await $fetch('/api/data/referrers', {
headers: useComputedHeaders({limit: 1000}).value headers: useComputedHeaders({ limit: 1000 }).value
}); });
dialogBarData.value = res?.map(e => { dialogBarData.value = res?.map(e => {
return { ...e, icon: iconProvider(e._id) } return { ...e, icon: iconProvider(e as any) }
}) || []; }) || [];
isDataLoading.value = false; isDataLoading.value = false;
@@ -47,7 +47,7 @@ async function showMore() {
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer" <BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true :iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website." :data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."
:dataIcons="true" :loading="referrersData.pending.value" label="Top Referrers" sub-label="Referrers"> :dataIcons="true" :loading="referrersData.pending.value" label="Top Sources" sub-label="Referrers">
</BarCardBase> </BarCardBase>
</div> </div>
</template> </template>

View File

@@ -7,6 +7,8 @@ import 'highlight.js/styles/stackoverflow-dark.css';
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import CardTitled from './CardTitled.vue'; import CardTitled from './CardTitled.vue';
import { Lit } from 'litlyx-js';
const props = defineProps<{ const props = defineProps<{
firstInteraction: boolean, firstInteraction: boolean,
refreshInteraction: () => any refreshInteraction: () => any
@@ -19,6 +21,7 @@ onMounted(() => {
function copyProjectId() { function copyProjectId() {
if (!navigator.clipboard) alert('You can\'t copy in HTTP'); if (!navigator.clipboard) alert('You can\'t copy in HTTP');
navigator.clipboard.writeText(project.value?._id?.toString() || ''); navigator.clipboard.writeText(project.value?._id?.toString() || '');
Lit.event('no_visit_copy_id');
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000); createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
} }
@@ -36,6 +39,7 @@ function copyScript() {
].join('') ].join('')
} }
Lit.event('no_visit_copy_script');
navigator.clipboard.writeText(createScriptText()); navigator.clipboard.writeText(createScriptText());
createAlert('Success', 'Script copied successfully.', 'far fa-circle-check', 5000); createAlert('Success', 'Script copied successfully.', 'far fa-circle-check', 5000);
} }
@@ -53,6 +57,7 @@ const scriptText = computed(() => {
function reloadPage() { function reloadPage() {
location.reload(); location.reload();
} }
</script> </script>
<template> <template>
@@ -256,7 +261,9 @@ function reloadPage() {
</defs> </defs>
</svg> </svg>
</div> </div>
<LyxUiButton type="secondary" to="https://docs.litlyx.com"> Visit documentation <LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
to="https://docs.litlyx.com">
Visit documentation
</LyxUiButton> </LyxUiButton>
</div> </div>
</CardTitled> </CardTitled>

View File

@@ -104,7 +104,7 @@ const avgSessionDuration = computed(() => {
<template> <template>
<div class="gap-6 px-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 m-cards-wrap:grid-cols-4"> <div class="gap-6 px-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 m-cards-wrap:grid-cols-4">
<DashboardCountCard :ready="!visitsData.pending.value" icon="far fa-earth" text="Total page visits" <DashboardCountCard :ready="!visitsData.pending.value" icon="far fa-earth" text="Total visits"
:value="formatNumberK(visitsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')" :value="formatNumberK(visitsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
:avg="formatNumberK(avgVisitDay) + '/day'" :trend="visitsData.data.value?.trend" :avg="formatNumberK(avgVisitDay) + '/day'" :trend="visitsData.data.value?.trend"
:data="visitsData.data.value?.data" :labels="visitsData.data.value?.labels" color="#5655d7"> :data="visitsData.data.value?.data" :labels="visitsData.data.value?.labels" color="#5655d7">
@@ -116,7 +116,7 @@ const avgSessionDuration = computed(() => {
</DashboardCountCard> </DashboardCountCard>
<DashboardCountCard :ready="!sessionsData.pending.value" icon="far fa-user" text="Unique visits sessions" <DashboardCountCard :ready="!sessionsData.pending.value" icon="far fa-user" text="Unique visitors"
:value="formatNumberK(sessionsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')" :value="formatNumberK(sessionsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
:avg="formatNumberK(avgSessionsDay) + '/day'" :trend="sessionsData.data.value?.trend" :avg="formatNumberK(avgSessionsDay) + '/day'" :trend="sessionsData.data.value?.trend"
:data="sessionsData.data.value?.data" :labels="sessionsData.data.value?.labels" color="#4abde8"> :data="sessionsData.data.value?.data" :labels="sessionsData.data.value?.labels" color="#4abde8">
@@ -124,7 +124,7 @@ const avgSessionDuration = computed(() => {
<DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer" <DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer"
text="Total avg session time" :value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend" text="Visit duration" :value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
:data="sessionsDurationData.data.value?.data" :labels="sessionsDurationData.data.value?.labels" :data="sessionsDurationData.data.value?.data" :labels="sessionsDurationData.data.value?.labels"
color="#f56523"> color="#f56523">
</DashboardCountCard> </DashboardCountCard>

View File

@@ -9,12 +9,16 @@ const props = defineProps<{
}>(); }>();
const isDone = ref<boolean>(false); const isDone = ref<boolean>(false);
const canDelete = ref<boolean>(false);
async function deleteData() { async function deleteData() {
try { try {
if (props.deleteData.isAll) { if (props.deleteData.isAll) {
await $fetch('/api/settings/delete_all', {
method: 'DELETE',
headers: useComputedHeaders({ useSnapshotDates: false }).value,
})
} else { } else {
await $fetch('/api/settings/delete_domain', { await $fetch('/api/settings/delete_domain', {
method: 'DELETE', method: 'DELETE',
@@ -60,9 +64,14 @@ async function deleteData() {
</div> </div>
<div class="grow"></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"> <div v-if="!isDone" class="flex justify-end gap-2">
<LyxUiButton type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton> <LyxUiButton type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton>
<LyxUiButton @click="deleteData()" :type="buttonType"> Confirm </LyxUiButton> <LyxUiButton :disabled="!canDelete" @click="canDelete ? deleteData() : () => { }" :type="buttonType"> Confirm </LyxUiButton>
</div> </div>
<div v-if="isDone" class="flex justify-end w-full"> <div v-if="isDone" class="flex justify-end w-full">

View 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;
}

View File

@@ -15,7 +15,7 @@ const sections: Section[] = [
entries: [ entries: [
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' }, { label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' }, { label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
{ label: 'AI Analyst', to: '/analyst', icon: 'fal fa-sparkles' }, { label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
{ label: 'Security', to: '/security', icon: 'fal fa-shield' }, { label: 'Security', to: '/security', icon: 'fal fa-shield' },
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true }, // { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true }, // { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },

View File

@@ -4,8 +4,26 @@ import type { AdminProjectsList } from '~/server/api/admin/projects';
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const timeRange = ref<number>(9);
function setTimeRange(n: number) {
timeRange.value = n;
}
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 { data: projectsAggregatedResponseData } = await useFetch<AdminProjectsList[]>('/api/admin/projects', signHeaders()); const { data: projectsAggregatedResponseData } = await useFetch<AdminProjectsList[]>('/api/admin/projects', signHeaders());
const { data: counts } = await useFetch('/api/admin/counts', signHeaders()); const { data: counts } = await useFetch(()=> `/api/admin/counts?from=${timeRangeTimestamp.value}`, signHeaders());
function onHideClicked() { function onHideClicked() {
isAdminHidden.value = true; isAdminHidden.value = true;
@@ -17,6 +35,8 @@ const projectsAggregated = computed(() => {
const sumVisitsA = a.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0) + (pe.counts?.events || 0), 0); 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); const sumVisitsB = b.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0) + (pe.counts?.events || 0), 0);
return sumVisitsB - sumVisitsA; return sumVisitsB - sumVisitsA;
}).filter(e=>{
return new Date(e.created_at).getTime() >= timeRangeTimestamp.value
}); });
}) })
@@ -96,6 +116,8 @@ function getLogBg(last_logged_at?: string) {
} }
</script> </script>
@@ -120,6 +142,14 @@ function getLogBg(last_logged_at?: string) {
</div> </div>
<Card class="p-2 flex gap-10 items-center justify-center">
<div :class="{ 'text-red-200': timeRange == 1 }" @click="setTimeRange(1)"> Last day </div>
<div :class="{ 'text-red-200': timeRange == 2 }" @click="setTimeRange(2)"> Last week </div>
<div :class="{ 'text-red-200': timeRange == 3 }" @click="setTimeRange(3)"> Last month </div>
<div :class="{ 'text-red-200': timeRange == 9 }" @click="setTimeRange(9)"> All </div>
</Card>
<Card class="p-4"> <Card class="p-4">
<div class="grid grid-cols-2 gap-1"> <div class="grid grid-cols-2 gap-1">
@@ -133,7 +163,7 @@ function getLogBg(last_logged_at?: string) {
Total visits: {{ formatNumberK(totalVisits) }} Total visits: {{ formatNumberK(totalVisits) }}
</div> </div>
<div> <div>
Active: {{ activeProjects }} | Active: {{ activeProjects }} |
Dead: {{ (counts?.projects || 0) - activeProjects }} Dead: {{ (counts?.projects || 0) - activeProjects }}
</div> </div>
<div> <div>
@@ -144,10 +174,6 @@ function getLogBg(last_logged_at?: string) {
</Card> </Card>
<Card>
<!-- <USelectMenu></USelectMenu> -->
</Card>
<div v-for="item of projectsAggregated || []" <div v-for="item of projectsAggregated || []"
class="bg-menu p-4 rounded-xl flex flex-col gap-2 w-full relative"> class="bg-menu p-4 rounded-xl flex flex-col gap-2 w-full relative">
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">

View File

@@ -55,22 +55,11 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
<div class="flex w-full justify-center mt-6 px-6"> <div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col xl:flex-row"> <div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1">
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
</div>
<div class="flex-1"> <div class="flex-1">
<BarCardReferrers :key="refreshKey"></BarCardReferrers> <BarCardReferrers :key="refreshKey"></BarCardReferrers>
</div> </div>
</div>
</div>
<div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1"> <div class="flex-1">
<BarCardBrowsers :key="refreshKey"></BarCardBrowsers> <BarCardWebsites :key="refreshKey"></BarCardWebsites>
</div>
<div class="flex-1">
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
</div> </div>
</div> </div>
</div> </div>
@@ -86,6 +75,17 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
</div> </div>
</div> </div>
<div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1">
<BarCardBrowsers :key="refreshKey"></BarCardBrowsers>
</div>
<div class="flex-1">
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
</div>
</div>
</div>
</div> </div>

View File

@@ -2,6 +2,8 @@
definePageMeta({ layout: 'none' }); definePageMeta({ layout: 'none' });
import { Lit } from 'litlyx-js';
const config = useRuntimeConfig() const config = useRuntimeConfig()
const isNoAuth = ref<boolean>(config.public.AUTH_MODE == 'NO_AUTH'); const isNoAuth = ref<boolean>(config.public.AUTH_MODE == 'NO_AUTH');
@@ -52,6 +54,8 @@ async function handleOnSuccess(response: any) {
body: JSON.stringify({ code: response.code }) body: JSON.stringify({ code: response.code })
}) })
Lit.event('google_login_signup');
if (result.error) return alert('Error during login, please try again'); if (result.error) return alert('Error during login, please try again');
setToken(result.access_token); setToken(result.access_token);
@@ -120,7 +124,7 @@ function goBackToEmailLogin() {
async function signInWithCredentials() { async function signInWithCredentials() {
try { try {
const result = await $fetch<{error:true, message:string} | {error: false, access_token:string}>('/api/auth/login', { const result = await $fetch<{ error: true, message: string } | { error: false, access_token: string }>('/api/auth/login', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.value, password: password.value }) body: JSON.stringify({ email: email.value, password: password.value })
@@ -224,7 +228,8 @@ async function signInWithCredentials() {
</div> </div>
<RouterLink tag="div" to="/register" class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]"> <RouterLink tag="div" to="/register"
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
You don't have an account ? Sign up You don't have an account ? Sign up
</RouterLink> </RouterLink>

View File

@@ -2,6 +2,7 @@
definePageMeta({ layout: 'none' }); definePageMeta({ layout: 'none' });
import { Lit } from 'litlyx-js';
const emailSended = ref<boolean>(false); const emailSended = ref<boolean>(false);
@@ -29,6 +30,9 @@ async function registerAccount() {
body: JSON.stringify({ email: email.value, password: password.value }) body: JSON.stringify({ email: email.value, password: password.value })
}); });
if (res.error === true) return alert(res.message); if (res.error === true) return alert(res.message);
Lit.event('email_signup');
emailSended.value = true; emailSended.value = true;
} catch (ex) { } catch (ex) {
alert('Something went wrong'); alert('Something went wrong');
@@ -113,7 +117,8 @@ async function registerAccount() {
</RouterLink> </RouterLink>
</div> </div>
<div v-if="!emailSended" class="text-[.9rem] poppins mt-5 xl:mt-20 text-text-sub text-center relative z-[2]"> <div v-if="!emailSended"
class="text-[.9rem] poppins mt-5 xl:mt-20 text-text-sub text-center relative z-[2]">
By continuing you are accepting By continuing you are accepting
<br> <br>
our our

View File

@@ -8,9 +8,16 @@ export default defineEventHandler(async event => {
if (!userData?.logged) return; if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return; if (!userData.user.roles.includes('ADMIN')) return;
const { from } = getQuery(event);
const projectsCount = await ProjectModel.countDocuments({}); const date = new Date(parseInt(from as any));
const usersCount = await UserModel.countDocuments({});
const projectsCount = await ProjectModel.countDocuments({
created_at: { $gte: date }
});
const usersCount = await UserModel.countDocuments({
created_at: { $gte: date }
});
return { users: usersCount, projects: projectsCount } return { users: usersCount, projects: projectsCount }

View 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`);
}

View File

@@ -85,10 +85,10 @@ async function taskDeleteDomain(project_id: Types.ObjectId, domain: string, dele
} }
if (deleteEvents === true) { if (deleteEvents === true) {
const sessions = data.flatMap(e => e.sessions).map(e => e._id.toString()); const events = data.flatMap(e => e.events).map(e => e._id.toString());
const batchSize = 1000; const batchSize = 1000;
for (let i = 0; i < sessions.length; i += batchSize) { for (let i = 0; i < events.length; i += batchSize) {
const batch = sessions.slice(i, i + batchSize); const batch = events.slice(i, i + batchSize);
await EventModel.deleteMany({ _id: { $in: batch } }); await EventModel.deleteMany({ _id: { $in: batch } });
} }
} }