mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
new selfhosted version
This commit is contained in:
78
dashboard/components/complex/line-data/Browsers.vue
Normal file
78
dashboard/components/complex/line-data/Browsers.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import LineDataTemplate, { type IconProvider, type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number, sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
const NO_BROWSER_INFO_TOOLTIP_TEXT = 'Browsers -> "Others" means the visitor used a rare or unidentified browser we couldn\'t clearly classify.';
|
||||
|
||||
const { data: browsers, status, refresh } = useAuthFetch<{ _id: string, count: number }[]>('/api/data/browsers', {
|
||||
headers: { 'x-limit': '10' }, lazy: true,
|
||||
transform: (data) => {
|
||||
return data.map(e => e._id === 'NO_BROWSER' ? { ...e, info: NO_BROWSER_INFO_TOOLTIP_TEXT } : e);
|
||||
}
|
||||
});
|
||||
|
||||
function iconProvider(e: { _id: 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'
|
||||
|
||||
return [
|
||||
'img',
|
||||
`https://github.com/alrra/browser-logos/blob/main/src/${name}/${name}_256x256.png?raw=true`
|
||||
]
|
||||
}
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Browsers',
|
||||
sub: 'The browsers most used to search your website.',
|
||||
data: browsers.value ?? [],
|
||||
iconProvider,
|
||||
iconStyle: 'width: 1.3rem; height: auto;',
|
||||
elementTextTransformer(text) {
|
||||
if (text === 'NO_BROWSER') return 'Others';
|
||||
return text;
|
||||
},
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: string, count: number }[]>('/api/data/browsers', { headers: { 'x-limit': '1000' } });
|
||||
showMoreDataItems.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
66
dashboard/components/complex/line-data/Cities.vue
Normal file
66
dashboard/components/complex/line-data/Cities.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { CircleHelp } from 'lucide-vue-next';
|
||||
import LineDataTemplate, { type IconProvider, type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number, sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
|
||||
const { data: cities, status, refresh } = useAuthFetch<{ _id: any, count: number }[]>('/api/data/cities', {
|
||||
headers: { 'x-limit': '10' }, lazy: true,
|
||||
transform: (data) => {
|
||||
const res = data.filter(e => e._id !== '??' && getCityFromISO(e._id.city, e._id.region, e._id.country)).map(e => ({
|
||||
...e, flag: e._id,
|
||||
_id: e._id ? (getCityFromISO(e._id.city, e._id.region, e._id.country) ?? `NO_CITY`) : 'NO_CITY'
|
||||
}));
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Cities',
|
||||
sub: 'Lists the cities where users access your website.',
|
||||
data: cities.value ?? [],
|
||||
iconStyle: 'width: 1.8rem; padding: 1px;',
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: any, count: number }[]>('/api/data/cities', { headers: { 'x-limit': '1000' } });
|
||||
showMoreDataItems.value = data.filter(e => e._id !== '??' && getCityFromISO(e._id.city, e._id.region, e._id.country)).map(e => ({
|
||||
...e, flag: e._id,
|
||||
_id: e._id ? (getCityFromISO(e._id.city, e._id.region, e._id.country) ?? `NO_CITY`) : 'NO_CITY'
|
||||
}));
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
58
dashboard/components/complex/line-data/Continents.vue
Normal file
58
dashboard/components/complex/line-data/Continents.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { CircleHelp } from 'lucide-vue-next';
|
||||
import LineDataTemplate, { type IconProvider, type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number, sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
|
||||
const { data: continents, status, refresh } = useAuthFetch<{ _id: string, count: number }[]>('/api/data/continents', {
|
||||
headers: { 'x-limit': '10' }, lazy: true,
|
||||
transform: (data) => {
|
||||
return data.filter(e => e._id !== '??').map(e => ({ ...e, flag: e._id, _id: e._id ? (getContinentFromISO(e._id) ?? e._id) : 'NO_CONTINENT' }));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Continents',
|
||||
sub: 'Lists the continents where users access your website.',
|
||||
data: continents.value ?? [],
|
||||
iconStyle: 'width: 1.8rem; padding: 1px;',
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: string, count: number }[]>('/api/data/continents', { headers: { 'x-limit': '1000' } });
|
||||
showMoreDataItems.value = data.filter(e => e._id !== '??').map(e => ({ ...e, flag: e._id, _id: e._id ? (getContinentFromISO(e._id) ?? e._id) : 'NO_CONTINENT' }));
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
67
dashboard/components/complex/line-data/Countries.vue
Normal file
67
dashboard/components/complex/line-data/Countries.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { CircleHelp } from 'lucide-vue-next';
|
||||
import LineDataTemplate, { type IconProvider, type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number,sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
function iconProvider(e: { _id: string, flag: string, count: number }): ReturnType<IconProvider> {
|
||||
if (!e.flag) return ['component', CircleHelp]
|
||||
return [
|
||||
'img',
|
||||
`https://raw.githubusercontent.com/hampusborgos/country-flags/refs/heads/main/svg/${e.flag.toLowerCase()}.svg`
|
||||
]
|
||||
}
|
||||
|
||||
const { data: countries, status, refresh } = useAuthFetch<{ _id: string, count: number }[]>('/api/data/countries', {
|
||||
headers: { 'x-limit': '10' }, lazy: true,
|
||||
transform: (data) => {
|
||||
return data.map(e => ({ ...e, flag: e._id, _id: e._id ? (getCountryFromISO(e._id) ?? e._id) : 'NO_COUNTRY' }));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Countries',
|
||||
sub: 'Lists the countries where users access your website.',
|
||||
data: countries.value ?? [],
|
||||
iconProvider,
|
||||
iconStyle: 'width: 1.8rem; padding: 1px;',
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: string, count: number }[]>('/api/data/countries', { headers: { 'x-limit': '1000' } });
|
||||
showMoreDataItems.value = data.map(e => ({ ...e, flag: e._id, _id: e._id ? (getCountryFromISO(e._id) ?? e._id) : 'NO_COUNTRY' }));
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
86
dashboard/components/complex/line-data/Devices.vue
Normal file
86
dashboard/components/complex/line-data/Devices.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { CircleHelp, Gamepad2, Monitor, Smartphone, Tablet, Tv } from 'lucide-vue-next';
|
||||
import LineDataTemplate, { type IconProvider, type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number, sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
function iconProvider(e: { _id: string, count: number }): ReturnType<IconProvider> {
|
||||
if (e._id === 'desktop') return ['component', Monitor];
|
||||
if (e._id === 'tablet') return ['component', Tablet];
|
||||
if (e._id === 'mobile') return ['component', Smartphone];
|
||||
if (e._id === 'smarttv') return ['component', Tv];
|
||||
if (e._id === 'console') return ['component', Gamepad2];
|
||||
return ['component', CircleHelp]
|
||||
}
|
||||
|
||||
const OTHERS_INFO_TOOLTIP_TEXT = 'Device -> "Others" means the device used isn’t clearly a phone, tablet, or desktop... like smart TVs, game consoles, or unknown devices.';
|
||||
|
||||
|
||||
const { data: devices, status, refresh } = useAuthFetch<{ _id: string, count: number }[]>('/api/data/devices', {
|
||||
headers: { 'x-limit': '10' }, lazy: true,
|
||||
transform: (data) => {
|
||||
return data.map(e => {
|
||||
if (e._id === 'mobile') return e;
|
||||
if (e._id === 'desktop') return e;
|
||||
if (e._id === 'tablet') return e;
|
||||
if (e._id === 'console') return e;
|
||||
if (e._id === 'smarttv') return e;
|
||||
return { ...e, info: OTHERS_INFO_TOOLTIP_TEXT };
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Devices',
|
||||
sub: 'The devices most used to access your website.',
|
||||
data: devices.value ?? [],
|
||||
iconProvider,
|
||||
elementTextTransformer(text) {
|
||||
if (!text) return 'Others';
|
||||
if (text === 'mobile') return 'Mobile';
|
||||
if (text === 'desktop') return 'Desktop';
|
||||
if (text === 'tablet') return 'Tablet';
|
||||
if (text === 'console') return 'Console';
|
||||
if (text === 'smarttv') return 'Smart Tv';
|
||||
return text;
|
||||
},
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: string, count: number }[]>('/api/data/devices', { headers: { 'x-limit': '1000' } });
|
||||
showMoreDataItems.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
53
dashboard/components/complex/line-data/EntryPages.vue
Normal file
53
dashboard/components/complex/line-data/EntryPages.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import LineDataTemplate, { type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number, sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
const { data: pages, status, refresh } = useAuthFetch<{ _id: string, count: number }[]>('/api/data/entry_pages', {
|
||||
headers: { 'x-limit': '10' }, lazy: true
|
||||
});
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Entry pages',
|
||||
sub: 'First page a user lands on.',
|
||||
data: pages.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
},
|
||||
actionProps: { to: '/raw_visits' }
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: string, count: number }[]>('/api/data/entry_pages', { headers: { 'x-limit': '1000' } });
|
||||
showMoreDataItems.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
51
dashboard/components/complex/line-data/Events.vue
Normal file
51
dashboard/components/complex/line-data/Events.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import LineDataTemplate, { type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const { data: events,status,refresh } = useAuthFetch<{ _id: string, count: number }[]>('/api/data/events', {
|
||||
headers: { 'x-limit': '9' }, lazy: true
|
||||
});
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
const props = defineProps<{ refreshToken: number }>();
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Top events',
|
||||
sub: 'Most frequent user events triggered in this project.',
|
||||
data: events.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
},
|
||||
actionProps: { to: '/raw_events' }
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: string, count: number }[]>('/api/data/events', { headers: { 'x-limit': '1000' } });
|
||||
showMoreDataItems.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
52
dashboard/components/complex/line-data/ExitPages.vue
Normal file
52
dashboard/components/complex/line-data/ExitPages.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import LineDataTemplate, { type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number, sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
const { data: pages, status,refresh } = useAuthFetch<{ _id: string, count: number }[]>('/api/data/exit_pages', {
|
||||
headers: { 'x-limit': '10' }, lazy: true
|
||||
});
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Exit pages',
|
||||
sub: 'Last page a user visits before leaving.',
|
||||
data: pages.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
},
|
||||
actionProps: { to: '/raw_visits' }
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: string, count: number }[]>('/api/data/exit_pages', { headers: { 'x-limit': '1000' } });
|
||||
showMoreDataItems.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
33
dashboard/components/complex/line-data/LineDataCard.vue
Normal file
33
dashboard/components/complex/line-data/LineDataCard.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
export type LineDataCardProps = {
|
||||
title: string,
|
||||
sub: string,
|
||||
action?: Component,
|
||||
actionProps?: Record<string, any>
|
||||
}
|
||||
|
||||
const props = defineProps<{ data: LineDataCardProps }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'showMore'): void
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{{ data.title }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{{ data.sub }}
|
||||
</CardDescription>
|
||||
<CardAction v-if="data.action" class="flex items-center">
|
||||
<component :data="data.actionProps" :is="data.action"></component>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent class="h-full">
|
||||
<slot></slot>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
160
dashboard/components/complex/line-data/LineDataTemplate.vue
Normal file
160
dashboard/components/complex/line-data/LineDataTemplate.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { CircleHelp, InfoIcon, Link } from 'lucide-vue-next';
|
||||
import LineDataCard, { type LineDataCardProps } from './LineDataCard.vue';
|
||||
import ShowMoreDialog, { type ShowMoreDialogProps } from './ShowMoreDialog.vue';
|
||||
|
||||
export type IconProvider<T = any> = (e: { _id: string, count: string } & T) => ['img', string] | ['component', Component] | undefined;
|
||||
|
||||
|
||||
export type LineDataProps = {
|
||||
title: string,
|
||||
sub: string,
|
||||
loading: boolean,
|
||||
data: { _id: string, count: number, info?: string, avgSeconds?: number }[],
|
||||
iconProvider?: IconProvider<any>,
|
||||
iconStyle?: string,
|
||||
elementTextTransformer?: (text: string) => string,
|
||||
hasLink?: boolean,
|
||||
showMoreData: {
|
||||
items: { _id: string, count: number }[],
|
||||
loading: boolean
|
||||
},
|
||||
action?: Component,
|
||||
actionProps?: Record<string, any>
|
||||
}
|
||||
|
||||
|
||||
const props = defineProps<{ data: LineDataProps }>();
|
||||
|
||||
const total = computed(() => props.data.data.reduce((a, e) => a + e.count, 0));
|
||||
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'showMore'): void
|
||||
}>();
|
||||
|
||||
const maxData = computed(() => props.data.data.reduce((a, e) => a + e.count, 0));
|
||||
|
||||
function openExternalLink(link: string) {
|
||||
if (link === 'self') return;
|
||||
return window.open('https://' + link, '_blank');
|
||||
}
|
||||
|
||||
const showMoreDialogData = computed<ShowMoreDialogProps>(() => {
|
||||
return {
|
||||
title: props.data.title,
|
||||
sub: props.data.sub,
|
||||
items: props.data.showMoreData.items,
|
||||
total: props.data.data.reduce((a, e) => a + e.count, 0),
|
||||
loading: props.data.showMoreData.loading,
|
||||
iconProvider: props.data.iconProvider,
|
||||
iconStyle: props.data.iconStyle
|
||||
}
|
||||
})
|
||||
|
||||
const iconsErrored = ref<number[]>([]);
|
||||
|
||||
function onIconError(index: number) {
|
||||
iconsErrored.value.push(index);
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-2 h-full">
|
||||
<div class="w-full flex flex-col gap-1">
|
||||
<div class="flex justify-between text-sm font-medium text-muted-foreground pb-2">
|
||||
<p>Source</p>
|
||||
<div class="flex gap-2">
|
||||
<p v-if="props.data.data.at(0)?.avgSeconds" class="w-[6rem] text-right">Time Spent</p>
|
||||
<p class="w-16 text-right">Count</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex justify-between items-center" v-if="data.data && data.data.length > 0 && !data.loading"
|
||||
v-for="(element, index) of props.data.data">
|
||||
|
||||
<div class="flex items-center gap-2 w-10/12 relative">
|
||||
|
||||
<div v-if="data.hasLink">
|
||||
<Link @click="openExternalLink(element._id)"
|
||||
class="size-4 cursor-pointer hover:text-muted-foreground">
|
||||
</Link>
|
||||
<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">
|
||||
|
||||
<div class="absolute rounded-sm w-full h-full bg-accent"
|
||||
:style="'width:' + 100 / maxData * element.count + '%;'"></div>
|
||||
|
||||
<div class="flex px-2 py-1 relative items-center gap-4">
|
||||
<div v-if="data.iconProvider && data.iconProvider(element) != undefined"
|
||||
class="flex items-center h-[1.3rem]">
|
||||
|
||||
<img v-if="!iconsErrored.includes(index) && data.iconProvider(element)?.[0] === 'img'"
|
||||
class="h-full" @error="onIconError(index)" :style="data.iconStyle"
|
||||
:src="(data.iconProvider(element)?.[1] as string)">
|
||||
|
||||
<CircleHelp v-if="iconsErrored.includes(index)"></CircleHelp>
|
||||
|
||||
<component v-if="data.iconProvider(element)?.[0] == 'component'" class="size-5"
|
||||
:is="data.iconProvider(element)?.[1]">
|
||||
</component>
|
||||
</div>
|
||||
<span
|
||||
class=" line-clamp-1 ui-font z-[19] text-[.95rem] max-w-56 md:max-w-64 lg:max-w-96 overflow-x-auto">
|
||||
{{ data.elementTextTransformer?.(element._id) || element._id }}
|
||||
</span>
|
||||
<span v-if="element.info">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<InfoIcon class="size-4"></InfoIcon>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ element.info }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-center w-[6rem] text-[.8rem] text-muted-foreground" v-if="element.avgSeconds">
|
||||
{{ formatTime(element.avgSeconds * 1000, false) }}
|
||||
</span>
|
||||
<div
|
||||
class="cursor-default w-16 group text-muted-foreground items-center justify-end font-medium text-[.9rem] md:text-[1rem] poppins flex gap-2">
|
||||
<span class="group-hover:hidden flex">
|
||||
{{ formatNumberK(element.count) }}
|
||||
</span>
|
||||
<span class="hidden group-hover:flex "> {{ (100 / total * element.count).toFixed(1) }}% </span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.data.length > 0" class="grow"> </div>
|
||||
<Loader v-if="data.loading" />
|
||||
|
||||
<ShowMoreDialog v-if="data.data.length > 0" :data="showMoreDialogData">
|
||||
<Button v-if="!data.loading" @click="emits('showMore')" variant="ghost" class="w-full shrink-0">
|
||||
Show more
|
||||
</Button>
|
||||
</ShowMoreDialog>
|
||||
|
||||
|
||||
<div class="font-medium" v-if="data.title === 'Top pages' && data.data.length == 0 && !data.loading">
|
||||
You need at least 2 views
|
||||
</div>
|
||||
<div class="font-medium" v-else-if="data.data.length == 0 && !data.loading">
|
||||
No data yet
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
52
dashboard/components/complex/line-data/Oss.vue
Normal file
52
dashboard/components/complex/line-data/Oss.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import LineDataTemplate, { type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number, sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
const { data: oss,status,refresh } = useAuthFetch<{ _id: string, count: number }[]>('/api/data/oss', {
|
||||
headers: { 'x-limit': '10' }, lazy: true
|
||||
});
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'OS',
|
||||
sub: 'The operating systems most commonly used by your website\'s visitors..',
|
||||
data: oss.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: string, count: number }[]>('/api/data/oss', { headers: { 'x-limit': '1000' } });
|
||||
showMoreDataItems.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
55
dashboard/components/complex/line-data/Pages.vue
Normal file
55
dashboard/components/complex/line-data/Pages.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import LineDataTemplate, { type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number, sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
const { data: pages, status, refresh } = useAuthFetch<{ _id: string, count: number, avgSeconds: number }[]>('/api/data/pages_durations', {
|
||||
headers: { 'x-limit': '10' }, lazy: true
|
||||
});
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number, avgSeconds: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Top pages',
|
||||
sub: 'Most visited pages.',
|
||||
data: pages.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
},
|
||||
actionProps: { to: '/raw_visits' }
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: string, count: number, avgSeconds: number }[]>('/api/data/pages_durations', {
|
||||
headers: { 'x-limit': '1000' }
|
||||
});
|
||||
showMoreDataItems.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
75
dashboard/components/complex/line-data/Referrers.vue
Normal file
75
dashboard/components/complex/line-data/Referrers.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { Link } from 'lucide-vue-next';
|
||||
import LineDataTemplate, { type IconProvider, type LineDataProps } from './LineDataTemplate.vue';
|
||||
import SelectRefer from './selectors/SelectRefer.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number, sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
const SELF_INFO_TOOLTIP_TEXT = '"Self" means the visitor came to your site directly, without any referrer (like from typing the URL, a bookmark, or a QR code).';
|
||||
|
||||
const { data: referrers, status, refresh } = useAuthFetch<{ _id: string, count: number }[]>('/api/data/referrers', {
|
||||
headers: {
|
||||
'x-limit': '10'
|
||||
}, lazy: true,
|
||||
transform: (data) => {
|
||||
return data.map(e => e._id === 'self' ? { ...e, info: SELF_INFO_TOOLTIP_TEXT } : e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
function iconProvider(e: { _id: string, count: number }): ReturnType<IconProvider> {
|
||||
if (e._id === 'self') return ['component', Link];
|
||||
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${e._id}&sz=64`]
|
||||
}
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Referrers',
|
||||
sub: 'Where users find your website.',
|
||||
data: referrers.value ?? [],
|
||||
//action:SelectRefer,
|
||||
iconProvider,
|
||||
iconStyle: 'width: 1.3rem; height: auto;',
|
||||
hasLink: true,
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: string, count: number }[]>('/api/data/referrers', {
|
||||
headers: {
|
||||
'x-limit': '1000'
|
||||
}
|
||||
});
|
||||
showMoreDataItems.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
65
dashboard/components/complex/line-data/Regions.vue
Normal file
65
dashboard/components/complex/line-data/Regions.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { CircleHelp } from 'lucide-vue-next';
|
||||
import LineDataTemplate, { type IconProvider, type LineDataProps } from './LineDataTemplate.vue';
|
||||
|
||||
const props = defineProps<{ refreshToken: number, sharedLink?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
onMounted(() => {
|
||||
emits('init', data.value);
|
||||
})
|
||||
|
||||
|
||||
const { data: regions, status, refresh } = useAuthFetch<{ _id: any, count: number }[]>('/api/data/regions', {
|
||||
headers: { 'x-limit': '10' }, lazy: true,
|
||||
transform: (data) => {
|
||||
return data.filter(e => e._id !== '??' && getRegionFromISO(e._id.region, e._id.country)).map(e => ({
|
||||
...e, flag: e._id,
|
||||
_id: e._id ? (getRegionFromISO(e._id.region, e._id.country) ?? "NO_REGION") : 'NO_REGION'
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const data = computed<LineDataProps>(() => {
|
||||
return {
|
||||
loading: status.value !== 'success',
|
||||
title: 'Regions',
|
||||
sub: 'Lists the regions where users access your website.',
|
||||
data: regions.value ?? [],
|
||||
iconStyle: 'width: 1.8rem; padding: 1px;',
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const data = await useAuthFetchSync<{ _id: any, count: number }[]>('/api/data/regions', { headers: { 'x-limit': '1000' } });
|
||||
showMoreDataItems.value = data.filter(e => e._id !== '??' && getRegionFromISO(e._id.region, e._id.country)).map(e => ({
|
||||
...e, flag: e._id,
|
||||
_id: e._id ? (getRegionFromISO(e._id.region, e._id.country) ?? "NO_REGION") : 'NO_REGION'
|
||||
}));
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data"> </LineDataTemplate>
|
||||
</template>
|
||||
84
dashboard/components/complex/line-data/ShowMoreDialog.vue
Normal file
84
dashboard/components/complex/line-data/ShowMoreDialog.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import type { IconProvider } from './LineDataTemplate.vue'
|
||||
|
||||
export type ShowMoreDialogProps = {
|
||||
title: string,
|
||||
sub?: string,
|
||||
items: { _id: string, count: number, avgSeconds?: number }[],
|
||||
total: any,
|
||||
iconProvider?: IconProvider,
|
||||
iconStyle?: string,
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{ data: ShowMoreDialogProps }>();
|
||||
const total = computed(() => props.data.total);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog>
|
||||
<DialogTrigger as-child>
|
||||
<slot></slot>
|
||||
</DialogTrigger>
|
||||
<DialogContent class="max-w-[90dvw] min-w-[40dvw] max-h-[90dvh] overflow-y-hidden poppins">
|
||||
<!-- sm:max-w-[425px] min-w-[40rem] -->
|
||||
<DialogHeader>
|
||||
<DialogTitle> {{ data.title }} </DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ data.sub }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="overflow-y-auto h-100">
|
||||
<Table v-if="!data.loading">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[90%]"> Item </TableHead>
|
||||
<TableHead> Count </TableHead>
|
||||
<TableHead v-if="data.items[0].avgSeconds"> Duration </TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class="w-full">
|
||||
<TableRow v-for="item in data.items" :key="item._id">
|
||||
<TableCell class="font-medium">
|
||||
<div class="flex items-center gap-3">
|
||||
<div v-if="data.iconProvider">
|
||||
<img v-if="data.iconProvider(item)?.[0] === 'img'" class="h-full"
|
||||
:style="data.iconStyle" :src="(data.iconProvider(item)?.[1] as string)">
|
||||
|
||||
<component v-if="data.iconProvider(item)?.[0] == 'component'" class="size-4"
|
||||
:is="data.iconProvider(item)?.[1]">
|
||||
</component>
|
||||
</div>
|
||||
|
||||
<span class="max-w-96 overflow-auto">{{ item._id }}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="group">
|
||||
<span class="group-hover:hidden flex">
|
||||
{{ formatNumberK(item.count) }}
|
||||
|
||||
</span>
|
||||
<span class="hidden group-hover:flex "> {{ (100 / total * item.count).toFixed(1) }}%
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span class="text-center w-[6rem]" v-if="item.avgSeconds">
|
||||
{{ formatTime(item.avgSeconds * 1000, false) }}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div v-else class="flex justify-center items-center h-full w-full">
|
||||
<Loader />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
146
dashboard/components/complex/line-data/UtmGeneric.vue
Normal file
146
dashboard/components/complex/line-data/UtmGeneric.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import LineDataTemplate, { type LineDataProps } from './LineDataTemplate.vue';
|
||||
import type { UtmKey } from './selectors/SelectRefer.vue';
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
advanced_data: {
|
||||
raw_selected: string
|
||||
},
|
||||
refreshToken: number,
|
||||
sharedLink?: string
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'init', data: any): void
|
||||
}>()
|
||||
|
||||
const { data: utms, status: utms_status, refresh } = useAuthFetch<{ _id: string, count: number }[]>(() => `/api/data/utm?utm_type=${props.advanced_data.raw_selected.split('_')[1]}`,
|
||||
{
|
||||
headers: { 'x-limit': '10' },
|
||||
lazy: true
|
||||
}
|
||||
);
|
||||
|
||||
watch(() => props.refreshToken, async () => {
|
||||
loading.value = true;
|
||||
await refresh(); // rifà il fetch
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const showMoreDataItems = ref<{ _id: string, count: number }[]>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
|
||||
const utmDataMap: Record<UtmKey, LineDataProps> = {
|
||||
'utm_term': {
|
||||
loading: false,
|
||||
title: 'UTM Term',
|
||||
sub: 'Term breakdown by usage',
|
||||
data: utms.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
},
|
||||
'utm_campaign': {
|
||||
loading: false,
|
||||
title: 'UTM Campaign',
|
||||
sub: 'Campaign breakdown by usage',
|
||||
data: utms.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
},
|
||||
'utm_medium': {
|
||||
loading: false,
|
||||
title: 'UTM Medium',
|
||||
sub: 'Medium breakdown by usage',
|
||||
data: utms.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
},
|
||||
'utm_source': {
|
||||
loading: false,
|
||||
title: 'UTM Source',
|
||||
sub: 'Source breakdown by usage',
|
||||
data: utms.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
},
|
||||
'utm_content': {
|
||||
loading: false,
|
||||
title: 'UTM Content',
|
||||
sub: 'Content breakdown by usage',
|
||||
data: utms.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function buildLineDataProps(): LineDataProps {
|
||||
const target = utmDataMap[props.advanced_data.raw_selected as UtmKey];
|
||||
if (target) return {
|
||||
...target,
|
||||
loading: utms_status.value !== 'success',
|
||||
data: utms.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
return {
|
||||
loading: utms_status.value !== 'success',
|
||||
title: props.advanced_data.raw_selected,
|
||||
sub: 'Custom utm parameter',
|
||||
data: utms.value ?? [],
|
||||
showMoreData: {
|
||||
items: showMoreDataItems.value,
|
||||
loading: loading.value
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const data = ref<LineDataProps>(buildLineDataProps());
|
||||
|
||||
function updateData() {
|
||||
data.value = buildLineDataProps();
|
||||
emits('init', data.value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateData()
|
||||
})
|
||||
|
||||
watch(props, () => {
|
||||
updateData()
|
||||
});
|
||||
|
||||
watch(utms_status, () => {
|
||||
updateData()
|
||||
})
|
||||
|
||||
async function showMore() {
|
||||
loading.value = true;
|
||||
const raw = await useAuthFetchSync<{ _id: string; count: number }[]>(`/api/data/utm?utm_type=${props.advanced_data.raw_selected.split('_')[1]}`, {
|
||||
headers: { 'x-limit': '1000' }
|
||||
});
|
||||
showMoreDataItems.value = raw;
|
||||
loading.value = false;
|
||||
updateData();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineDataTemplate @show-more="showMore()" :data="data" />
|
||||
</template>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Lock } from 'lucide-vue-next'
|
||||
|
||||
// Props e emits
|
||||
const props = defineProps<{ modelValue?: string }>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
// Stato locale del valore selezionato
|
||||
const selectedCountryOption = ref(props.modelValue || '')
|
||||
|
||||
// Sync locale ↔ parent
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val !== selectedCountryOption.value) {
|
||||
selectedCountryOption.value = val || ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedCountryOption, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select v-model="selectedCountryOption">
|
||||
<SelectTrigger class="bg-gray-100 dark:bg-black">
|
||||
<SelectValue placeholder="Select a Source" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="continents">
|
||||
Continents
|
||||
</SelectItem>
|
||||
<SelectItem value="countries">
|
||||
Country
|
||||
</SelectItem>
|
||||
<SelectItem value="regions">
|
||||
Regions
|
||||
</SelectItem>
|
||||
<SelectItem value="cities">
|
||||
City
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
// Props e emits
|
||||
const props = defineProps<{ modelValue?: string }>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
// Stato locale del valore selezionato
|
||||
const selectedDeviceOption = ref(props.modelValue || '')
|
||||
|
||||
// Sync locale ↔ parent
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val !== selectedDeviceOption.value) {
|
||||
selectedDeviceOption.value = val || ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedDeviceOption, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select v-model="selectedDeviceOption">
|
||||
<SelectTrigger class="bg-gray-100 dark:bg-black">
|
||||
<SelectValue placeholder="Select a Source" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="devices">
|
||||
Devices
|
||||
</SelectItem>
|
||||
<SelectItem value="oss">
|
||||
OS
|
||||
</SelectItem>
|
||||
<SelectItem value="browsers">
|
||||
Browsers
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Lock } from 'lucide-vue-next'
|
||||
|
||||
// Props e emits
|
||||
const props = defineProps<{ modelValue?: string }>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
// Stato locale del valore selezionato
|
||||
const selectPageOption = ref(props.modelValue || '')
|
||||
|
||||
// Sync locale ↔ parent
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val !== selectPageOption.value) {
|
||||
selectPageOption.value = val || ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectPageOption, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select v-model="selectPageOption">
|
||||
<SelectTrigger class="bg-gray-100 dark:bg-black">
|
||||
<SelectValue placeholder="Select a Source" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="pages">
|
||||
Top Pages
|
||||
</SelectItem>
|
||||
|
||||
|
||||
<SelectItem value="pages_entries">
|
||||
Entry Pages
|
||||
</SelectItem>
|
||||
<SelectItem value="pages_exits">
|
||||
Exit Pages
|
||||
</SelectItem>
|
||||
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Lock } from 'lucide-vue-next'
|
||||
|
||||
// Props e emits
|
||||
const props = defineProps<{ modelValue?: string }>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
// Stato locale del valore selezionato
|
||||
const selectedTrafficOption = ref(props.modelValue || '')
|
||||
|
||||
export type UtmKey = keyof typeof utmKeysMap;
|
||||
|
||||
const utmKeysMap = {
|
||||
'utm_campaign': 'Campaign',
|
||||
'utm_source': 'Source',
|
||||
'utm_medium': 'Medium',
|
||||
'utm_term': 'Term',
|
||||
'utm_content': 'Content'
|
||||
}
|
||||
|
||||
// Sync locale ↔ parent
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val !== selectedTrafficOption.value) {
|
||||
selectedTrafficOption.value = val || ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedTrafficOption, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select v-model="selectedTrafficOption">
|
||||
<SelectTrigger class="bg-gray-100 dark:bg-black">
|
||||
<SelectValue placeholder="Select a Source" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Sources</SelectLabel>
|
||||
<SelectItem value="referrers">
|
||||
Referrer
|
||||
</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectLabel>UTM</SelectLabel>
|
||||
<SelectItem v-for="(value, key) in utmKeysMap" :value="key">
|
||||
{{ value }}
|
||||
</SelectItem>
|
||||
<!-- <SelectSeparator />
|
||||
<SelectLabel>Custom</SelectLabel>
|
||||
<SelectItem v-if="utm_keys" v-for="key of utm_keys" :value="key._id">
|
||||
{{ key._id.split('_').slice(1).join(' ') }}
|
||||
</SelectItem> -->
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</template>
|
||||
Reference in New Issue
Block a user