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

@@ -10,7 +10,7 @@ export type Entry = {
icon?: string,
action?: () => any,
adminOnly?: boolean,
premiumOnly?:boolean,
premiumOnly?: boolean,
external?: boolean,
grow?: boolean
}
@@ -117,10 +117,13 @@ watch(selected, () => {
setActiveProject(selected.value._id.toString())
})
const isPremium = computed(()=>{
const isPremium = computed(() => {
return activeProject.value?.premium;
})
const pricingDrawer = usePricingDrawer();
</script>
<template>
@@ -131,6 +134,13 @@ const isPremium = computed(()=>{
}">
<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 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>
<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">
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full"

View File

@@ -1,185 +1,201 @@
<script lang="ts" setup>
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[] = [
{
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 { data: planData, refresh: refreshPlanData } = useFetch('/api/project/plan', {
...signHeaders(),
lazy: true
});
const activeProject = useActiveProject();
watch(activeProject, () => {
refreshPlanData();
});
function getPricingsData() {
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: (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<{
(evt: 'onCloseClick'): void
}>();
const activeProject = useActiveProject()
async function onLifetimeUpgradeClick() {
const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create-onetime`, {
...signHeaders({ 'content-type': 'application/json' }),
@@ -201,9 +217,9 @@ async function onLifetimeUpgradeClick() {
</div>
<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="slidePricings" :default-index="2"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="customPricing"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="getPricingsData().freePricing"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="getPricingsData().slidePricings" :default-index="2"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="getPricingsData().customPricing"></PricingCardGeneric>
</div>
<LyxUiCard class="w-full mt-6">