mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
adjust dashboard
This commit is contained in:
@@ -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 }}
|
||||
@@ -132,7 +132,7 @@ function openExternalLink(link: string) {
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
@@ -256,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>
|
||||
|
||||
@@ -104,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">
|
||||
@@ -116,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">
|
||||
@@ -124,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>
|
||||
|
||||
@@ -9,12 +9,16 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
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',
|
||||
@@ -60,9 +64,14 @@ async function deleteData() {
|
||||
</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 @click="deleteData()" :type="buttonType"> Confirm </LyxUiButton>
|
||||
<LyxUiButton :disabled="!canDelete" @click="canDelete ? deleteData() : () => { }" :type="buttonType"> Confirm </LyxUiButton>
|
||||
</div>
|
||||
|
||||
<div v-if="isDone" class="flex justify-end w-full">
|
||||
|
||||
Reference in New Issue
Block a user