Rewrite endpoints + bar cards

This commit is contained in:
Emily
2024-10-03 15:07:16 +02:00
parent 314660d8a3
commit e1953f2f9f
28 changed files with 667 additions and 434 deletions

View File

@@ -0,0 +1,168 @@
<script lang="ts" setup>
export type IconProvider = (id: string) => ['img' | 'icon', string] | undefined;
type Props = {
data: { _id: string, count: number }[],
iconProvider?: IconProvider,
elementTextTransformer?: (text: string) => string,
label: string,
subLabel: string,
desc: string,
loading?: boolean,
interactive?: boolean,
isDetailView?: boolean,
rawButton?: boolean,
hideShowMore?: boolean,
customIconStyle?: string,
showLink?: boolean
}
const props = defineProps<Props>();
const emits = defineEmits<{
(e: 'dataReload'): void,
(e: 'showDetails', id: string): void,
(e: 'showRawData'): void,
(e: 'showGeneral'): void,
(e: 'showMore'): void,
}>();
const maxData = computed(() => {
const counts = props.data.map(e => e.count);
return Math.max(...counts);
});
function reloadData() {
emits('dataReload');
}
function showDetails(id: string) {
emits('showDetails', id);
}
function openExternalLink(link: string) {
if (link === 'self') return;
return window.open('https://' + link, '_blank');
}
</script>
<template>
<LyxUiCard class="w-full h-full p-4 flex flex-col gap-8 relative">
<div class="flex justify-between mb-3">
<div class="flex flex-col gap-1">
<div class="flex gap-4 items-center">
<div class="poppins font-semibold text-[1.4rem] text-text">
{{ label }}
</div>
<div class="flex items-center">
<i @click="reloadData()"
class="hover:rotate-[50deg] transition-all duration-100 fas fa-refresh text-[1.2rem] cursor-pointer"></i>
</div>
</div>
<div class="poppins text-[1rem] text-text-sub/90">
{{ desc }}
</div>
</div>
<div v-if="rawButton" class="hidden lg:flex">
<div @click="$emit('showRawData')"
class="cursor-pointer flex gap-1 items-center justify-center font-semibold poppins rounded-lg text-[#5680f8] hover:text-[#5681f8ce]">
<div> Raw data </div>
<div class="flex items-center"> <i class="fas fa-arrow-up-right"></i> </div>
</div>
</div>
</div>
<div>
<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">
<i @click="$emit('showGeneral')"
class="fas fa-arrow-left text-[.9rem] hover:text-text cursor-pointer"></i>
</div>
<div> {{ subLabel }} </div>
</div>
<div> Count </div>
</div>
<div class="flex flex-col gap-1">
<div v-if="props.data.length > 0" class="flex justify-between items-center"
v-for="element of props.data">
<div class="flex items-center gap-2 w-10/12 relative">
<div v-if="showLink">
<i @click="openExternalLink(element._id)"
class="fas fa-link text-gray-300 hover:text-gray-400 cursor-pointer"></i>
</div>
<div class="flex gap-1 items-center" @click="showDetails(element._id)"
:class="{ 'cursor-pointer line-active': interactive }">
<div class="absolute rounded-sm w-full h-full bg-[#92abcf38]"
: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"
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]">
<i v-else :class="iconProvider(element._id)?.[1]"></i>
</div>
<span class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-text/70">
{{ elementTextTransformer?.(element._id) || element._id }}
</span>
</div>
</div>
</div>
<div class="text-text font-semibold text-[.9rem] md:text-[1rem] manrope"> {{
formatNumberK(element.count) }} </div>
</div>
<div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-bold text-[1.1rem]">
No data yet
</div>
</div>
<div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 ">
<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
</div>
</div>
</div>
<div v-if="loading"
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-accent animate-[spin_1s_linear_infinite] duration-500"></i>
</div>
</LyxUiCard>
</template>
<style scoped lang="scss">
.line-active:hover {
.absolute {
@apply bg-accent/20
}
}
.ui-font {
font-feature-settings: normal;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-variation-settings: normal;
font-weight: 600;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4
}
</style>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const browsersData = useFetch('/api/data/browsers', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = browsersData.data.value || [];
}
</script>
<template>
<div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()"
:data="browsersData.data.value || []" desc="The browsers most used to search your website."
:dataIcons="false" :loading="browsersData.pending.value" label="Top Browsers" sub-label="Browsers">
</BarCardBase>
</div>
</template>

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const devicesData = useFetch('/api/data/devices', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = devicesData.data.value || [];
}
</script>
<template>
<div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" @dataReload="devicesData.refresh()" :data="devicesData.data.value || []" :dataIcons="false"
desc="The devices most used to access your website." :loading="devicesData.pending.value" label="Top Devices"
sub-label="Devices"></BarCardBase>
</div>
</template>

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
const router = useRouter();
function goToView() {
router.push('/dashboard/events');
}
const isShowMore = ref<boolean>(false);
const eventsData = useFetch('/api/data/events', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = eventsData.data.value || [];
}
</script>
<template>
<div class="flex flex-col gap-2 h-full">
<BarCardBase @showMore="showMore()" @showRawData="goToView()"
desc="Most frequent user events triggered in this project" @dataReload="eventsData.refresh()"
:data="eventsData.data.value || []" :loading="eventsData.pending.value" label="Top Events"
sub-label="Events" :rawButton="!isLiveDemo()"></BarCardBase>
</div>
</template>

