This commit is contained in:
Emily
2024-09-12 16:16:19 +02:00
parent 6a9a698b7a
commit 525a371a6e
20 changed files with 746 additions and 365 deletions

View File

@@ -20,7 +20,8 @@ export async function startStreamLoop() {
await RedisStreamService.startReadingLoop({ await RedisStreamService.startReadingLoop({
streamName: requireEnv('STREAM_NAME'), streamName: requireEnv('STREAM_NAME'),
delay: { base: 10, empty: 5000 }, delay: { base: 10, empty: 5000 },
readBlock: 2000 readBlock: 2000,
consumer: 'consumer_' + process.env.NODE_APP_INSTANCE
}, processStreamEvent); }, processStreamEvent);
} }

View File

@@ -12,12 +12,6 @@ const { showDialog, closeDialog, dialogComponent, dialogParams, dialogStyle, dia
const { visible } = usePricingDrawer(); const { visible } = usePricingDrawer();
const { data: planData } = useFetch('/api/project/plan', {
...signHeaders(),
lazy: true
});
</script> </script>
<template> <template>
@@ -25,7 +19,7 @@ const { data: planData } = useFetch('/api/project/plan', {
<div class="w-dvw h-dvh bg-lyx-background-light relative"> <div class="w-dvw h-dvh bg-lyx-background-light relative">
<Transition name="pdrawer"> <Transition name="pdrawer">
<LazyPricingDrawer @onCloseClick="visible = false" :currentSub="planData?.premium_type || 0" <LazyPricingDrawer @onCloseClick="visible = false"
class="bg-black fixed right-0 top-0 w-full xl:w-[60vw] xl:min-w-[65rem] h-full z-[20]" v-if=visible> class="bg-black fixed right-0 top-0 w-full xl:w-[60vw] xl:min-w-[65rem] h-full z-[20]" v-if=visible>
</LazyPricingDrawer> </LazyPricingDrawer>
</Transition> </Transition>

View File

@@ -10,7 +10,7 @@ export type Entry = {
icon?: string, icon?: string,
action?: () => any, action?: () => any,
adminOnly?: boolean, adminOnly?: boolean,
premiumOnly?:boolean, premiumOnly?: boolean,
external?: boolean, external?: boolean,
grow?: boolean grow?: boolean
} }
@@ -117,10 +117,13 @@ watch(selected, () => {
setActiveProject(selected.value._id.toString()) setActiveProject(selected.value._id.toString())
}) })
const isPremium = computed(()=>{ const isPremium = computed(() => {
return activeProject.value?.premium; return activeProject.value?.premium;
}) })
const pricingDrawer = usePricingDrawer();
</script> </script>
<template> <template>
@@ -131,6 +134,13 @@ const isPremium = computed(()=>{
}"> }">
<div class="py-4 px-2 gap-6 flex flex-col w-full"> <div class="py-4 px-2 gap-6 flex flex-col w-full">
<div class="flex px-2" v-if="!isPremium">
<LyxUiButton type="primary" class="w-full text-center text-[.8rem] font-medium" @click="pricingDrawer.visible.value = true;">
Upgrade plan
</LyxUiButton>
</div>
<div class="flex px-2 flex-col"> <div class="flex px-2 flex-col">
<div class="flex items-center gap-2 w-full"> <div class="flex items-center gap-2 w-full">

View File

@@ -0,0 +1,312 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import DateService, { type Slice } from '@services/DateService';
import type { ChartData, ChartOptions, TooltipModel } from 'chart.js';
import { useLineChart, LineChart } from 'vue-chart-3';
registerChartComponents();
const chartOptions = ref<ChartOptions<'line'>>({
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'nearest',
axis: 'x',
includeInvisible: true
},
scales: {
y: {
ticks: { display: true },
grid: {
display: true,
drawBorder: false,
color: '#CCCCCC22',
// borderDash: [5, 10]
},
},
x: {
ticks: { display: true },
grid: {
display: true,
drawBorder: false,
color: '#CCCCCC22',
}
}
},
plugins: {
legend: { display: false },
title: { display: false },
tooltip: {
// enabled: true,
// backgroundColor: 'rgba(0, 0, 0, 0.8)',
// titleFont: { size: 16, weight: 'bold' },
// bodyFont: { size: 14 },
// padding: 10,
// cornerRadius: 4,
// boxPadding: 10,
// caretPadding: 20,
// yAlign: 'bottom',
// xAlign: 'center',
enabled: false,
position: 'nearest',
external: externalTooltipHandler
}
},
});
const chartData = ref<ChartData<'line' | 'bar' | 'bubble'>>({
labels: [],
datasets: [
{
label: 'Visits',
data: [],
backgroundColor: ['#5655d7'],
borderColor: '#5655d7',
borderWidth: 4,
fill: true,
tension: 0.45,
pointRadius: 0,
pointHoverRadius: 10,
hoverBackgroundColor: '#5655d7',
hoverBorderColor: 'white',
hoverBorderWidth: 2,
},
{
label: 'Unique sessions',
data: [],
backgroundColor: ['#4abde8'],
borderColor: '#4abde8',
borderWidth: 2,
hoverBackgroundColor: '#4abde8',
hoverBorderColor: '#4abde8',
hoverBorderWidth: 2,
type: 'bar'
},
{
label: 'Events',
data: [],
backgroundColor: ['#fbbf24'],
borderColor: '#fbbf24',
borderWidth: 2,
hoverBackgroundColor: '#fbbf24',
hoverBorderColor: '#fbbf24',
hoverBorderWidth: 2,
type: 'bubble',
stack: 'combined'
},
],
});
const { lineChartProps, lineChartRef, update: updateChart } = useLineChart({ chartData: (chartData as any), options: chartOptions });
const externalTooltipElement = ref<null | HTMLDivElement>(null);
function externalTooltipHandler(context: { chart: any, tooltip: TooltipModel<'line' | 'bar'> }) {
const { chart, tooltip } = context;
const tooltipEl = externalTooltipElement.value;
currentTooltipData.value = [0,0,0,''];
currentTooltipData.value.push(...tooltip.dataPoints.map(e => e.raw) as number[]);
currentTooltipData.value[2] = ((tooltip.dataPoints[2]?.raw as any)?.r2 || 0) as number;
currentTooltipData.value[3] = new Date(allDatesFull.value[tooltip.dataPoints[0].dataIndex]).toLocaleDateString();
if (!tooltipEl) return;
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = '0';
return;
}
const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas;
tooltipEl.style.opacity = '1';
tooltipEl.style.left = positionX + tooltip.caretX + 'px';
tooltipEl.style.top = positionY + tooltip.caretY + 'px';
tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
}
const selectLabels: { label: string, value: Slice }[] = [
{ label: 'Hour', value: 'hour' },
{ label: 'Day', value: 'day' },
];
const selectedLabelIndex = ref<number>(1);
const activeProject = useActiveProject();
const { safeSnapshotDates } = useSnapshot()
const allDatesFull = ref<string[]>([]);
function transformResponse(input: { _id: string, count: number }[]) {
const data = input.map(e => e.count);
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, selectLabels[selectedLabelIndex.value].value));
allDatesFull.value = input.map(e => e._id.toString());
return { data, labels }
}
const body = computed(() => {
console.log('INDEX IS', selectedLabelIndex.value, 'VALUE IS', selectLabels[selectedLabelIndex.value].value)
return {
from: safeSnapshotDates.value.from,
to: safeSnapshotDates.value.to,
slice: selectLabels[selectedLabelIndex.value].value
}
});
const visitsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/visits`, {
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse,
lazy: true, immediate: false
});
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events`, {
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse,
lazy: true, immediate: false
});
const sessionsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions`, {
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse,
lazy: true, immediate: false
});
const readyToDisplay = computed(() => {
return !visitsData.pending.value && !eventsData.pending.value && !sessionsData.pending.value;
});
watch(readyToDisplay, () => {
if (readyToDisplay.value === true) onDataReady();
})
function createGradient(startColor: string) {
const c = document.createElement('canvas');
const ctx = c.getContext("2d");
let gradient: any = `${startColor}22`;
if (ctx) {
gradient = ctx.createLinearGradient(0, 25, 0, 300);
gradient.addColorStop(0, `${startColor}99`);
gradient.addColorStop(0.35, `${startColor}66`);
gradient.addColorStop(1, `${startColor}22`);
} else {
console.warn('Cannot get context for gradient');
}
return gradient;
}
function onDataReady() {
console.log('DATA READY');
if (!visitsData.data.value) return;
if (!eventsData.data.value) return;
if (!sessionsData.data.value) return;
console.log('DATA READY 2');
chartData.value.labels = visitsData.data.value.labels;
const maxChartY = Math.max(...visitsData.data.value.data, ...sessionsData.data.value.data);
const maxEventSize = Math.max(...eventsData.data.value.data)
chartData.value.datasets[0].data = visitsData.data.value.data;
chartData.value.datasets[1].data = sessionsData.data.value.data;
chartData.value.datasets[2].data = eventsData.data.value.data.map(e => {
return { x: 0, y: maxChartY + 70, r: 25 / maxEventSize * e, r2: e }
});
chartData.value.datasets[0].backgroundColor = [createGradient('#5655d7')];
chartData.value.datasets[1].backgroundColor = [createGradient('#4abde8')];
chartData.value.datasets[2].backgroundColor = [createGradient('#fbbf24')];
console.log('UPDATE CHART');
updateChart();
}
const currentTooltipData = ref<[number, number, number, string]>([0, 0, 0, '']);
function onLegendChange(dataset: any, index: number, checked: any) {
dataset.hidden = !checked;
}
const legendColors = [
'#5655d7',
'#4abde8',
'#fbbf24'
]
onMounted(async () => {
visitsData.execute();
eventsData.execute();
sessionsData.execute();
});
</script>
<template>
<CardTitled title="Trend chart" sub="Easily match Visits, Unique sessions and Events trends." class="w-full">
<template #header>
<SelectButton class="w-fit" @changeIndex="selectedLabelIndex = $event" :currentIndex="selectedLabelIndex"
:options="selectLabels">
</SelectButton>
</template>
<div class="flex gap-6 w-full justify-end">
<div v-for="(dataset, index) of chartData.datasets" class="flex gap-2 items-center text-[.9rem]">
<UCheckbox :ui="{
color: `text-[${legendColors[index]}]`
}" :model-value="true" @change="onLegendChange(dataset, index, $event)"></UCheckbox>
<label class="mt-[2px]"> {{ dataset.label }} </label>
</div>
</div>
<div id='external-tooltip' ref="externalTooltipElement" class="z-[400]">
<LyxUiCard>
<div class="flex gap-2 items-center">
<div> Date: </div>
<div v-if="currentTooltipData"> {{ currentTooltipData[3] }}</div>
</div>
<div v-for="(dataset, index) of chartData.datasets" class="flex gap-2 items-center">
<div :style="`background-color: ${legendColors[index]}`" class="h-4 w-4 rounded-full">
</div>
<div> {{ dataset.label }}</div>
<div v-if="currentTooltipData" class="grow text-right px-4">
{{ currentTooltipData[index] }}
</div>
</div>
<!-- <div class="bg-lyx-background-lighter h-[2px] w-full my-2"> </div> -->
</LyxUiCard>
</div>
<div v-if="!readyToDisplay" class="flex justify-center py-40">
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
</div>
<div class="flex flex-col items-end" v-if="readyToDisplay">
<LineChart ref="lineChartRef" class="w-full h-full" v-bind="lineChartProps"> </LineChart>
</div>
</CardTitled>
</template>
<style lang="scss" scoped>
#external-tooltip {
border-radius: 3px;
color: white;
opacity: 0;
pointer-events: none;
position: absolute;
transform: translate(-50%, 0);
transition: all .1s ease;
}
</style>

View File

@@ -38,7 +38,7 @@ async function analyzeEvent() {
<template> <template>
<CardTitled title="Event User Flow" <CardTitled title="Event User Flow"
sub="Track your user's journey from external links to custom events within your platform." class="w-full p-4"> sub="Track your user's journey from external links to in-app events, maintaining a complete view of their path from entry to engagement." class="w-full p-4">
<div class="p-2 flex flex-col gap-3"> <div class="p-2 flex flex-col gap-3">
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full" <USelectMenu searchable searchable-placeholder="Search an event..." class="w-full"

View File

@@ -1,185 +1,201 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { PricingCardProp } from './PricingCardGeneric.vue'; import type { PricingCardProp } from './PricingCardGeneric.vue';
const props = defineProps<{ currentSub: number }>();
const freePricing: PricingCardProp[] = [
{
title: 'Free',
price: '€0 / mo',
subs: [
'Up to 5000 visits/events per month',
'CPM 0€ per visit/event'
],
features: [
'Email support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 10',
'Server type: SHARED',
'Data retention: 2 Months'
],
cta: 'Start For Free now!',
active: props.currentSub == 0,
isDowngrade: props.currentSub > 0,
planId: 0
},
]
const customPricing: PricingCardProp[] = [ const { data: planData, refresh: refreshPlanData } = useFetch('/api/project/plan', {
{ ...signHeaders(),
title: 'Enterprise', lazy: true
price: 'Custom', });
subs: [
'Unlimited visits/events per month', const activeProject = useActiveProject();
'Service Tailor-made on needs'
], watch(activeProject, () => {
features: [ refreshPlanData();
'Priority support', });
'Server type: DEDICATED',
'DB instance: DEDICATED',
'Dedicated operator', function getPricingsData() {
'White label',
'Custom Data Aggregation' const freePricing: PricingCardProp[] = [
], {
cta: 'Let\'s Talk!', title: 'Free',
link: 'mailto:help@litlyx.com', price: '€0 / mo',
active: false, subs: [
isDowngrade: false, 'Up to 5000 visits/events per month',
planId: -1 'CPM 0€ per visit/event'
} ],
] features: [
'Email support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 10',
'Server type: SHARED',
'Data retention: 2 Months'
],
cta: 'Start For Free now!',
active: (planData.value?.premium_type || 0) == 0,
isDowngrade: (planData.value?.premium_type || 0) > 0,
planId: 0
},
]
const customPricing: PricingCardProp[] = [
{
title: 'Enterprise',
price: 'Custom',
subs: [
'Unlimited visits/events per month',
'Service Tailor-made on needs'
],
features: [
'Priority support',
'Server type: DEDICATED',
'DB instance: DEDICATED',
'Dedicated operator',
'White label',
'Custom Data Aggregation'
],
cta: 'Let\'s Talk!',
link: 'mailto:help@litlyx.com',
active: false,
isDowngrade: false,
planId: -1
}
]
const slidePricings: PricingCardProp[] = [
{
title: 'Incubation',
price: '€4,99 / mo',
subs: [
'Up to 50.000 visits/events per month',
'CPM 0,10€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 30',
'Server type: SHARED',
'Data retention: 6 Months'
],
cta: 'Go to Cloud Dashboard',
active: (planData.value?.premium_type || 0) == 101,
isDowngrade: (planData.value?.premium_type || 0) > 101,
planId: 101
},
{
title: 'Acceleration',
price: '€9,99 / mo',
subs: [
'Up to 150.000 visits/events per month',
'CPM 0,06€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 100',
'Server type: SHARED',
'Data retention: 9 Months'
],
cta: 'Go to Cloud Dashboard',
active: (planData.value?.premium_type || 0) == 102,
isDowngrade: (planData.value?.premium_type || 0) > 102,
planId: 102
},
{
title: 'Growth',
price: '€29,99 / mo',
subs: [
'Up to 500.000 visits/events per month',
'CPM 0,059€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 3.000',
'Server type: SHARED',
'Data retention: 1 Year'
],
cta: 'Go to Cloud Dashboard',
active: (planData.value?.premium_type || 0) == 103,
isDowngrade: (planData.value?.premium_type || 0) > 103,
planId: 103
},
{
title: 'Expansion',
price: '€59,99 / mo',
subs: [
'Up to 1.000.000 visits/events per month',
'CPM 0,059€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 5.000',
'Server type: SHARED',
'Data retention: 1 Year'
],
cta: 'Go to Cloud Dashboard',
active: (planData.value?.premium_type || 0) == 104,
isDowngrade: (planData.value?.premium_type || 0) > 104,
planId: 104
},
{
title: 'Scaling',
price: '€99,99 / mo',
subs: [
'Up to 2.500.000 visits/events per month',
'CPM 0,039€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 10.000',
'Server type: DEDICATED',
'Data retention: 2 Years'
],
cta: 'Go to Cloud Dashboard',
active: (planData.value?.premium_type || 0) == 105,
isDowngrade: (planData.value?.premium_type || 0) > 105,
planId: 105
},
{
title: 'Unicorn',
price: '€149,99 / mo',
subs: [
'Up to 5.000.000 visits/events per month',
'CPM 0,029€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 20.000',
'Server type: DEDICATED',
'Data retention: 3 Years'
],
cta: 'Go to Cloud Dashboard',
active: (planData.value?.premium_type || 0) == 106,
isDowngrade: (planData.value?.premium_type || 0) > 106,
planId: 106
}
]
return { freePricing, customPricing, slidePricings }
}
const slidePricings: PricingCardProp[] = [
{
title: 'Incubation',
price: '€4,99 / mo',
subs: [
'Up to 50.000 visits/events per month',
'CPM 0,10€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 30',
'Server type: SHARED',
'Data retention: 6 Months'
],
cta: 'Go to Cloud Dashboard',
active: props.currentSub == 101,
isDowngrade: props.currentSub > 101,
planId: 101
},
{
title: 'Acceleration',
price: '€9,99 / mo',
subs: [
'Up to 150.000 visits/events per month',
'CPM 0,06€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 100',
'Server type: SHARED',
'Data retention: 9 Months'
],
cta: 'Go to Cloud Dashboard',
active: props.currentSub == 102,
isDowngrade: props.currentSub > 102,
planId: 102
},
{
title: 'Growth',
price: '€29,99 / mo',
subs: [
'Up to 500.000 visits/events per month',
'CPM 0,059€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 3.000',
'Server type: SHARED',
'Data retention: 1 Year'
],
cta: 'Go to Cloud Dashboard',
active: props.currentSub == 103,
isDowngrade: props.currentSub > 103,
planId: 103
},
{
title: 'Expansion',
price: '€59,99 / mo',
subs: [
'Up to 1.000.000 visits/events per month',
'CPM 0,059€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 5.000',
'Server type: SHARED',
'Data retention: 1 Year'
],
cta: 'Go to Cloud Dashboard',
active: props.currentSub == 104,
isDowngrade: props.currentSub > 104,
planId: 104
},
{
title: 'Scaling',
price: '€99,99 / mo',
subs: [
'Up to 2.500.000 visits/events per month',
'CPM 0,039€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 10.000',
'Server type: DEDICATED',
'Data retention: 2 Years'
],
cta: 'Go to Cloud Dashboard',
active: props.currentSub == 105,
isDowngrade: props.currentSub > 105,
planId: 105
},
{
title: 'Unicorn',
price: '€149,99 / mo',
subs: [
'Up to 5.000.000 visits/events per month',
'CPM 0,029€ per visit/event'
],
features: [
'Slack support',
'Unlimited domains',
'Unlimited reports',
'AI Tokens: 20.000',
'Server type: DEDICATED',
'Data retention: 3 Years'
],
cta: 'Go to Cloud Dashboard',
active: props.currentSub == 106,
isDowngrade: props.currentSub > 106,
planId: 106
}
]
const emits = defineEmits<{ const emits = defineEmits<{
(evt: 'onCloseClick'): void (evt: 'onCloseClick'): void
}>(); }>();
const activeProject = useActiveProject()
async function onLifetimeUpgradeClick() { async function onLifetimeUpgradeClick() {
const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create-onetime`, { const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create-onetime`, {
...signHeaders({ 'content-type': 'application/json' }), ...signHeaders({ 'content-type': 'application/json' }),
@@ -201,9 +217,9 @@ async function onLifetimeUpgradeClick() {
</div> </div>
<div class="flex gap-8 mt-10 h-max xl:flex-row flex-col"> <div class="flex gap-8 mt-10 h-max xl:flex-row flex-col">
<PricingCardGeneric class="flex-1" :datas="freePricing"></PricingCardGeneric> <PricingCardGeneric class="flex-1" :datas="getPricingsData().freePricing"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="slidePricings" :default-index="2"></PricingCardGeneric> <PricingCardGeneric class="flex-1" :datas="getPricingsData().slidePricings" :default-index="2"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="customPricing"></PricingCardGeneric> <PricingCardGeneric class="flex-1" :datas="getPricingsData().customPricing"></PricingCardGeneric>
</div> </div>
<LyxUiCard class="w-full mt-6"> <LyxUiCard class="w-full mt-6">

View File

@@ -1,11 +1,12 @@
import { Chart, registerables } from 'chart.js'; import { Chart, registerables } from 'chart.js';
import annotaionPlugin from 'chartjs-plugin-annotation';
let registered = false; let registered = false;
export async function registerChartComponents() { export async function registerChartComponents() {
if (registered) return; if (registered) return;
if (process.client) { if (process.client) {
Chart.register(...registerables); Chart.register(...registerables, annotaionPlugin);
registered = true; registered = true;
} }
} }

View File

@@ -16,6 +16,7 @@
"@getbrevo/brevo": "^2.2.0", "@getbrevo/brevo": "^2.2.0",
"@nuxtjs/tailwindcss": "^6.12.0", "@nuxtjs/tailwindcss": "^6.12.0",
"chart.js": "^3.9.1", "chart.js": "^3.9.1",
"chartjs-plugin-annotation": "^2.2.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"google-auth-library": "^9.9.0", "google-auth-library": "^9.9.0",

View File

@@ -115,24 +115,29 @@ function goToUpgrade() {
</div> </div>
<DashboardTopSection></DashboardTopSection> <!-- <DashboardTopSection></DashboardTopSection>
<DashboardTopCards :key="refreshKey"></DashboardTopCards> <DashboardTopCards :key="refreshKey"></DashboardTopCards> -->
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
</div>
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row"> <div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row">
<CardTitled :key="refreshKey" class="p-4 flex-1 w-full" title="Visits trends" <!-- <CardTitled :key="refreshKey" class="p-4 flex-1 w-full" title="Visits trends"
sub="Shows trends in page visits."> sub="Shows trends in page visits.">
<template #header> <template #header>
<SelectButton @changeIndex="mainChartSelectIndex = $event" :currentIndex="mainChartSelectIndex" <SelectButton @changeIndex="mainChartSelectIndex = $event" :currentIndex="mainChartSelectIndex"
:options="selectLabels"> :options="selectLabels">
</SelectButton> </SelectButton>
</template> </template>
<div> <div>
<DashboardVisitsLineChart :slice="(selectLabels[mainChartSelectIndex].value as any)"> <DashboardVisitsLineChart :slice="(selectLabels[mainChartSelectIndex].value as any)">
</DashboardVisitsLineChart> </DashboardVisitsLineChart>
</div> </div>
</CardTitled> </CardTitled> -->
<!-- <CardTitled :key="refreshKey" class="p-4 flex-1 w-full" title="Sessions" <!-- <CardTitled :key="refreshKey" class="p-4 flex-1 w-full" title="Sessions"
sub="Shows trends in sessions."> sub="Shows trends in sessions.">
@@ -149,7 +154,7 @@ function goToUpgrade() {
</div> </div>
<div class="flex w-full justify-center mt-6 px-6"> <!-- <div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col xl:flex-row"> <div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1"> <div class="flex-1">
<DashboardWebsitesBarCard :key="refreshKey"></DashboardWebsitesBarCard> <DashboardWebsitesBarCard :key="refreshKey"></DashboardWebsitesBarCard>
@@ -160,7 +165,6 @@ function goToUpgrade() {
</div> </div>
</div> </div>
<div class="flex w-full justify-center mt-6 px-6"> <div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col xl:flex-row"> <div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1"> <div class="flex-1">
@@ -191,7 +195,7 @@ function goToUpgrade() {
<div class="flex-1"> <div class="flex-1">
</div> </div>
</div> </div>
</div> </div> -->
</div> </div>

View File

@@ -17,6 +17,9 @@ importers:
chart.js: chart.js:
specifier: ^3.9.1 specifier: ^3.9.1
version: 3.9.1 version: 3.9.1
chartjs-plugin-annotation:
specifier: ^2.2.1
version: 2.2.1(chart.js@3.9.1)
date-fns: date-fns:
specifier: ^3.6.0 specifier: ^3.6.0
version: 3.6.0 version: 3.6.0
@@ -1425,20 +1428,20 @@ packages:
'@vue/reactivity@3.4.27': '@vue/reactivity@3.4.27':
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==} resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
'@vue/reactivity@3.4.38': '@vue/reactivity@3.5.4':
resolution: {integrity: sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==} resolution: {integrity: sha512-HKKbEuP7tYSGCq4e4nK6ZW6l5hyG66OUetefBp4budUyjvAYsnQDf+bgFzg2RAgnH0CInyqXwD9y47jwJEHrQw==}
'@vue/runtime-core@3.4.27': '@vue/runtime-core@3.4.27':
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==} resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
'@vue/runtime-core@3.4.38': '@vue/runtime-core@3.5.4':
resolution: {integrity: sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==} resolution: {integrity: sha512-f3ek2sTA0AFu0n+w+kCtz567Euqqa3eHewvo4klwS7mWfSj/A+UmYTwsnUFo35KeyAFY60JgrCGvEBsu1n/3LA==}
'@vue/runtime-dom@3.4.27': '@vue/runtime-dom@3.4.27':
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==} resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
'@vue/runtime-dom@3.4.38': '@vue/runtime-dom@3.5.4':
resolution: {integrity: sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==} resolution: {integrity: sha512-ofyc0w6rbD5KtjhP1i9hGOKdxGpvmuB1jprP7Djlj0X7R5J/oLwuNuE98GJ8WW31Hu2VxQHtk/LYTAlW8xrJdw==}
'@vue/server-renderer@3.4.27': '@vue/server-renderer@3.4.27':
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==} resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
@@ -1448,11 +1451,8 @@ packages:
'@vue/shared@3.4.27': '@vue/shared@3.4.27':
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==} resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
'@vue/shared@3.4.34': '@vue/shared@3.5.4':
resolution: {integrity: sha512-x5LmiRLpRsd9KTjAB8MPKf0CDPMcuItjP0gbNqFCIgL1I8iYp4zglhj9w9FPCdIbHG2M91RVeIbArFfFTz9I3A==} resolution: {integrity: sha512-L2MCDD8l7yC62Te5UUyPVpmexhL9ipVnYRw9CsWfm/BGRL5FwDX4a25bcJ/OJSD3+Hx+k/a8LDKcG2AFdJV3BA==}
'@vue/shared@3.4.38':
resolution: {integrity: sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==}
'@vueuse/components@10.10.0': '@vueuse/components@10.10.0':
resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==} resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==}
@@ -1826,6 +1826,11 @@ packages:
chart.js@3.9.1: chart.js@3.9.1:
resolution: {integrity: sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==} resolution: {integrity: sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==}
chartjs-plugin-annotation@2.2.1:
resolution: {integrity: sha512-RL9UtrFr2SXd7C47zD0MZqn6ZLgrcRt3ySC6cYal2amBdANcYB1QcwFXcpKWAYnO4SGJYRok7P5rKDDNgJMA/w==}
peerDependencies:
chart.js: '>=3.7.0'
check-error@1.0.3: check-error@1.0.3:
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
@@ -6990,7 +6995,7 @@ snapshots:
'@volar/language-core': 1.11.1 '@volar/language-core': 1.11.1
'@volar/source-map': 1.11.1 '@volar/source-map': 1.11.1
'@vue/compiler-dom': 3.4.27 '@vue/compiler-dom': 3.4.27
'@vue/shared': 3.4.34 '@vue/shared': 3.5.4
computeds: 0.0.1 computeds: 0.0.1
minimatch: 9.0.4 minimatch: 9.0.4
muggle-string: 0.3.1 muggle-string: 0.3.1
@@ -7003,19 +7008,19 @@ snapshots:
dependencies: dependencies:
'@vue/shared': 3.4.27 '@vue/shared': 3.4.27
'@vue/reactivity@3.4.38': '@vue/reactivity@3.5.4':
dependencies: dependencies:
'@vue/shared': 3.4.38 '@vue/shared': 3.5.4
'@vue/runtime-core@3.4.27': '@vue/runtime-core@3.4.27':
dependencies: dependencies:
'@vue/reactivity': 3.4.27 '@vue/reactivity': 3.4.27
'@vue/shared': 3.4.27 '@vue/shared': 3.4.27
'@vue/runtime-core@3.4.38': '@vue/runtime-core@3.5.4':
dependencies: dependencies:
'@vue/reactivity': 3.4.38 '@vue/reactivity': 3.5.4
'@vue/shared': 3.4.38 '@vue/shared': 3.5.4
'@vue/runtime-dom@3.4.27': '@vue/runtime-dom@3.4.27':
dependencies: dependencies:
@@ -7023,11 +7028,11 @@ snapshots:
'@vue/shared': 3.4.27 '@vue/shared': 3.4.27
csstype: 3.1.3 csstype: 3.1.3
'@vue/runtime-dom@3.4.38': '@vue/runtime-dom@3.5.4':
dependencies: dependencies:
'@vue/reactivity': 3.4.38 '@vue/reactivity': 3.5.4
'@vue/runtime-core': 3.4.38 '@vue/runtime-core': 3.5.4
'@vue/shared': 3.4.38 '@vue/shared': 3.5.4
csstype: 3.1.3 csstype: 3.1.3
'@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))': '@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))':
@@ -7038,9 +7043,7 @@ snapshots:
'@vue/shared@3.4.27': {} '@vue/shared@3.4.27': {}
'@vue/shared@3.4.34': {} '@vue/shared@3.5.4': {}
'@vue/shared@3.4.38': {}
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))': '@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
dependencies: dependencies:
@@ -7429,6 +7432,10 @@ snapshots:
chart.js@3.9.1: {} chart.js@3.9.1: {}
chartjs-plugin-annotation@2.2.1(chart.js@3.9.1):
dependencies:
chart.js: 3.9.1
check-error@1.0.3: check-error@1.0.3:
dependencies: dependencies:
get-func-name: 2.0.2 get-func-name: 2.0.2
@@ -11028,8 +11035,8 @@ snapshots:
vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)): vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)):
dependencies: dependencies:
'@vue/runtime-core': 3.4.38 '@vue/runtime-core': 3.5.4
'@vue/runtime-dom': 3.4.38 '@vue/runtime-dom': 3.5.4
chart.js: 3.9.1 chart.js: 3.9.1
csstype: 3.1.3 csstype: 3.1.3
lodash-es: 4.17.21 lodash-es: 4.17.21

View File

@@ -4,6 +4,8 @@ import type Event from 'stripe';
import { ProjectModel } from '@schema/ProjectSchema'; import { ProjectModel } from '@schema/ProjectSchema';
import { PREMIUM_DATA, PREMIUM_PLAN, getPlanFromId, getPlanFromPrice, getPlanFromTag } from '@data/PREMIUM'; import { PREMIUM_DATA, PREMIUM_PLAN, getPlanFromId, getPlanFromPrice, getPlanFromTag } from '@data/PREMIUM';
import { ProjectLimitModel } from '@schema/ProjectsLimits'; import { ProjectLimitModel } from '@schema/ProjectsLimits';
import EmailService from '@services/EmailService'
import { UserModel } from '@schema/UserSchema';
@@ -87,6 +89,14 @@ async function onPaymentOnetimeSuccess(event: Event.PaymentIntentSucceededEvent)
await addSubscriptionToProject(project._id.toString(), PLAN, subscription.id, subscription.current_period_start, subscription.current_period_end) await addSubscriptionToProject(project._id.toString(), PLAN, subscription.id, subscription.current_period_start, subscription.current_period_end)
const user = await UserModel.findOne({ _id: project.owner });
if (!user) return { ok: false, error: 'USER NOT EXIST FOR PROJECT' + project.id }
setTimeout(() => {
EmailService.sendPurchaseEmail(user.email);
}, 1);
return { ok: true }; return { ok: true };
} }
@@ -125,6 +135,13 @@ async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, currentSubscription.current_period_start, currentSubscription.current_period_end) await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, currentSubscription.current_period_start, currentSubscription.current_period_end)
const user = await UserModel.findOne({ _id: project.owner });
if (!user) return { ok: false, error: 'USER NOT EXIST FOR PROJECT' + project.id }
setTimeout(() => {
EmailService.sendPurchaseEmail(user.email);
}, 1);
return { ok: true }; return { ok: true };

View File

@@ -3,6 +3,7 @@ import { WELCOME_EMAIL } from './email_templates/WelcomeEmail';
import { LIMIT_50_EMAIL } from './email_templates/Limit50Email'; import { LIMIT_50_EMAIL } from './email_templates/Limit50Email';
import { LIMIT_90_EMAIL } from './email_templates/Limit90Email'; import { LIMIT_90_EMAIL } from './email_templates/Limit90Email';
import { LIMIT_MAX_EMAIL } from './email_templates/LimitMaxEmail'; import { LIMIT_MAX_EMAIL } from './email_templates/LimitMaxEmail';
import { PURCHASE_EMAIL } from './email_templates/PurchaseEmail';
class EmailService { class EmailService {
@@ -17,7 +18,7 @@ class EmailService {
try { try {
const sendSmtpEmail = new SendSmtpEmail(); const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "You've reached 50% limit on Litlyx"; sendSmtpEmail.subject = "You've reached 50% limit on Litlyx";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" }; sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }]; sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = LIMIT_50_EMAIL sendSmtpEmail.htmlContent = LIMIT_50_EMAIL
@@ -36,7 +37,7 @@ class EmailService {
try { try {
const sendSmtpEmail = new SendSmtpEmail(); const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "You've reached 90% limit on Litlyx"; sendSmtpEmail.subject = "You've reached 90% limit on Litlyx";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" }; sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }]; sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = LIMIT_90_EMAIL sendSmtpEmail.htmlContent = LIMIT_90_EMAIL
.replace(/\[Project Name\]/, projectName) .replace(/\[Project Name\]/, projectName)
@@ -53,7 +54,7 @@ class EmailService {
try { try {
const sendSmtpEmail = new SendSmtpEmail(); const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "You've reached your limit on Litlyx!"; sendSmtpEmail.subject = "You've reached your limit on Litlyx!";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" }; sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }]; sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = LIMIT_MAX_EMAIL sendSmtpEmail.htmlContent = LIMIT_MAX_EMAIL
.replace(/\[Project Name\]/, projectName) .replace(/\[Project Name\]/, projectName)
@@ -70,7 +71,7 @@ class EmailService {
try { try {
const sendSmtpEmail = new SendSmtpEmail(); const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "Welcome to Litlyx!"; sendSmtpEmail.subject = "Welcome to Litlyx!";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" }; sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }]; sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = WELCOME_EMAIL; sendSmtpEmail.htmlContent = WELCOME_EMAIL;
await this.apiInstance.sendTransacEmail(sendSmtpEmail); await this.apiInstance.sendTransacEmail(sendSmtpEmail);
@@ -81,6 +82,21 @@ class EmailService {
} }
} }
async sendPurchaseEmail(target: string) {
try {
const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "Thank You for Upgrading Your Litlyx Plan!";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = PURCHASE_EMAIL;
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
return true;
} catch (ex) {
console.error('ERROR SENDING EMAIL', ex);
return false;
}
}
} }
const instance = new EmailService(); const instance = new EmailService();

View File

@@ -36,7 +36,11 @@ export class RedisStreamService {
setTimeout(() => this.readingLoop(options, processFunction), 1); setTimeout(() => this.readingLoop(options, processFunction), 1);
return; return;
} }
await processFunction(result); try {
await processFunction(result);
} catch (ex) {
console.error('Error on processing function');
}
RedisStreamService.processed++; RedisStreamService.processed++;
await new Promise(r => setTimeout(r, options.delay?.base || 100)); await new Promise(r => setTimeout(r, options.delay?.base || 100));
setTimeout(() => this.readingLoop(options, processFunction), 1); setTimeout(() => this.readingLoop(options, processFunction), 1);

View File

@@ -0,0 +1,45 @@
export const PURCHASE_EMAIL = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thank You for Upgrading Your Litlyx Plan!</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<!-- Email Content -->
<p>Dear User,</p>
<p>We are thrilled to inform you that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has successfully been upgraded to a higher plan! Thank you for choosing to elevate your experience with us and for believing in our project.</p>
<p>We appreciate your trust in Litlyx and are committed to providing you with the best analytics experience. Your support helps us to continually improve our platform and bring new features to make your analytics journey even better.</p>
<p>You can find your current plan details and download your invoices under <strong>Settings > Billing Tab</strong>.</p>
<h3>What does this mean for you?</h3>
<p>With your upgraded plan, you can now enjoy more data collection, advanced features, and additional benefits to enhance your data analysis capabilities:</p>
<ol>
<li>Access to more storage and increased data limits.</li>
<li>Advanced analytics tools like IP-Company Matching, download CSV for your raw data, AI insights, and more.</li>
<li>Priority support to help you make the most of your Litlyx experience on Slack or Discord!</li>
</ol>
<p>If you have any questions about your new plan or need assistance, feel free to reach out to our support team at <a href="mailto:help@litlyx.com" style="color: #28a745; text-decoration: none;"><strong>help@litlyx.com</strong></a>. Were here to help you make the most out of your upgraded plan!</p>
<p><strong>Thank you for using Litlyx every day as your analytics tool and for being a part of our journey.</strong></p>
<p>We look forward to continuing to support your growth and success!</p>
<p>Best regards,</p>
<p>Antonio,</p>
<p>CEO | Litlyx</p>
</body>
</html>
`

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>❗️ Anomaly detected by our AI</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<!-- Email Content -->
<h2 style="color: #D32F2F;">❗️ Anomaly detected by our AI</h2>
<p>Dear User,</p>
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has an anomaly that our AI agent detected. Here is the report:</p>
<p><strong>Anomaly:</strong> Suspicious DNS</p>
<p><strong>Message:</strong> [Suspicious DNS name] is logging data in your project. Is that you?</p>
<p><strong>Date:</strong> Current date!</p>
<p>You can analyze the suspicious DNS on your Litlyx dashboard. We put a symbol next to each suspicious DNS to let users know something might be wrong!</p>
<h3>What can I do?</h3>
<p>To resolve this issue, you should reach out to the webmasters of the websites that have duplicated your content and request them to remove it or give you proper attribution (if available).</p>
<p>You can also use <a href="https://www.whois.com/whois/" style="color: #D32F2F; text-decoration: none;">https://www.whois.com/whois/</a> to get the contact details of the webmaster or domain owner.</p>
<p>If webmasters don't respond or cooperate, <strong>you can file a DMCA complaint here:</strong> <a href="https://support.google.com/legal/answer/3110420?hl=en" style="color: #D32F2F; text-decoration: none;">https://support.google.com/legal/answer/3110420?hl=en</a> <strong>with Google to request the removal of the duplicate content from their search results.</strong></p>
<h3>Please refer to this for more information:</h3>
<ul>
<li><a href="https://support.google.com/legal/answer/3110420?hl=en&sjid=14235884554806745995-AP&authuser=2" style="color: #D32F2F; text-decoration: none;">Report Content for Legal Reasons</a></li>
<li><a href="https://www.dmca.com/FAQ/How-can-I-get-a-webpage-removed-from-Google-search-results" style="color: #D32F2F; text-decoration: none;">How can I get a webpage removed from Google search results?</a></li>
</ul>
<p>Your safety is our main priority.</p>
<p>Thank you for choosing Litlyx every day as your analytics tool!</p>
<p>Antonio,</p>
<p>CEO | Litlyx</p>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🚨 Unexpected Activity Detected by our AI</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<!-- Email Content -->
<h2 style="color: #D32F2F;">🚨 Unexpected Activity Detected by our AI</h2>
<p>Dear User,</p>
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> is receiving an unexpected amount of visits or events. This could indicate unusual activity that might require your attention.</p>
<p>If this spike in activity is expected, theres no need to worry. However, if you believe this could be unexpected or suspicious, we recommend taking a closer look at your data on the <strong>Litlyx Dashboard</strong>. You can analyze your recent traffic and event logs to identify any irregularities or potential issues.</p>
<h3>What can I do?</h3>
<p>To better understand the situation, you can:</p>
<ol>
<li>Review your traffic sources to see where the visits or events are coming from.</li>
<li>Check for any unexpected patterns, such as a high number of visits from unknown sources or abnormal event triggers.</li>
<li>Check your code to find bugs on a specific action that is triggered in loops.</li>
</ol>
<p>If you need help understanding this activity or have any concerns, feel free to reach out to our support team at <a href="mailto:help@litlyx.com" style="color: #D32F2F; text-decoration: none;"><strong>help@litlyx.com</strong></a>. We are here to assist you!</p>
<p><strong>Your safety and data integrity are our top priorities.</strong></p>
<p>Thank you for trusting Litlyx as your analytics tool!</p>
<p>Best regards,</p>
<p>Antonio,</p>
<p>CEO | Litlyx</p>
</body>
</html>

View File

@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Youve reached 50% limit on Litlyx</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<!-- Email Content -->
<h2 style="color: #FF5733;">Youve reached 50% limit on Litlyx</h2>
<p>Dear User,</p>
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has reached 50% of its data collection limit for this month.</p>
<p>To avoid losing precious data, please remember to monitor your usage on the <strong>Litlyx Dashboard</strong>. You can find your current usage details under <strong>Settings > Billing Tab</strong>.</p>
<p>If you need more data collection storage, you may consider upgrading your plan to get additional benefits and ensure uninterrupted data collection.</p>
<p>Feel free to reply to this email or contact us at <a href="mailto:help@litlyx.com" style="color: #FF5733; text-decoration: none;">help@litlyx.com</a> if you have any questions or need assistance.</p>
<p>Thank you for choosing Litlyx every day as your analytics tool.</p>
<p>Have a nice day!</p>
<p>Antonio,</p>
<p>CEO | Litlyx</p>
</body>
</html>

View File

@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Youve reached 90% limit on Litlyx</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<!-- Email Content -->
<h2 style="color: #FF0000;">Youve reached 90% limit on Litlyx</h2>
<p>Dear User,</p>
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has reached 90% of its data collection limit for this month.</p>
<p>To avoid losing precious data, please remember to monitor your usage on the <strong>Litlyx Dashboard</strong>. You can find your current usage details under <strong>Settings > Billing Tab</strong>.</p>
<p>If you need more data collection storage, you may consider upgrading your plan to get additional benefits and ensure uninterrupted data collection.</p>
<p>Feel free to reply to this email or contact us at <a href="mailto:help@litlyx.com" style="color: #FF0000; text-decoration: none;">help@litlyx.com</a> if you have any questions or need assistance.</p>
<p>Thank you for choosing Litlyx every day as your analytics tool.</p>
<p>Have a nice day!</p>
<p>Antonio,</p>
<p>CEO | Litlyx</p>
</body>
</html>

View File

@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>❗️ Youve reached your limit on Litlyx!</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<!-- Email Content -->
<h2 style="color: #D32F2F;">❗️ Youve reached your limit on Litlyx!</h2>
<p>Dear User,</p>
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has reached the limit of your current plan.</p>
<p>To avoid losing precious data, please remember to monitor your usage on the <strong>Litlyx Dashboard</strong>. You can find your current usage details under <strong>Settings > Billing Tab</strong>.</p>
<p>If you need more data collection storage, you may consider upgrading your plan to get additional benefits and ensure uninterrupted data collection.</p>
<p>Feel free to reply to this email or contact us at <a href="mailto:help@litlyx.com" style="color: #D32F2F; text-decoration: none;">help@litlyx.com</a> if you have any questions or need assistance.</p>
<p>Thank you for choosing Litlyx every day as your analytics tool.</p>
<p>Have a nice day!</p>
<p>Antonio,</p>
<p>CEO | Litlyx</p>
</body>
</html>

View File

@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Litlyx!</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<!-- Subject -->
<h2 style="color: #007BFF;">Welcome to Litlyx!</h2>
<p>Were happy to have you onboard,</p>
<p>At Litlyx, were committed to creating the best analytics collection experience for everybody, starting from developers.</p>
<p>Here are a few things you can do to get started tracking analytics today:</p>
<ol>
<li><strong><a href="https://dashboard.litlyx.com" style="color: #007BFF; text-decoration: none;">Create a new project</a></strong> by just naming it</li>
<li><strong><a style="color: #0a0a0a; text-decoration: none;">Copy the universal Script</a></strong> we provide you the snippets to copy in your index.html file and start instantly to track metrics on your website or web app.</li>
<li><strong><a style="color: #0a0a0a; text-decoration: none;">Third Step</a></strong> Encourage engagement or interaction.</li>
</ol>
<p>If you have any questions or need support, visit <a href="http://docs.litlyx.com" style="color: #007BFF;">docs.litlyx.com</a>.</p>
<p>Feel free to reply to this email or reach out to our team at <a href="mailto:help@litlyx.com" style="color: #007BFF;">help@litlyx.com</a>. Were here to help!</p>
<p>Link to Discord for developer support: <a href="https://discord.com/invite/9cQykjsmWX" style="color: #007BFF;">https://discord.com/invite/9cQykjsmWX</a></p>
<p>Thank you for joining us, and we look forward to seeing you around.</p>
<p>We want to make analytics the freshest thing on the web.</p>
<p>Antonio,</p>
<p>CEO | Litlyx</p>
</body>
</html>