mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
update
This commit is contained in:
@@ -6,6 +6,7 @@ registerChartComponents();
|
||||
const props = defineProps<{
|
||||
datasets: any[],
|
||||
labels: string[],
|
||||
legendPosition?: "left" | "top" | "right" | "bottom" | "center" | "chartArea"
|
||||
}>();
|
||||
|
||||
const chartOptions = ref<ChartOptions<'bar'>>({
|
||||
@@ -40,7 +41,7 @@ const chartOptions = ref<ChartOptions<'bar'>>({
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'right',
|
||||
position: props.legendPosition ?? 'right',
|
||||
},
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
|
||||
@@ -84,23 +84,8 @@ function reloadPage() {
|
||||
|
||||
<div class="flex gap-6 xl:flex-row flex-col">
|
||||
|
||||
<div class="h-full w-full">
|
||||
<CardTitled class="h-full w-full xl:min-w-[400px] xl:h-[35rem]" title="Quick setup tutorial"
|
||||
sub="Quickly Set Up Litlyx in 30 Seconds!">
|
||||
|
||||
<div class="flex items-center justify-center h-full w-full">
|
||||
|
||||
<iframe class="w-full h-full min-h-[400px]"
|
||||
src="https://www.youtube.com/embed/LInFoNLJ-CI?si=a97HVXpXFDgFg2Yp" title="Litlyx"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
</CardTitled>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex gap-4">
|
||||
|
||||
<div class="w-full">
|
||||
<CardTitled title="Quick Integration"
|
||||
@@ -133,27 +118,6 @@ function reloadPage() {
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<CardTitled class="w-full h-full" title="Start with Wordpress." sub="Setup Litlyx analytics in 30 seconds on Wordpress.">
|
||||
<template #header>
|
||||
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
|
||||
to="https://docs.litlyx.com/techs/wordpress">
|
||||
Visit documentation
|
||||
</LyxUiButton>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="justify-center w-full hidden xl:flex gap-3">
|
||||
<a href="https://docs.litlyx.com/techs/wordpress">
|
||||
<img class="cursor-pointer" :src="'tech-icons/wpel.png'" alt="Litlyx-Wordpress-Elementor">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</CardTitled>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<CardTitled class="w-full h-full" title="Modules"
|
||||
@@ -168,28 +132,36 @@ function reloadPage() {
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="justify-center w-full hidden xl:flex gap-3">
|
||||
<a href="https://docs.litlyx.com/techs/js" target="_blank">
|
||||
<img class="cursor-pointer" :src="'tech-icons/js.png'" alt="Litlyx-Javascript-Analytics">
|
||||
<img class="cursor-pointer" :src="'tech-icons/js.png'"
|
||||
alt="Litlyx-Javascript-Analytics">
|
||||
</a>
|
||||
<a href="https://docs.litlyx.com/techs/nuxt" target="_blank">
|
||||
<img class="cursor-pointer" :src="'tech-icons/nuxt.png'" alt="Litlyx-Nuxt-Analytics">
|
||||
<img class="cursor-pointer" :src="'tech-icons/nuxt.png'"
|
||||
alt="Litlyx-Nuxt-Analytics">
|
||||
</a>
|
||||
<a href="https://docs.litlyx.com/techs/next" target="_blank">
|
||||
<img class="cursor-pointer" :src="'tech-icons/next.png'" alt="Litlyx-Next-Analytics">
|
||||
<img class="cursor-pointer" :src="'tech-icons/next.png'"
|
||||
alt="Litlyx-Next-Analytics">
|
||||
</a>
|
||||
<a href="https://docs.litlyx.com/techs/react" target="_blank">
|
||||
<img class="cursor-pointer" :src="'tech-icons/react.png'" alt="Litlyx-React-Analytics">
|
||||
<img class="cursor-pointer" :src="'tech-icons/react.png'"
|
||||
alt="Litlyx-React-Analytics">
|
||||
</a>
|
||||
<a href="https://docs.litlyx.com/techs/vue" target="_blank">
|
||||
<img class="cursor-pointer" :src="'tech-icons/vue.png'" alt="Litlyx-Vue-Analytics">
|
||||
<img class="cursor-pointer" :src="'tech-icons/vue.png'"
|
||||
alt="Litlyx-Vue-Analytics">
|
||||
</a>
|
||||
<a href="https://docs.litlyx.com/techs/angular" target="_blank">
|
||||
<img class="cursor-pointer" :src="'tech-icons/angular.png'" alt="Litlyx-Angular-Analytics">
|
||||
<img class="cursor-pointer" :src="'tech-icons/angular.png'"
|
||||
alt="Litlyx-Angular-Analytics">
|
||||
</a>
|
||||
<a href="https://docs.litlyx.com/techs/python" target="_blank">
|
||||
<img class="cursor-pointer" :src="'tech-icons/py.png'" alt="Litlyx-Python-Analytics">
|
||||
<img class="cursor-pointer" :src="'tech-icons/py.png'"
|
||||
alt="Litlyx-Python-Analytics">
|
||||
</a>
|
||||
<a href="https://docs.litlyx.com/techs/serverless" target="_blank">
|
||||
<img class="cursor-pointer" :src="'tech-icons/serverless.png'" alt="Litlyx-Serverless-Analytics">
|
||||
<img class="cursor-pointer" :src="'tech-icons/serverless.png'"
|
||||
alt="Litlyx-Serverless-Analytics">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -197,6 +169,48 @@ function reloadPage() {
|
||||
</CardTitled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 w-full">
|
||||
|
||||
|
||||
<CardTitled class="w-full h-full" title="Start with Wordpress."
|
||||
sub="Setup Litlyx analytics in 30 seconds on Wordpress.">
|
||||
<!-- <template #header>
|
||||
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
|
||||
to="https://docs.litlyx.com/techs/wordpress">
|
||||
Visit documentation
|
||||
</LyxUiButton>
|
||||
</template> -->
|
||||
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="justify-center w-full hidden xl:flex gap-3">
|
||||
<a href="https://docs.litlyx.com/techs/wordpress">
|
||||
<img class="cursor-pointer" :src="'tech-icons/wpel.png'"
|
||||
alt="Litlyx-Wordpress-Elementor">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</CardTitled>
|
||||
|
||||
<CardTitled class="w-full h-full" title="Start with Shopify."
|
||||
sub="Setup Litlyx analytics in 30 seconds on Shopify.">
|
||||
<!-- <template #header>
|
||||
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
|
||||
to="https://docs.litlyx.com/techs/shopify">
|
||||
Visit documentation
|
||||
</LyxUiButton>
|
||||
</template> -->
|
||||
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="justify-center w-full hidden xl:flex gap-3">
|
||||
<a href="https://docs.litlyx.com/techs/wordpress">
|
||||
<img class="cursor-pointer" :src="'tech-icons/shopify.png'" alt="Litlyx-Shopify">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</CardTitled>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ const canSearch = computed(() => {
|
||||
<template>
|
||||
|
||||
|
||||
<CardTitled title="Event metadata analyzer" sub="Filter events metadata fields to analyze them" class="w-full p-4">
|
||||
<CardTitled title="Analyze event metadata" sub="Filter events metadata fields to analyze them" class="w-full p-4">
|
||||
|
||||
<div class="">
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<AdvancedStackedBarChart v-if="!eventsStackedData.pending.value && !errorData.errored"
|
||||
:datasets="eventsStackedData.data.value?.datasets || []"
|
||||
:labels="eventsStackedData.data.value?.labels || []">
|
||||
:labels="eventsStackedData.data.value?.labels || []" legendPosition="bottom">
|
||||
</AdvancedStackedBarChart>
|
||||
<div v-if="errorData.errored" class="flex items-center justify-center py-8 h-full">
|
||||
{{ errorData.text }}
|
||||
|
||||
73
dashboard/components/selector/ImageSelector.vue
Normal file
73
dashboard/components/selector/ImageSelector.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'file_selected', value: string): void;
|
||||
}>();
|
||||
|
||||
const fileInput = ref<HTMLElement | null>(null)
|
||||
const isDragging = ref(false)
|
||||
|
||||
const triggerFileSelect = () => { (fileInput.value as any).click() }
|
||||
|
||||
const handleFileChange = async (event: any) => {
|
||||
const file = event.target.files[0]
|
||||
if (file) {
|
||||
const b64 = await getBase64FromFile(file);
|
||||
emits('file_selected', b64);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function getBase64FromFile(file: File) {
|
||||
return new Promise<string>(resolve => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = async function () {
|
||||
const base64String = reader.result;
|
||||
if (!base64String) return alert('Error reading image');
|
||||
resolve(base64String as string);
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const handleDrop = async (event: any) => {
|
||||
const file = event.dataTransfer.files[0]
|
||||
isDragging.value = false
|
||||
if (file) {
|
||||
const b64 = await getBase64FromFile(file);
|
||||
emits('file_selected', b64);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = () => {
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<div id="drop-area"
|
||||
class="w-full select-none max-w-md border-2 border-dashed border-gray-600 rounded-lg p-12 text-center cursor-pointer hover:border-blue-500 transition"
|
||||
@click="triggerFileSelect" @dragover.prevent="handleDragOver" @dragleave="handleDragLeave"
|
||||
@drop.prevent="handleDrop" :class="{ 'border-blue-500': isDragging }">
|
||||
<p class="text-gray-400">
|
||||
Drag & drop an image here
|
||||
<br>
|
||||
or click to select a file
|
||||
</p>
|
||||
<input ref="fileInput" type="file" accept="image/*" class="hidden" @change="handleFileChange" />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -148,7 +148,7 @@ const sessionsLabel = computed(() => {
|
||||
|
||||
<div v-if="!isGuest"
|
||||
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-lyx-lightmode-widget-light dark:bg-[#1e1412]">
|
||||
<div class="poppins font-semibold"> This operation will reset this project to it's initial state (0
|
||||
<div class="poppins font-semibold"> This operation will reset this project to its initial state (0
|
||||
visits 0 events 0 sessions) </div>
|
||||
<div @click="openDeleteAllDomainDataDialog()"
|
||||
class="text-[#e95b61] poppins font-semibold cursor-pointer hover:text-black hover:bg-red-700 outline rounded-lg w-fit px-8 py-2 outline-[1px] outline-[#532b26] bg-lyx-lightmode-widget-light dark:bg-[#291415]">
|
||||
|
||||
@@ -18,6 +18,7 @@ const sections: Section[] = [
|
||||
entries: [
|
||||
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
||||
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
||||
{ label: 'Reports', to: '/reports', icon: 'fal fa-file' },
|
||||
{ label: 'Members', to: '/members', icon: 'fal fa-users' },
|
||||
{ label: 'Shields', to: '/shields', icon: 'fal fa-shield' },
|
||||
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
|
||||
|
||||
@@ -53,8 +53,17 @@ const eventsData = await useFetch(`/api/data/count`, {
|
||||
</LyxUiCard>
|
||||
|
||||
|
||||
<div>
|
||||
<BarCardEvents :key="refreshKey"></BarCardEvents>
|
||||
<div class="flex gap-6 flex-col xl:flex-row xl:h-full">
|
||||
|
||||
<BarCardEvents class="xl:flex-[4]" :key="refreshKey"></BarCardEvents>
|
||||
|
||||
|
||||
<CardTitled :key="refreshKey" class="p-4 xl:flex-[2] w-full h-full" title="Top events"
|
||||
sub="Displays key events.">
|
||||
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
|
||||
</CardTitled>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex gap-6 flex-col xl:flex-row xl:h-full">
|
||||
@@ -74,11 +83,6 @@ const eventsData = await useFetch(`/api/data/count`, {
|
||||
</div>
|
||||
</CardTitled>
|
||||
|
||||
<CardTitled :key="refreshKey" class="p-4 xl:flex-[2] w-full h-full" title="Top events"
|
||||
sub="Displays key events.">
|
||||
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
|
||||
</CardTitled>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
|
||||
async function generatePDF() {
|
||||
|
||||
try {
|
||||
const res = await $fetch<Blob>('/api/project/generate_pdf', {
|
||||
...signHeaders(),
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(res);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `Report.pdf`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (ex: any) {
|
||||
alert(ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<div class="home w-full h-full px-10 lg:px-0 overflow-y-auto pb-[12rem] md:pb-0">
|
||||
|
||||
<DialogCreateSnapshot></DialogCreateSnapshot>
|
||||
|
||||
<!-- <div class="flex flex-col items-center justify-center mt-20 gap-20">
|
||||
|
||||
<div class="flex flex-col items-center justify-center gap-10">
|
||||
<div class="poppins text-[2.4rem] font-bold text-text">
|
||||
Project Report
|
||||
</div>
|
||||
<div class="poppins text-[1.4rem] text-center lg:text-[1.8rem] text-text-sub/90">
|
||||
One-Click, Comprehensive KPI PDF for Your Investors or Team.
|
||||
</div>
|
||||
<div v-if="activeProject" class="flex md:gap-2 flex-col md:flex-row">
|
||||
<div class="poppins text-[1.4rem] font-semibold text-text-sub/90">
|
||||
Relative to:
|
||||
</div>
|
||||
<div class="poppins text-[1.4rem] font-semibold text-text">
|
||||
{{ activeProject.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
<div @click="generatePDF()"
|
||||
class="flex flex-col rounded-xl overflow-hidden hover:shadow-[0_0_50px_#2969f1] hover:outline hover:outline-[2px] hover:outline-accent cursor-pointer">
|
||||
<div class="h-[14rem] aspect-[9/7] bg-[#2f2a64] flex relative">
|
||||
<img class="object-cover" :src="'/report/card_image.png'">
|
||||
|
||||
<div
|
||||
class="absolute px-4 py-1 rounded-lg poppins left-2 flex gap-2 bottom-2 bg-orange-500/80 items-center">
|
||||
<div class="flex items-center"> <i class="far fa-fire text-[1.1rem]"></i></div>
|
||||
<div class="poppins text-[1rem] font-semibold"> Popular </div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="bg-[#444444cc] p-4 h-[7rem] relative">
|
||||
<div class="poppins text-[1.2rem] font-bold text-text">
|
||||
Generate
|
||||
</div>
|
||||
<div class="poppins text-[1rem] text-text-sub/90">
|
||||
Create your report now
|
||||
</div>
|
||||
<div class="absolute right-4 bottom-3">
|
||||
<i class="fas fa-arrow-right text-[1.2rem]"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
108
dashboard/pages/reports.vue
Normal file
108
dashboard/pages/reports.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const customization = ref<any>();
|
||||
|
||||
const { snapshot } = useSnapshot();
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await $fetch('/api/report/customization', {
|
||||
headers: useComputedHeaders().value
|
||||
})
|
||||
customization.value = res;
|
||||
})
|
||||
|
||||
async function updateCustomization() {
|
||||
await $fetch('/api/report/update_customization', {
|
||||
method: 'POST',
|
||||
headers: useComputedHeaders({
|
||||
custom: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).value,
|
||||
body: JSON.stringify(customization.value)
|
||||
})
|
||||
}
|
||||
|
||||
async function generateReport(type: number) {
|
||||
try {
|
||||
const res = await $fetch<Blob>(`/api/project/generate_pdf?type=${type}`, {
|
||||
headers: useComputedHeaders({
|
||||
useSnapshotDates: false, custom: {
|
||||
'x-snapshot-name': snapshot.value.name
|
||||
}
|
||||
}).value,
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(res);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `Report.pdf`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (ex: any) {
|
||||
alert(ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function selectColor(color: string) {
|
||||
customization.value.bg = color;
|
||||
updateCustomization();
|
||||
}
|
||||
|
||||
function onFileSelected(e: string) {
|
||||
customization.value.logo = e;
|
||||
updateCustomization();
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<CardTitled class="w-full h-full" title="Choose a report" sub="Select a report type">
|
||||
<div style="height: 18rem;" class="w-full flex gap-4">
|
||||
<LyxUiCard>
|
||||
<div @click="generateReport(1)" class="cursor-pointer hover:text-lyx-text-darker">
|
||||
Easy report
|
||||
</div>
|
||||
</LyxUiCard>
|
||||
<LyxUiCard>
|
||||
<div @click="generateReport(1)" class="cursor-pointer hover:text-lyx-text-darker">
|
||||
Product report
|
||||
</div>
|
||||
</LyxUiCard>
|
||||
</div>
|
||||
</CardTitled>
|
||||
<div class="flex gap-4">
|
||||
<CardTitled class="w-full h-full" title="Customize theme" sub="Choose the report colors">
|
||||
<div v-if="customization" style="height: 18rem;" class="w-full flex gap-2">
|
||||
<div @click="selectColor('white')"
|
||||
class="flex items-center justify-center rounded-lg bg-white border-solid border-[1px] border-gray-200 cursor-pointer w-[4rem] h-[2rem]">
|
||||
<i v-if="customization.bg == 'white'" class="fas fa-check text-blue-600"></i>
|
||||
</div>
|
||||
<div @click="selectColor('black')"
|
||||
class="flex items-center justify-center rounded-lg bg-black border-solid border-[1px] border-gray-200 cursor-pointer w-[4rem] h-[2rem]">
|
||||
<i v-if="customization.bg == 'black'" class="fas fa-check text-blue-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</CardTitled>
|
||||
<CardTitled class="w-full h-full" title="Customize logo" sub="Upload your logo">
|
||||
<div v-if="customization" style="height: 18rem;" class="w-full flex gap-4">
|
||||
<img v-if="customization.logo" :src="customization.logo" class="w-[256px] h-[256px]">
|
||||
<div class="flex h-[10rem]">
|
||||
<SelectorImageSelector class="w-fit" @file_selected="onFileSelected">
|
||||
</SelectorImageSelector>
|
||||
</div>
|
||||
</div>
|
||||
</CardTitled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
BIN
dashboard/public/tech-icons/shopify.png
Normal file
BIN
dashboard/public/tech-icons/shopify.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -5,6 +5,7 @@ import { PassThrough } from 'node:stream';
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
import { VisitModel } from '@schema/metrics/VisitSchema';
|
||||
import { EventModel } from '@schema/metrics/EventSchema';
|
||||
import { ReportCustomizationModel, TReportCustomization } from '~/shared/schema/report/ReportCustomizationSchema';
|
||||
|
||||
|
||||
type PDFGenerationData = {
|
||||
@@ -18,7 +19,7 @@ type PDFGenerationData = {
|
||||
topCountries: string[],
|
||||
topReferrers: string[],
|
||||
avgGrowthText: string,
|
||||
|
||||
customization?: TReportCustomization
|
||||
}
|
||||
|
||||
function formatNumberK(value: string | number, decimals: number = 1) {
|
||||
@@ -37,14 +38,26 @@ const resourcePath = process.env.MODE === 'TEST' ? './public/pdf/' : './.output/
|
||||
function createPdf(data: PDFGenerationData) {
|
||||
|
||||
const pdf = new pdfkit({ size: 'A4', margins: { top: 50, bottom: 50, left: 50, right: 50 }, });
|
||||
pdf.fillColor('#ffffff').rect(0, 0, pdf.page.width, pdf.page.height).fill('#000000');
|
||||
|
||||
pdf.font(resourcePath + 'pdf_fonts/Poppins-Bold.ttf').fontSize(16).fillColor('#ffffff');
|
||||
let bgColor = '#0A0A0A';
|
||||
let textColor = 'FFFFFF';
|
||||
let logo = data.customization?.logo ?? resourcePath + 'pdf_images/logo.png'
|
||||
|
||||
pdf.text(`Project name: ${data.projectName}`, { align: 'left' }).moveDown(LINE_SPACING);
|
||||
if (data.customization?.bg) {
|
||||
bgColor = data.customization.bg === 'white' ? '#FFFFFF' : '#0A0A0A';
|
||||
textColor = data.customization.bg === 'white' ? '#000000' : '#FFFFFF';
|
||||
}
|
||||
|
||||
|
||||
|
||||
pdf.fillColor(bgColor).rect(0, 0, pdf.page.width, pdf.page.height).fill(bgColor);
|
||||
|
||||
pdf.font(resourcePath + 'pdf_fonts/Poppins-Bold.ttf').fontSize(16).fillColor(textColor);
|
||||
|
||||
pdf.text(`${data.projectName}`, { align: 'center' }).moveDown(LINE_SPACING);
|
||||
pdf.text(`Timeframe name: ${data.snapshotName}`, { align: 'left' }).moveDown(LINE_SPACING);
|
||||
|
||||
pdf.font(resourcePath + 'pdf_fonts/Poppins-Regular.ttf').fontSize(12).fillColor('#ffffff')
|
||||
pdf.font(resourcePath + 'pdf_fonts/Poppins-Regular.ttf').fontSize(12).fillColor(textColor)
|
||||
|
||||
pdf.text(`Total visits: ${data.totalVisits}`, { align: 'left' }).moveDown(LINE_SPACING);
|
||||
pdf.text(`Average visits per day: ${data.avgVisitsDay}`, { align: 'left' }).moveDown(LINE_SPACING);
|
||||
@@ -71,10 +84,11 @@ function createPdf(data: PDFGenerationData) {
|
||||
|
||||
pdf.font(resourcePath + 'pdf_fonts/Poppins-Regular.ttf')
|
||||
.fontSize(10)
|
||||
.fillColor('#ffffff')
|
||||
.fillColor(textColor)
|
||||
.text('Created with Litlyx.com', 50, 760, { align: 'center' });
|
||||
|
||||
pdf.image(resourcePath + 'pdf_images/logo.png', 460, 700, { width: 100 });
|
||||
|
||||
pdf.image(logo, 460, 700, { width: 100 });
|
||||
|
||||
pdf.end();
|
||||
return pdf;
|
||||
@@ -147,6 +161,8 @@ export default defineEventHandler(async event => {
|
||||
{ $limit: 3 }
|
||||
]);
|
||||
|
||||
const customization = await ReportCustomizationModel.findOne({ project_id: project._id });
|
||||
|
||||
const pdf = createPdf({
|
||||
projectName: project.name,
|
||||
snapshotName: snapshotHeader || 'NO_NAME',
|
||||
@@ -157,7 +173,8 @@ export default defineEventHandler(async event => {
|
||||
topDevice: topDevice,
|
||||
topDomain: topDomain,
|
||||
topCountries: topCountries.map(e => e._id),
|
||||
topReferrers: topReferrers.map(e => e._id)
|
||||
topReferrers: topReferrers.map(e => e._id),
|
||||
customization: customization?.toJSON() as TReportCustomization
|
||||
});
|
||||
|
||||
const passThrough = new PassThrough();
|
||||
|
||||
20
dashboard/server/api/report/customization.ts
Normal file
20
dashboard/server/api/report/customization.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
import { ReportCustomizationModel, TReportCustomization } from "~/shared/schema/report/ReportCustomizationSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event, []);
|
||||
if (!data) return;
|
||||
|
||||
const customization = await ReportCustomizationModel.findOne({ project_id: data.project_id });
|
||||
|
||||
if (!customization) return {
|
||||
_id: '' as any,
|
||||
project_id: data.project_id.toString() as any,
|
||||
bg: 'black',
|
||||
logo: undefined,
|
||||
text: 'white'
|
||||
} as TReportCustomization;
|
||||
|
||||
return customization.toJSON() as TReportCustomization;
|
||||
|
||||
});
|
||||
26
dashboard/server/api/report/update_customization.post.ts
Normal file
26
dashboard/server/api/report/update_customization.post.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
import z from 'zod';
|
||||
import { ReportCustomizationModel } from "~/shared/schema/report/ReportCustomizationSchema";
|
||||
|
||||
const ZUpdateCustomizationBody = z.object({
|
||||
logo: z.string().optional(),
|
||||
bg: z.enum(['black', 'white'])
|
||||
})
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event, []);
|
||||
if (!data) return;
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
const bodyData = ZUpdateCustomizationBody.parse(body);
|
||||
|
||||
await ReportCustomizationModel.updateOne({ project_id: data.project_id }, {
|
||||
logo: bodyData.logo,
|
||||
bg: bodyData.bg,
|
||||
text: bodyData.bg === 'white' ? 'black' : 'white'
|
||||
}, { upsert: true });
|
||||
|
||||
return { ok: true }
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user