View File

@@ -0,0 +1,48 @@
<script lang="ts" setup>
import type { IconProvider } from '../BarCard/Base.vue';
function iconProvider(id: string): ReturnType<IconProvider> {
if (id === 'self') return ['icon', 'fas fa-link'];
return [
'img',
`https://raw.githubusercontent.com/hampusborgos/country-flags/main/png250px/${id.toLowerCase()}.png`
]
}
const customIconStyle = `width: 2rem; padding: 1px;`
const isShowMore = ref<boolean>(false);
const geolocationData = useFetch('/api/data/countries', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = geolocationData.data.value?.map(e => {
return { ...e, icon: iconProvider(e._id) }
}) || [];
isDataLoading.value = false;
}
</script>
<template>
<div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()" :data="geolocationData.data.value || []" :dataIcons="false"
:loading="geolocationData.pending.value" label="Top Countries" sub-label="Countries" :iconProvider="iconProvider"
:customIconStyle="customIconStyle" desc=" Lists the countries where users access your website.">
</BarCardBase>
</div>
</template>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const ossData = useFetch('/api/data/oss', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = ossData.data.value || [];
}
</script>
<template>
<div class="flex flex-col gap-2 h-full">
<BarCardBase @showMore="showMore()" @dataReload="ossData.refresh()" :data="ossData.data.value || []"
desc="The operating systems most commonly used by your website's visitors." :dataIcons="false"
:loading="ossData.pending.value" label="Top OS" sub-label="OSs"></BarCardBase>
</div>
</template>

View File

@@ -0,0 +1,44 @@
<script lang="ts" setup>
import type { IconProvider } from './Base.vue';
function iconProvider(id: string): ReturnType<IconProvider> {
if (id === 'self') return ['icon', 'fas fa-link'];
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${id}&sz=64`]
}
function elementTextTransformer(element: string) {
if (element === 'self') return 'Direct Link';
return element;
}
const isShowMore = ref<boolean>(false);
const referrersData = useFetch('/api/data/referrers', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = referrersData.data.value?.map(e => {
return { ...e, icon: iconProvider(e._id) }
}) || [];
}
</script>
<template>
<div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."
:dataIcons="true" :loading="referrersData.pending.value" label="Top Referrers" sub-label="Referrers">
</BarCardBase>
</div>
</template>

View File

@@ -0,0 +1,59 @@
<script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const currentWebsite = ref<string>("");
const websitesData = useFetch('/api/data/websites', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const pagesData = useFetch('/api/data/websites_pages', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
custom: {
'x-website-name': currentWebsite
}
}), lazy: true
});
const isPagesView = ref<boolean>(false);
const currentData = computed(() => {
return isPagesView.value ? pagesData : websitesData
})
async function showDetails(website: string) {
currentWebsite.value = website;
isPagesView.value = true;
}
async function showGeneral() {
websitesData.execute();
isPagesView.value = false;
}
const router = useRouter();
function goToView() {
router.push('/dashboard/visits');
}
</script>
<template>
<div class="flex flex-col gap-2 h-full">
<BarCardBase :hideShowMore="true" @showGeneral="showGeneral()" @showRawData="goToView()"
@dataReload="currentData.refresh()" @showDetails="showDetails" :data="currentData.data.value || []"
:loading="currentData.pending.value" :label="isPagesView ? 'Top pages' : 'Top Websites'"
:sub-label="isPagesView ? 'Page' : 'Website'"
:desc="isPagesView ? 'Most visited pages' : 'Most visited website in this project'"
:interactive="!isPagesView" :rawButton="!isLiveDemo()" :isDetailView="isPagesView">
</BarCardBase>
</div>
</template>