12 Commits

Author SHA1 Message Date
Emily
6b5d23566c fix SELFHOST env on docker-compose 2025-01-17 18:07:53 +01:00
Emily
dbcda95823 fix selfhost 2025-01-17 17:40:20 +01:00
Emily
fb89c87489 fix selfhost 2025-01-17 16:44:22 +01:00
Emily
b59eea47e9 active snapshot on creation 2025-01-17 16:44:16 +01:00
Emily
473331047d fix dockercompose 2025-01-16 18:21:59 +01:00
Emily
5af77ff63e update docker-compose 2025-01-16 18:16:16 +01:00
Emily
e6e2340432 fix lightmode 2025-01-16 16:47:35 +01:00
Emily
0b90c2fe3c add lightmode to alerts 2025-01-15 16:34:10 +01:00
Emily
a6d1797a4f add lightmode 2025-01-15 16:31:10 +01:00
Emily
d1abe1a91f change packages 2025-01-15 14:45:02 +01:00
Emily
b733cd2a68 navbar lightmode 2025-01-14 17:38:33 +01:00
Emily
88ebfc188c add password reset + password change 2025-01-13 17:01:34 +01:00
58 changed files with 1560 additions and 340 deletions

View File

@@ -1,5 +0,0 @@
{
"files.exclude": {
"**/node_modules": true
}
}

View File

@@ -1,30 +0,0 @@
FROM node:21-alpine as base
FROM base as build
RUN npm i -g pnpm
RUN npm i -g pm2
# COPY --link dashboard/package.json dashboard/pnpm-lock.yaml ./
# RUN npm install --production=false
WORKDIR /home/app
COPY --link dashboard ./dashboard
COPY --link lyx-ui ./lyx-ui
COPY --link consumer ./consumer
COPY --link producer ./producer
COPY --link shared ./shared
WORKDIR /home/app/producer
RUN pnpm install
WORKDIR /home/app/consumer
RUN pnpm install
WORKDIR /home/app/dashboard
RUN pnpm install
RUN pnpm run dev
# CMD [ "node", "/home/app/.output/server/index.mjs" ]

View File

@@ -16,18 +16,18 @@ const { drawerVisible, hideDrawer, drawerClasses } = useDrawer();
<template> <template>
<div class="w-dvw h-dvh bg-lyx-background-light relative"> <div class="w-dvw h-dvh bg-lyx-lightmode-background-light dark:bg-lyx-background-light relative">
<Transition name="drawer"> <Transition name="drawer">
<LazyDrawerGeneric @onCloseClick="hideDrawer()" :class="drawerClasses" <LazyDrawerGeneric @onCloseClick="hideDrawer()" :class="drawerClasses"
class="bg-black fixed right-0 top-0 w-full xl:w-[60vw] xl:min-w-[65rem] h-full z-[20]" v-if="drawerVisible"> class="bg-lyx-lightmode-background-light dark:bg-black fixed right-0 top-0 w-full xl:w-[60vw] xl:min-w-[65rem] h-full z-[20]" v-if="drawerVisible">
</LazyDrawerGeneric> </LazyDrawerGeneric>
</Transition> </Transition>
<div class="fixed top-4 right-8 z-[999] flex flex-col gap-2" v-if="alerts.length > 0"> <div class="fixed top-4 right-8 z-[999] flex flex-col gap-2" v-if="alerts.length > 0">
<div v-for="alert of alerts" <div v-for="alert of alerts"
class="w-[30vw] min-w-[20rem] relative bg-[#151515] overflow-hidden border-solid border-[2px] border-[#262626] rounded-lg p-6 drop-shadow-lg"> class="w-[30vw] min-w-[20rem] relative bg-lyx-lightmode-background dark:bg-[#151515] overflow-hidden border-solid border-[2px] border-lyx-lightmode-widget dark:border-[#262626] rounded-lg p-6 drop-shadow-lg">
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div> <i :class="alert.icon"></i> </div> <div> <i :class="alert.icon"></i> </div>
<div class="grow"> <div class="grow">
@@ -56,8 +56,8 @@ const { drawerVisible, hideDrawer, drawerClasses } = useDrawer();
</div> </div>
<div v-if="showDialog" <div v-if="showDialog"
class="custom-dialog w-full h-full flex items-center justify-center lg:pl-32 lg:p-20 p-4 absolute left-0 top-0 z-[100] backdrop-blur-[2px] bg-black/50"> class="custom-dialog w-full h-full flex items-center justify-center lg:pl-32 lg:p-20 p-4 absolute left-0 top-0 z-[100] backdrop-blur-[2px] dark:bg-black/50">
<div :style="dialogStyle" class="bg-lyx-widget rounded-xl relative outline outline-1 outline-lyx-widget-lighter"> <div :style="dialogStyle" class="bg-lyx-lightmode-widget-light outline-lyx-lightmode-widget dark:bg-lyx-widget dark:outline-lyx-widget-lighter rounded-xl relative outline outline-1">
<div v-if="dialogClosable" class="flex justify-end absolute z-[100] right-8 top-8"> <div v-if="dialogClosable" class="flex justify-end absolute z-[100] right-8 top-8">
<i @click="closeDialog()" class="fas fa-close text-[1.6rem] hover:text-gray-500 cursor-pointer"></i> <i @click="closeDialog()" class="fas fa-close text-[1.6rem] hover:text-gray-500 cursor-pointer"></i>
</div> </div>

View File

@@ -54,7 +54,7 @@ function openExternalLink(link: string) {
<div class="flex justify-between mb-3"> <div class="flex justify-between mb-3">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex gap-4 items-center"> <div class="flex gap-4 items-center">
<div class="poppins font-semibold text-[1.4rem] text-text"> <div class="poppins font-semibold text-[1.4rem] text-lyx-lightmode-text dark:text-lyx-text">
{{ label }} {{ label }}
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
@@ -63,7 +63,7 @@ function openExternalLink(link: string) {
</div> </div>
</div> </div>
<div class="poppins text-[1rem] text-text-sub/90"> <div class="poppins text-[1rem] text-lyx-ligtmode-text-darker dark:text-text-sub/90">
{{ desc }} {{ desc }}
</div> </div>
</div> </div>
@@ -81,7 +81,8 @@ function openExternalLink(link: string) {
</div> </div>
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<div class="flex justify-between font-bold text-text-sub/80 text-[1.1rem] mb-4"> <div
class="flex justify-between font-bold lyx-text-lightmode-text-dark dark:text-text-sub/80 text-[1.1rem] mb-4">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div v-if="isDetailView" class="flex items-center justify-center"> <div v-if="isDetailView" class="flex items-center justify-center">
<i @click="$emit('showGeneral')" <i @click="$emit('showGeneral')"
@@ -107,7 +108,7 @@ function openExternalLink(link: string) {
<div class="flex gap-1 items-center" @click="showDetails(element._id)" <div class="flex gap-1 items-center" @click="showDetails(element._id)"
:class="{ 'cursor-pointer line-active': interactive }"> :class="{ 'cursor-pointer line-active': interactive }">
<div class="absolute rounded-sm w-full h-full bg-[#92abcf38]" <div class="absolute rounded-sm w-full h-full bg-[#6f829c38] dark:bg-[#92abcf38]"
:style="'width:' + 100 / maxData * element.count + '%;'"></div> :style="'width:' + 100 / maxData * element.count + '%;'"></div>
<div class="flex px-2 py-1 relative items-center gap-4"> <div class="flex px-2 py-1 relative items-center gap-4">
@@ -119,24 +120,28 @@ function openExternalLink(link: string) {
<i v-else :class="iconProvider(element)?.[1]"></i> <i v-else :class="iconProvider(element)?.[1]"></i>
</div> </div>
<span class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-text/70"> <span
class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-lyx-lightmode-text-dark dark:text-text/70">
{{ elementTextTransformer?.(element._id) || element._id }} {{ elementTextTransformer?.(element._id) || element._id }}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div class="text-text font-semibold text-[.9rem] md:text-[1rem] manrope"> {{ <div
formatNumberK(element.count) }} </div> class="text-lyx-lightmode-text dark:text-lyx-text font-semibold text-[.9rem] md:text-[1rem] manrope">
{{
formatNumberK(element.count) }} </div>
</div> </div>
<div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-light text-[1.1rem]"> <div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-light text-[1.1rem]">
No data yet No data yet
</div> </div>
</div> </div>
<div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 items-end grow"> <div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 items-end grow">
<div @click="$emit('showMore')"
class="poppins hover:bg-black cursor-pointer w-fit px-6 py-1 rounded-lg border-[1px] border-text-sub text-[.9rem]"> <LyxUiButton type="outline" @click="$emit('showMore')">
Show more Show more
</div> </LyxUiButton>
</div> </div>
</div> </div>

View File

@@ -23,6 +23,19 @@ type Props = {
sections: Section[] sections: Section[]
} }
const colorMode = useColorMode()
const isDark = computed({
get() {
return colorMode.value === 'dark'
},
set() {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
const route = useRoute(); const route = useRoute();
const props = defineProps<Props>(); const props = defineProps<Props>();
@@ -108,7 +121,7 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
</script> </script>
<template> <template>
<div class="CVerticalNavigation border-solid border-[#202020] border-r-[1px] h-full w-[20rem] bg-lyx-background flex shadow-[1px_0_10px_#000000] rounded-r-lg" <div class="CVerticalNavigation border-solid border-[#D9D9E0] dark:border-[#202020] border-r-[1px] h-full w-[20rem] bg-lyx-lightmode-background dark:bg-lyx-background flex shadow-[1px_0_10px_#000000]"
:class="{ :class="{
'absolute top-0 w-full md:w-[20rem] z-[45] open': isOpen, 'absolute top-0 w-full md:w-[20rem] z-[45] open': isOpen,
'hidden lg:flex': !isOpen 'hidden lg:flex': !isOpen
@@ -157,7 +170,7 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
<div class="w-full flex-col px-2"> <div class="w-full flex-col px-2">
<div class="flex mb-2 items-center justify-between"> <div class="flex mb-2 items-center justify-between text-lyx-lightmode-text dark:text-lyx-text">
<div class="poppins text-[.8rem]"> <div class="poppins text-[.8rem]">
Snapshots Snapshots
</div> </div>
@@ -170,7 +183,7 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
</UTooltip> --> </UTooltip> -->
<UTooltip text="Create new snapshot"> <UTooltip text="Create new snapshot">
<LyxUiButton @click="openSnapshotDialog()" type="outlined" class="!px-3 !py-1"> <LyxUiButton @click="openSnapshotDialog()" type="outlined" class="!px-3 !py-1">
<div><i class="fas fa-plus text-[.9rem]"></i></div> <div><i class="fas fa-plus text-[.8rem]"></i></div>
</LyxUiButton> </LyxUiButton>
</UTooltip> </UTooltip>
</div> </div>
@@ -179,11 +192,11 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<USelectMenu :uiMenu="{ <USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter', select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
base: '!bg-lyx-widget', base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget',
option: { option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer', base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter' active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
} }
}" class="w-full" v-model="snapshot" :options="snapshotsItems"> }" class="w-full" v-model="snapshot" :options="snapshotsItems">
<template #label> <template #label>
@@ -204,7 +217,8 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
</div> </div>
<div v-if="snapshot" class="flex flex-col text-[.7rem] mt-2"> <div v-if="snapshot" class="flex flex-col text-[.7rem] mt-2">
<div class="flex gap-1 items-center justify-center text-lyx-text-dark"> <div
class="flex gap-1 items-center justify-center text-lyx-lightmode-text-dark dark:text-lyx-text-dark">
<div class="poppins"> <div class="poppins">
{{ new Date(snapshot.from).toLocaleString().split(',')[0].trim() }} {{ new Date(snapshot.from).toLocaleString().split(',')[0].trim() }}
</div> </div>
@@ -237,14 +251,14 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
</div> </div>
<div class="w-full flex mt-4"> <div class="w-full flex mt-4">
<LyxUiButton type="outline" class="w-full text-center text-[.7rem]"> <LyxUiButton type="outline" class="w-full text-center text-[.8rem]">
Export report Export report
</LyxUiButton> </LyxUiButton>
</div> </div>
</div> </div>
<div class="bg-[#202020] h-[1px] w-full"></div> <div class="bg-lyx-lightmode-widget dark:bg-[#202020] h-[1px] w-full"></div>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
@@ -253,11 +267,11 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
<div v-for="entry of section.entries" :class="{ 'grow flex items-end': entry.grow }"> <div v-for="entry of section.entries" :class="{ 'grow flex items-end': entry.grow }">
<div v-if="(!entry.adminOnly || (userRoles.isAdmin.value && !isAdminHidden))" <div v-if="(!entry.adminOnly || (userRoles.isAdmin.value && !isAdminHidden))"
class="bg-lyx-background w-full cursor-pointer text-lyx-text-dark py-[.35rem] px-2 rounded-lg text-[.95rem] flex items-center" class="bg-lyx-lightmode-background text-lyx-lightmode-text-dark dark:bg-lyx-background dark:text-lyx-text-dark w-full cursor-pointer py-[.35rem] px-2 rounded-lg text-[.95rem] flex items-center"
:class="{ :class="{
'!text-lyx-text-darker pointer-events-none': entry.disabled, '!text-lyx-text-darker pointer-events-none': entry.disabled,
'bg-lyx-background-lighter !text-lyx-text/90': route.path == (entry.to || '#'), 'bg-lyx-lightmode-background-light !text-lyx-lightmode-text dark:bg-lyx-background-lighter dark:!text-lyx-text': route.path == (entry.to || '#'),
'hover:bg-lyx-background-light hover:!text-lyx-text/90': route.path != (entry.to || '#'), 'hover:bg-lyx-lightmode-background-light hover:!text-lyx-lightmode-text dark:hover:bg-lyx-background-light dark:hover:!text-lyx-text': route.path != (entry.to || '#'),
}"> }">
<NuxtLink @click="close() && entry.action?.()" :target="entry.external ? '_blank' : ''" <NuxtLink @click="close() && entry.action?.()" :target="entry.external ? '_blank' : ''"
@@ -281,36 +295,25 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
<div class="grow"></div> <div class="grow"></div>
<div class="bg-[#202020] h-[1px] w-full px-4 mb-3"></div> <div class="bg-lyx-lightmode-widget dark:bg-[#202020] h-[1px] w-full px-4 mb-3"></div>
<div class="flex justify-end px-2"> <div class="flex justify-end px-2">
<div class="grow flex gap-3"> <div class="grow flex gap-3">
<!-- <NuxtLink to="https://github.com/litlyx/litlyx" target="_blank"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark"> <div>
<i class="fab fa-github"></i> <i @click="isDark = !isDark" class="cursor-pointer hover:text-lyx-lightmode-text text-lyx-lightmode-text-dark dark:hover:text-lyx-text dark:text-lyx-text-dark"
</NuxtLink> --> :class="isDark ? 'far fa-moon' : 'far fa-sun'"></i>
<!-- <NuxtLink to="https://discord.gg/9cQykjsmWX" target="_blank" </div>
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fab fa-discord"></i>
</NuxtLink> -->
<NuxtLink to="https://x.com/litlyx" target="_blank"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fab fa-x-twitter"></i>
</NuxtLink>
<!-- <NuxtLink to="https://dev.to/litlyx-org" target="_blank"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fab fa-dev"></i>
</NuxtLink> -->
<NuxtLink to="/admin" v-if="userRoles.isAdmin.value" <NuxtLink to="/admin" v-if="userRoles.isAdmin.value"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark"> class="cursor-pointer hover:text-lyx-lightmode-text text-lyx-lightmode-text-dark dark:hover:text-lyx-text dark:text-lyx-text-dark">
<i class="fas fa-cat"></i> <i class="far fa-cat"></i>
</NuxtLink> </NuxtLink>
</div> </div>
<UTooltip text="Logout" :popper="{ arrow: true, placement: 'top' }"> <UTooltip text="Logout" :popper="{ arrow: true, placement: 'top' }">
<div @click="onLogout()" class="cursor-pointer hover:text-lyx-text text-lyx-text-dark"> <div @click="onLogout()" class="cursor-pointer hover:text-lyx-lightmode-text text-lyx-lightmode-text-dark dark:hover:text-lyx-text dark:text-lyx-text-dark">
<i class="far fa-arrow-right-from-bracket scale-x-[-100%]"></i> <i class="far fa-arrow-right-from-bracket scale-x-[-100%]"></i>
</div> </div>
</UTooltip> </UTooltip>

View File

@@ -9,10 +9,10 @@ const props = defineProps<{ title: string, sub?: string }>();
<div class="flex flex-col gap-4 h-full"> <div class="flex flex-col gap-4 h-full">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex flex-col grow"> <div class="flex flex-col grow">
<div class="poppins font-semibold text-[1rem] md:text-[1.3rem] text-text"> <div class="poppins font-semibold text-[1rem] md:text-[1.3rem] text-lyx-lightmode-text-dark dark:text-text">
{{ props.title }} {{ props.title }}
</div> </div>
<div v-if="props.sub" class="poppins text-[.7rem] md:text-[1rem] text-text-sub"> <div v-if="props.sub" class="poppins text-[.7rem] md:text-[1rem] text-lyx-lightmode-text-darker dark:text-text-sub">
{{ props.sub }} {{ props.sub }}
</div> </div>
</div> </div>

View File

@@ -11,9 +11,9 @@ const activeTabIndex = ref<number>(0);
<div> <div>
<div class="flex"> <div class="flex">
<div v-for="(tab, index) of items" @click="activeTabIndex = index" <div v-for="(tab, index) of items" @click="activeTabIndex = index"
class="px-6 pb-3 poppins font-medium text-lyx-text-darker border-b-[1px] border-lyx-text-darker" :class="{ class="px-6 pb-3 poppins font-medium text-lyx-lightmode-text dark:text-lyx-text-darker border-b-[1px] border-lyx-text-darker" :class="{
'!border-[#88A7FF] !text-[#88A7FF]': activeTabIndex === index, '!border-[#88A7FF] !text-[#88A7FF]': activeTabIndex === index,
'hover:border-lyx-text-dark hover:text-lyx-text-dark cursor-pointer': activeTabIndex !== index 'hover:border-lyx-lightmode-text-dark hover:text-lyx-lightmode-text-dark/60 dark:hover:border-lyx-text-dark dark:hover:text-lyx-text-dark cursor-pointer': activeTabIndex !== index
}"> }">
{{ tab.label }} {{ tab.label }}
</div> </div>

View File

@@ -67,7 +67,7 @@ function reloadPage() {
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<div class="mr-4 animate-pulse w-[1rem] h-[1rem] bg-accent rounded-full"> </div> <div class="mr-4 animate-pulse w-[1rem] h-[1rem] bg-accent rounded-full"> </div>
<div class="text-text/90 poppins text-[1.1rem] font-medium"> <div class="text-lyx-lightmode-text dark:text-text/90 poppins text-[1.1rem] font-medium">
Waiting for your first visit Waiting for your first visit
</div> </div>
<LyxUiButton class="ml-6" type="secondary" @click="reloadPage()"> <LyxUiButton class="ml-6" type="secondary" @click="reloadPage()">
@@ -122,8 +122,8 @@ function reloadPage() {
<CardTitled class="h-full w-full" title="Project id" <CardTitled class="h-full w-full" title="Project id"
sub="This is the identifier for this project, used to forward data"> sub="This is the identifier for this project, used to forward data">
<div class="flex items-center justify-between gap-4 mt-6"> <div class="flex items-center justify-between gap-4 mt-6">
<div class="p-2 bg-[#1c1b1b] rounded-md w-full"> <div class="p-2 bg-lyx-lightmode-widget dark:bg-[#1c1b1b] rounded-md w-full">
<div class="w-full text-[.9rem] text-[#acacac]"> {{ project?._id }} </div> <div class="w-full text-[.9rem] dark:text-[#acacac]"> {{ project?._id }} </div>
</div> </div>
<LyxUiButton type="secondary" @click="copyProjectId()"> Copy </LyxUiButton> <LyxUiButton type="secondary" @click="copyProjectId()"> Copy </LyxUiButton>
</div> </div>

View File

@@ -8,12 +8,18 @@ const props = defineProps<{ type: ButtonType, link?: string, target?: string, di
<template> <template>
<NuxtLink tag="div" :to="disabled ? '' : link" :target="target" <NuxtLink tag="div" :to="disabled ? '' : link" :target="target"
class="poppins w-fit cursor-pointer px-4 py-1 rounded-md outline outline-[1px] text-text" :class="{ class="poppins w-fit cursor-pointer px-4 py-1 rounded-md outline outline-[1px] text-lyx-lightmode-text dark:text-lyx-text"
'bg-lyx-primary-dark outline-lyx-primary hover:bg-lyx-primary-hover': type === 'primary', :class="{
'bg-lyx-widget-lighter outline-lyx-widget-lighter hover:bg-lyx-widget-light': type === 'secondary',
'bg-lyx-transparent outline-lyx-widget-lighter hover:bg-lyx-widget-light': (type === 'outline' || type === 'outlined'), 'bg-[#85a3ff] hover:bg-[#9db5fc] outline-lyx-lightmode-widget-light dark:bg-lyx-primary-dark dark:outline-lyx-primary dark:hover:bg-lyx-primary-hover': type === 'primary',
'bg-lyx-danger-dark outline-lyx-danger hover:bg-lyx-danger': type === 'danger',
'!bg-lyx-widget !outline-lyx-widget-lighter !cursor-not-allowed': disabled === true, 'bg-lyx-lightmode-widget-light outline-lyx-lightmode-widget dark:bg-lyx-widget-lighter hover:bg-lyx-lightmode-widget dark:outline-lyx-widget-lighter dark:hover:bg-lyx-widget-light': type === 'secondary',
'bg-lyx-transparent outline-lyx-lightmode-widget hover:bg-lyx-lightmode-widget-light dark:outline-lyx-widget-lighter dark:hover:bg-lyx-widget-light': (type === 'outline' || type === 'outlined'),
'bg-[#fcd1cb] hover:bg-[#f8c5be] dark:bg-lyx-danger-dark outline-lyx-danger dark:hover:bg-lyx-danger': type === 'danger',
'text-lyx-text !bg-lyx-widget !outline-lyx-widget-lighter !cursor-not-allowed': disabled === true,
}"> }">
<slot></slot> <slot></slot>
</NuxtLink> </NuxtLink>

View File

@@ -4,7 +4,7 @@
</script> </script>
<template> <template>
<div class="w-fit h-fit rounded-md bg-lyx-widget p-4 outline outline-[1px] outline-lyx-background-lighter"> <div class="w-fit h-fit rounded-md bg-lyx-lightmode-background outline-lyx-lightmode-widget dark:bg-lyx-widget dark:outline-lyx-background-lighter p-4 outline outline-[1px] ">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>

View File

@@ -19,6 +19,6 @@ const handleChange = (event: Event) => {
<template> <template>
<input <input
class="bg-lyx-widget-light text-lyx-text-dark poppins rounded-md outline outline-[1px] outline-lyx-widget-lighter" class="bg-lyx-lightmode-widget-light outline-lyx-lightmode-widget text-lyx-lightmode-text dark:bg-lyx-widget-light dark:text-lyx-text-dark poppins rounded-md outline outline-[1px] dark:outline-lyx-widget-lighter"
:type="props.type ?? 'text'" :placeholder="props.placeholder" :value="props.modelValue" @input="handleChange"> :type="props.type ?? 'text'" :placeholder="props.placeholder" :value="props.modelValue" @input="handleChange">
</template> </template>

View File

@@ -109,11 +109,11 @@ const showOnboarding = computed(() => {
<div v-if="page == 0" class="bg-lyx-background-light mt-[10vh] w-[50vw] min-w-[400px] h-fit p-8 rounded-md"> <div v-if="page == 0" class="bg-lyx-lightmode-background-light dark:bg-lyx-background-light mt-[10vh] w-[50vw] min-w-[400px] h-fit p-8 rounded-md">
<div class="text-lyx-text text-[1.4rem] text-center font-medium"> Getting Started </div> <div class="text-lyx-lightmode-text dark:text-lyx-text text-[1.4rem] text-center font-medium"> Getting Started </div>
<div class="text-lyx-text mt-4"> <div class="text-lyx-lightmode-text dark:text-lyx-text mt-4">
For the current project do you already have other Analytics tools implemented (e.g. GA4) or Litlyx is For the current project do you already have other Analytics tools implemented (e.g. GA4) or Litlyx is
going to be your first/main analytics? going to be your first/main analytics?
</div> </div>
@@ -122,7 +122,7 @@ const showOnboarding = computed(() => {
<div v-for="(e, i) of analyticsList"> <div v-for="(e, i) of analyticsList">
<div @click="selectIndex(i)" <div @click="selectIndex(i)"
:class="{ 'outline outline-[1px] outline-[#5680f8]': selectedIndex == i }" :class="{ 'outline outline-[1px] outline-[#5680f8]': selectedIndex == i }"
class="bg-lyx-widget-light text-center p-2 rounded-md cursor-pointer"> class="bg-lyx-lightmode-widget-light dark:bg-lyx-widget-light text-center p-2 rounded-md cursor-pointer">
{{ e }} {{ e }}
</div> </div>
</div> </div>
@@ -140,11 +140,11 @@ const showOnboarding = computed(() => {
</div> </div>
</div> </div>
<div v-if="page == 1" class="bg-lyx-background-light mt-[10vh] w-[50vw] min-w-[400px] h-fit p-8 rounded-md"> <div v-if="page == 1" class="bg-lyx-lightmode-background-light dark:bg-lyx-background-light mt-[10vh] w-[50vw] min-w-[400px] h-fit p-8 rounded-md">
<div class="text-lyx-text text-[1.4rem] text-center font-medium"> Getting Started </div> <div class="text-lyx-lightmode-text dark:text-lyx-text text-[1.4rem] text-center font-medium"> Getting Started </div>
<div class="text-lyx-text mt-4"> <div class="text-lyx-lightmode-text dark:text-lyx-text mt-4">
What is your job title ? What is your job title ?
</div> </div>
@@ -152,7 +152,7 @@ const showOnboarding = computed(() => {
<div v-for="(e, i) of jobsList"> <div v-for="(e, i) of jobsList">
<div @click="selectIndex2(i)" <div @click="selectIndex2(i)"
:class="{ 'outline outline-[1px] outline-[#5680f8]': selectedIndex2 == i }" :class="{ 'outline outline-[1px] outline-[#5680f8]': selectedIndex2 == i }"
class="bg-lyx-widget-light text-center p-2 rounded-md cursor-pointer"> class="bg-lyx-lightmode-widget-light dark:bg-lyx-widget-light text-center p-2 rounded-md cursor-pointer">
{{ e }} {{ e }}
</div> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@ import type { TProject } from '@schema/project/ProjectSchema';
const { user } = useLoggedUser() const { user } = useLoggedUser()
const { projectList, guestProjectList,allProjectList, actions, project } = useProject(); const { projectList, guestProjectList, allProjectList, actions, project } = useProject();
function isProjectMine(owner?: string) { function isProjectMine(owner?: string) {
@@ -22,11 +22,11 @@ function onChange(e: TProject) {
<template> <template>
<USelectMenu :uiMenu="{ <USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter', select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
base: '!bg-lyx-widget', base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget',
option: { option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer', base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter' active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
} }
}" class="w-full" v-if="allProjectList" @change="onChange" :value="project" :options="allProjectList"> }" class="w-full" v-if="allProjectList" @change="onChange" :value="project" :options="allProjectList">

View File

@@ -16,12 +16,12 @@ const emits = defineEmits<{
<template> <template>
<div class="flex gap-2 border-[1px] border-lyx-widget-lighter p-1 md:p-2 rounded-xl bg-lyx-widget"> <div class="flex gap-2 border-[1px] p-1 md:p-2 rounded-xl bg-lyx-lightmode-widget-light border-lyx-lightmode-widget dark:bg-lyx-widget dark:border-lyx-widget-lighter">
<div @click="opt.disabled ? ()=>{}: $emit('changeIndex', index)" v-for="(opt, index) of options" <div @click="opt.disabled ? ()=>{}: $emit('changeIndex', index)" v-for="(opt, index) of options"
class="hover:bg-lyx-widget-lighter/60 select-btn-animated cursor-pointer rounded-lg poppins font-regular px-2 md:px-3 py-1 text-[.8rem] md:text-[1rem]" class="hover:bg-lyx-lightmode-widget dark:hover:bg-lyx-widget-lighter/60 select-btn-animated cursor-pointer rounded-lg poppins font-regular px-2 md:px-3 py-1 text-[.8rem] md:text-[1rem]"
:class="{ :class="{
'bg-lyx-widget-lighter hover:!bg-lyx-widget-lighter': currentIndex == index && !opt.disabled, 'bg-lyx-lightmode-widget hover:!bg-lyx-lightmode-widget dark:bg-lyx-widget-lighter dark:hover:!bg-lyx-widget-lighter': currentIndex == index && !opt.disabled,
'hover:!bg-lyx-widget !cursor-not-allowed text-lyx-widget-lighter': opt.disabled 'hover:!bg-lyx-lightmode-widget-light text-lyx-lightmode-widget dark:hover:!bg-lyx-widget !cursor-not-allowed dark:!text-lyx-widget-lighter': opt.disabled
}"> }">
{{ opt.label }} {{ opt.label }}
</div> </div>

View File

@@ -333,8 +333,8 @@ const legendClasses = ref<string[]>([
<div class="flex gap-6 w-full justify-between lg:flex-row flex-col"> <div class="flex gap-6 w-full justify-between lg:flex-row flex-col">
<LyxUiButton type="secondary" :to="isLiveDemo ? '#' : '/analyst'" :disabled="isLiveDemo"> <LyxUiButton type="secondary" :to="isLiveDemo ? '#' : '/analyst'" :disabled="isLiveDemo">
<div class="flex items-center gap-2 px-10"> <div class="flex items-center gap-2 px-10">
<i class="far fa-sparkles text-yellow-400"></i> <i class="far fa-sparkles text-yellow-600 dark:text-yellow-400"></i>
<div class="poppins text-lyx-text"> Ask AI </div> <div class="poppins text-lyx-lightmode-text dark:text-lyx-text"> Ask AI </div>
</div> </div>
</LyxUiButton> </LyxUiButton>
<div class="flex gap-6"> <div class="flex gap-6">
@@ -352,7 +352,7 @@ const legendClasses = ref<string[]>([
<div id='external-tooltip' ref="externalTooltipElement" class="z-[400]"> <div id='external-tooltip' ref="externalTooltipElement" class="z-[400]">
<LyxUiCard> <LyxUiCard class="text-lyx-lightmode-text dark:text-lyx-text">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<div> Date: </div> <div> Date: </div>
<div v-if="currentTooltipData"> {{ currentTooltipData.date }}</div> <div v-if="currentTooltipData"> {{ currentTooltipData.date }}</div>

View File

@@ -29,12 +29,12 @@ const { showDrawer } = useDrawer();
</div> </div>
<div class="flex flex-col grow"> <div class="flex flex-col grow">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="brockmann text-text-dirty text-[1.2rem] 2xl:text-[1.4rem]"> <div class="brockmann text-lyx-lightmode-text-dark dark:text-text-dirty text-[1.2rem] 2xl:text-[1.4rem]">
{{ value }} {{ value }}
</div> </div>
<div class="poppins text-text-sub text-[.65rem] 2xl:text-[.8rem]"> {{ avg }} </div> <div class="poppins text-lyx-lightmode-darker dark:text-text-sub text-[.65rem] 2xl:text-[.8rem]"> {{ avg }} </div>
</div> </div>
<div class="poppins text-text-sub text-[.9rem] 2xl:text-[1rem]"> {{ text }} </div> <div class="poppins text-lyx-lightmode-darker dark:text-text-sub text-[.9rem] 2xl:text-[1rem]"> {{ text }} </div>
</div> </div>
<div class="flex flex-col items-center gap-1"> <div class="flex flex-col items-center gap-1">

View File

@@ -13,8 +13,8 @@ const columns = [
<template> <template>
<div class="w-full h-full bg-bg rounded-xl p-8"> <div class="w-full h-full bg-lyx-lightmode-background dark:bg-lyx-background-light rounded-xl p-8">
<div class="full h-full overflow-y-auto"> <div class="full h-full overflow-y-auto text-lyx-lightmode-text dark:text-lyx-text">
<UTable :columns="columns" :rows="dialogBarData" :loading="isDataLoading" v-if="dialogBarData"> <UTable :columns="columns" :rows="dialogBarData" :loading="isDataLoading" v-if="dialogBarData">
<template #count-data="{ row }"> <template #count-data="{ row }">
<div class="font-bold"> {{ formatNumberK(row.count) }} </div> <div class="font-bold"> {{ formatNumberK(row.count) }} </div>

View File

@@ -27,7 +27,6 @@ const chartOptions = ref<ChartOptions<'doughnut'>>({
position: 'top', position: 'top',
align: 'center', align: 'center',
labels: { labels: {
color: 'white',
font: { font: {
family: 'Poppins', family: 'Poppins',
size: 16 size: 16

View File

@@ -101,8 +101,8 @@ const todayIndex = computed(() => {
<DashboardCountCard :todayIndex="todayIndex" :ready="!visitsData.pending.value" icon="far fa-earth" <DashboardCountCard :todayIndex="todayIndex" :ready="!visitsData.pending.value" icon="far fa-earth"
text="Total visits" :value="formatNumberK(visitsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')" text="Total visits" :value="formatNumberK(visitsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
:avg="formatNumberK(avgVisitDay) + '/day'" :data="visitsData.data.value?.data" :avg="formatNumberK(avgVisitDay) + '/day'" :data="visitsData.data.value?.data"
tooltipText="Sum of all page views on your website." tooltipText="Sum of all page views on your website." :labels="visitsData.data.value?.labels"
:labels="visitsData.data.value?.labels" color="#5655d7"> color="#5655d7">
</DashboardCountCard> </DashboardCountCard>
<DashboardCountCard :todayIndex="todayIndex" :ready="!bouncingRateData.pending.value" icon="far fa-chart-user" <DashboardCountCard :todayIndex="todayIndex" :ready="!bouncingRateData.pending.value" icon="far fa-chart-user"
@@ -115,16 +115,15 @@ const todayIndex = computed(() => {
<DashboardCountCard :todayIndex="todayIndex" :ready="!sessionsData.pending.value" icon="far fa-user" <DashboardCountCard :todayIndex="todayIndex" :ready="!sessionsData.pending.value" icon="far fa-user"
text="Unique visitors" text="Unique visitors"
:value="formatNumberK(sessionsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')" :value="formatNumberK(sessionsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
tooltipText="Count of distinct users visiting your website." tooltipText="Count of distinct users visiting your website." :avg="formatNumberK(avgSessionsDay) + '/day'"
:avg="formatNumberK(avgSessionsDay) + '/day'" :data="sessionsData.data.value?.data" :data="sessionsData.data.value?.data" :labels="sessionsData.data.value?.labels" color="#4abde8">
:labels="sessionsData.data.value?.labels" color="#4abde8">
</DashboardCountCard> </DashboardCountCard>
<DashboardCountCard :todayIndex="todayIndex" :ready="!sessionsDurationData.pending.value" icon="far fa-timer" <DashboardCountCard :todayIndex="todayIndex" :ready="!sessionsDurationData.pending.value" icon="far fa-timer"
text="Visit duration" :value="avgSessionDuration" :data="sessionsDurationData.data.value?.data" text="Visit duration" :value="avgSessionDuration" :data="sessionsDurationData.data.value?.data"
tooltipText="Average time users spend on your website." tooltipText="Average time users spend on your website." :labels="sessionsDurationData.data.value?.labels"
:labels="sessionsDurationData.data.value?.labels" color="#f56523"> color="#f56523">
</DashboardCountCard> </DashboardCountCard>
</div> </div>

View File

@@ -34,10 +34,10 @@ function showAnomalyInfoAlert() {
<template> <template>
<div <div class="w-full px-6 pb-2 lg:pb-6 font-bold flex flex-col xl:flex-row text-lg gap-2 xl:gap-12 lg:text-2xl">
class="w-full px-6 pb-2 lg:pb-6 font-bold text-text-sub/40 flex flex-col xl:flex-row text-lg gap-2 xl:gap-12 lg:text-2xl">
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start"> <div
class="flex gap-2 items-center text-lyx-lightmode-text/90 dark:text-lyx-text/90 justify-center md:justify-start">
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div> <div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
<div class="poppins font-medium text-[.9rem]"> {{ onlineUsers.data }} Online users</div> <div class="poppins font-medium text-[.9rem]"> {{ onlineUsers.data }} Online users</div>
</div> </div>
@@ -63,7 +63,8 @@ function showAnomalyInfoAlert() {
</div> </div>
</div> --> </div> -->
<div v-if="!selfhosted" class="flex gap-2 items-center text-text/90 justify-center md:justify-start"> <div v-if="!selfhosted"
class="flex gap-2 items-center text-lyx-lightmode-text/90 dark:text-lyx-text/90 justify-center md:justify-start">
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div> <div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
<div class="poppins font-regular text-[.9rem]"> AI Anomaly Detector </div> <div class="poppins font-regular text-[.9rem]"> AI Anomaly Detector </div>
<div class="flex items-center"> <div class="flex items-center">

View File

@@ -36,7 +36,7 @@ function onColorChange() {
const snapshotName = ref<string>(""); const snapshotName = ref<string>("");
const { updateSnapshots } = useSnapshot(); const { updateSnapshots, snapshot, snapshots } = useSnapshot();
const { createAlert } = useAlert() const { createAlert } = useAlert()
async function confirmSnapshot() { async function confirmSnapshot() {
@@ -54,6 +54,9 @@ async function confirmSnapshot() {
await updateSnapshots(); await updateSnapshots();
closeDialog(); closeDialog();
createAlert('Snapshot created', 'Snapshot created successfully', 'far fa-circle-check', 5000); createAlert('Snapshot created', 'Snapshot created successfully', 'far fa-circle-check', 5000);
const newSnapshot = snapshots.value.at(-1);
if (newSnapshot) snapshot.value = newSnapshot;
} }
</script> </script>
@@ -61,7 +64,7 @@ async function confirmSnapshot() {
<template> <template>
<div class="w-full h-full flex flex-col"> <div class="w-full h-full flex flex-col">
<div class="poppins text-center"> <div class="poppins text-center text-lyx-lightmode-text dark:text-lyx-text">
Create a snapshot Create a snapshot
</div> </div>

View File

@@ -35,7 +35,7 @@ async function sendFeedback() {
overlay: { overlay: {
background: 'bg-lyx-background/85' background: 'bg-lyx-background/85'
}, },
background: 'bg-lyx-widget', background: 'dark:bg-lyx-widget bg-lyx-lightmode-widget-light',
ring: 'border-solid border-[1px] border-[#262626]' ring: 'border-solid border-[1px] border-[#262626]'
}"> }">
<div class="h-full flex flex-col gap-2 p-4"> <div class="h-full flex flex-col gap-2 p-4">
@@ -43,7 +43,7 @@ async function sendFeedback() {
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div> Share everything with us. </div> <div> Share everything with us. </div>
<textarea v-model="text" placeholder="Leave your feedback" <textarea v-model="text" placeholder="Leave your feedback"
class="p-2 w-full h-[8rem] resize-none rounded-md outline outline-[2px] outline-[#3a3f47]"></textarea> class="p-2 w-full h-[8rem] dark:bg-lyx-widget bg-lyx-lightmode-widget-light resize-none rounded-md outline outline-[2px] outline-[#3a3f47]"></textarea>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div>Need help ? Check the docs <a href="https://docs.litlyx.com" target="_blank" <div>Need help ? Check the docs <a href="https://docs.litlyx.com" target="_blank"
class="text-blue-500">here</a> </div> class="text-blue-500">here</a> </div>

View File

@@ -10,7 +10,7 @@ const { drawerComponent } = useDrawer();
<div class="p-8 overflow-y-auto"> <div class="p-8 overflow-y-auto">
<div @click="$emit('onCloseClick')" <div @click="$emit('onCloseClick')"
class="cursor-pointer fixed top-4 right-4 rounded-full bg-menu drop-shadow-[0_0_2px_#CCCCCCCC] w-9 h-9 flex items-center justify-center"> class="cursor-pointer fixed top-4 right-4 rounded-full dark:bg-menu drop-shadow-[0_0_2px_#CCCCCCCC] w-9 h-9 flex items-center justify-center">
<i class="fas fa-close text-[1.6rem]"></i> <i class="fas fa-close text-[1.6rem]"></i>
</div> </div>

View File

@@ -195,19 +195,27 @@ function getPricingsData() {
<div class="flex justify-between items-center mt-10 flex-col xl:flex-row"> <div class="flex justify-between items-center mt-10 flex-col xl:flex-row">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="poppins text-[1.1rem] text-lyx-lightmode-text dark:text-yellow-400 mb-2">
*Plan upgrades are applicable exclusively to this project(workspace).
</div>
<div class="poppins text-[2rem] font-semibold"> <div class="poppins text-[2rem] font-semibold">
Do you need help ? Do you need help ?
</div> </div>
<div class="poppins text-[1.2rem] text-text/90"> <div class="poppins text-[1.2rem]">
We respond in max. 1-2 days We respond in max. 1-2 days
</div> </div>
</div> </div>
<div class="mt-2"> <div class="flex flex-col gap-2">
<div class="rounded-lg px-10 py-3 bg-[#151515]"> <LyxUiButton type="secondary">
<a href="mailto:help@litlyx.com" class="poppins text-[1.3rem]"> <a href="mailto:help@litlyx.com" class="poppins text-[1.1rem]">
help@litlyx.com help@litlyx.com
</a> </a>
</div> </LyxUiButton>
<LyxUiButton type="secondary">
<a href="https://discord.com/invite/9cQykjsmWX" class="poppins text-[1.1rem]">
Discord support
</a>
</LyxUiButton>
</div> </div>
</div> </div>

View File

@@ -81,11 +81,11 @@ const canSearch = computed(() => {
<div class="flex-[2]"> <div class="flex-[2]">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<USelectMenu :uiMenu="{ <USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter', select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
base: '!bg-lyx-widget', base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget',
option: { option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer', base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter' active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
} }
}" searchable searchable-placeholder="Search an event..." class="w-full" }" searchable searchable-placeholder="Search an event..." class="w-full"
placeholder="Select an event" :options="eventNames.data.value || []" placeholder="Select an event" :options="eventNames.data.value || []"
@@ -93,11 +93,11 @@ const canSearch = computed(() => {
</USelectMenu> </USelectMenu>
<USelectMenu :uiMenu="{ <USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter', select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
base: '!bg-lyx-widget', base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget',
option: { option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer', base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter' active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
} }
}" searchable searchable-placeholder="Search a field..." class="w-full" }" searchable searchable-placeholder="Search a field..." class="w-full"
placeholder="Select a field" :options="metadataFields" v-model="selectedMetadataField"> placeholder="Select a field" :options="metadataFields" v-model="selectedMetadataField">
@@ -110,7 +110,7 @@ const canSearch = computed(() => {
</div> </div>
<div v-if="canSearch" class="h-full flex items-center text-[1.2rem]"> <div v-if="canSearch" class="h-full flex items-center text-[1.2rem]">
<div class="bg-lyx-widget-light flex items-center rounded-md pl-4"> <div class="bg-lyx-lightmode-widget dark:bg-lyx-widget-light flex items-center rounded-md pl-4">
<div><i class="far fa-search"></i></div> <div><i class="far fa-search"></i></div>
<input class="bg-transparent px-4 py-2 text-[1rem] outline-none" type="text" <input class="bg-transparent px-4 py-2 text-[1rem] outline-none" type="text"
placeholder="Filter by metadata name" v-model="currentSearchText"> placeholder="Filter by metadata name" v-model="currentSearchText">
@@ -121,7 +121,7 @@ const canSearch = computed(() => {
<div class="flex flex-wrap gap-2 lg:mt-4 mt-10"> <div class="flex flex-wrap gap-2 lg:mt-4 mt-10">
<div class="bg-lyx-widget-light text-lyx-text-dark px-3 py-2 rounded-md w-fit" <div class="bg-lyx-lightmode-widget dark:bg-lyx-widget-light text-lyx-lightmode-text dark:text-lyx-text-dark px-3 py-2 rounded-md w-fit"
v-for="item of metadataFieldGroupedFiltered"> v-for="item of metadataFieldGroupedFiltered">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<div> {{ item._id || 'OLD_EVENTS' }} </div> <div> {{ item._id || 'OLD_EVENTS' }} </div>

View File

@@ -46,12 +46,12 @@ async function analyzeEvent() {
<div class="py-2 flex items-center gap-3"> <div class="py-2 flex items-center gap-3">
<USelectMenu :uiMenu="{ <USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter', select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
base: '!bg-lyx-widget', base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget',
option: { option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer', base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter' active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
} }
}" searchable searchable-placeholder="Search an event..." class="w-full" placeholder="Select an event" }" searchable searchable-placeholder="Search an event..." class="w-full" placeholder="Select an event"
:options="eventNames.data.value || []" v-model="selectedEventName"> :options="eventNames.data.value || []" v-model="selectedEventName">
</USelectMenu> </USelectMenu>
@@ -71,7 +71,7 @@ async function analyzeEvent() {
</div> </div>
<div class="flex flex-col gap-2" v-if="userFlowData"> <div class="flex flex-col gap-2" v-if="userFlowData">
<div class="flex gap-4 items-center bg-bg py-2 px-2 bg-lyx-widget-light rounded-lg" <div class="flex gap-4 items-center bg-bg py-2 px-2 bg-lyx-lightmode-widget dark:bg-lyx-widget-light rounded-lg"
v-for="(count, referrer) in userFlowData"> v-for="(count, referrer) in userFlowData">
<div class="w-5 h-5 flex items-center justify-center"> <div class="w-5 h-5 flex items-center justify-center">
<img :src="`https://s2.googleusercontent.com/s2/favicons?domain=${referrer}&sz=64`" <img :src="`https://s2.googleusercontent.com/s2/favicons?domain=${referrer}&sz=64`"

View File

@@ -22,86 +22,27 @@ const widthHeight = computed(() => {
return 9 + props.size * props.spacing; return 9 + props.size * props.spacing;
}); });
const colorMode = useColorMode();
</script> </script>
<template> <template>
<div class="w-fit h-fit"> <div class="w-fit h-fit">
<svg xmlns="http://www.w3.org/2000/svg" :width="widthHeight" :height="widthHeight" :style="`opacity: ${props.opacity};`" <svg xmlns="http://www.w3.org/2000/svg" :width="widthHeight" :height="widthHeight" :style="`opacity: ${props.opacity};`"
fill="none"> fill="none">
<template v-for="(p, x) of sizeArr"> <template v-for="(p, x) of sizeArr">
<template v-for="(p, y) of sizeArr"> <template v-for="(p, y) of sizeArr">
<circle :cx="9 + (spacing * x)" :cy="9 + (spacing * y)" r="1" fill="#fff" <circle :cx="9 + (spacing * x)" :cy="9 + (spacing * y)" r="1" :fill="colorMode.value === 'light' ? '#000' : '#FFF'"
:fill-opacity="calculateOpacity(x, y)" /> :fill-opacity="calculateOpacity(x, y)" />
</template> </template>
</template> </template>
<!-- <circle cx="27" cy="9" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="45" cy="9" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="63" cy="9" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="81" cy="9" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="99" cy="9" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="117" cy="9" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="135" cy="9" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="9" cy="27" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="27" cy="27" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="45" cy="27" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="63" cy="27" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="81" cy="27" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="99" cy="27" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="117" cy="27" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="135" cy="27" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="9" cy="45" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="27" cy="45" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="45" cy="45" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="63" cy="45" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="81" cy="45" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="99" cy="45" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="117" cy="45" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="135" cy="45" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="9" cy="63" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="27" cy="63" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="45" cy="63" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="63" cy="63" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="81" cy="63" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="99" cy="63" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="117" cy="63" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="135" cy="63" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="9" cy="81" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="27" cy="81" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="45" cy="81" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="63" cy="81" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="81" cy="81" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="99" cy="81" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="117" cy="81" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="135" cy="81" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="9" cy="99" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="27" cy="99" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="45" cy="99" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="63" cy="99" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="81" cy="99" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="99" cy="99" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="117" cy="99" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="135" cy="99" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="9" cy="117" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="27" cy="117" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="45" cy="117" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="63" cy="117" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="81" cy="117" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="99" cy="117" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="117" cy="117" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="135" cy="117" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="9" cy="135" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="27" cy="135" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="45" cy="135" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="63" cy="135" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="81" cy="135" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="99" cy="135" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="117" cy="135" r="1" fill="#fff" fill-opacity=".9" />
<circle cx="135" cy="135" r="1" fill="#fff" fill-opacity=".9" />
-->
</svg> </svg>
</div> </div>

View File

@@ -41,7 +41,7 @@ async function onUpgradeClick() {
<template> <template>
<div <div
class="relative bg-[#151515] outline outline-[1px] outline-[#262626] py-8 px-10 rounded-lg w-full max-w-[30rem]"> class="relative bg-lyx-lightmode-widget-light dark:bg-[#151515] outline outline-[1px] outline-lyx-lightmode-widget dark:outline-[#262626] py-8 px-10 rounded-lg w-full max-w-[30rem]">
<div class="flex flex-col gap-3 text-center pt-3"> <div class="flex flex-col gap-3 text-center pt-3">
<div v-if="data.active" <div v-if="data.active"
@@ -56,7 +56,7 @@ async function onUpgradeClick() {
<div class="poppins text-4xl font-medium"> {{ data.price }} </div> <div class="poppins text-4xl font-medium"> {{ data.price }} </div>
</div> </div>
<div class="sep bg-[#262626] h-[1px] my-8"></div> <div class="sep bg-lyx-lightmode-widget dark:bg-[#262626] h-[1px] my-8"></div>
<div class="flex flex-col text-center h-[6rem] justify-center gap-2"> <div class="flex flex-col text-center h-[6rem] justify-center gap-2">
<div v-if="datas.length > 1"> <div v-if="datas.length > 1">
@@ -76,7 +76,7 @@ async function onUpgradeClick() {
</div> </div>
</div> </div>
<div class="sep bg-[#262626] h-[1px] my-8"></div> <div class="sep bg-lyx-lightmode-widget dark:bg-[#262626] h-[1px] my-8"></div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2" v-for="feature of data.features"> <div class="flex gap-2" v-for="feature of data.features">

View File

@@ -2,12 +2,18 @@
import type { SettingsTemplateEntry } from './Template.vue'; import type { SettingsTemplateEntry } from './Template.vue';
const entries: SettingsTemplateEntry[] = [ const entries: SettingsTemplateEntry[] = [
{ id: 'change_pass', title: 'Change password', text: 'Change your password' },
{ id: 'delete', title: 'Delete account', text: 'Delete your account' }, { id: 'delete', title: 'Delete account', text: 'Delete your account' },
] ]
const { user } = useLoggedUser();
const { setToken } = useAccessToken(); const { setToken } = useAccessToken();
const canChangePassword = useFetch('/api/user/password/can_change', {
headers: useComputedHeaders({ useSnapshotDates: false })
});
async function deleteAccount() { async function deleteAccount() {
const sure = confirm("Are you sure you want to delete this account ?"); const sure = confirm("Are you sure you want to delete this account ?");
if (!sure) return; if (!sure) return;
@@ -20,17 +26,63 @@ async function deleteAccount() {
location.href = "/login" location.href = "/login"
} }
const old_password = ref<string>("");
const new_password = ref<string>("");
const { createAlert } = useAlert()
async function changePassword() {
try {
const res = await $fetch("/api/user/password/change", {
...signHeaders({ 'Content-Type': 'application/json' }),
method: "POST",
body: JSON.stringify({ old_password: old_password.value, new_password: new_password.value })
})
if (!res) throw Error('No response');
if (res.error) return createAlert('Error', res.message, 'far fa-triangle-exclamation', 5000);
old_password.value = '';
new_password.value = '';
return createAlert('Success', 'Password changed successfully', 'far fa-circle-check', 5000);
} catch (ex) {
console.error(ex);
createAlert('Error', 'Internal error', 'far fa-triangle-exclamation', 5000);
}
}
</script> </script>
<template> <template>
<SettingsTemplate :entries="entries"> <SettingsTemplate :entries="entries">
<template #change_pass>
<div v-if="canChangePassword.data.value?.can_change">
<div class="flex flex-col gap-4">
<LyxUiInput type="password" class="py-1 px-2" v-model="old_password" placeholder="Current password"></LyxUiInput>
<LyxUiInput type="password" class="py-1 px-2" v-model="new_password" placeholder="New password"></LyxUiInput>
<LyxUiButton type="primary" @click="changePassword()"> Change password </LyxUiButton>
</div>
</div>
<div v-if="!canChangePassword.data.value?.can_change">
You cannot change the password for accounts created using social login options.
</div>
</template>
<template #delete> <template #delete>
<div <div
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-[#1e1412]"> 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"> Deleting this account will also remove its projects </div> <div class="poppins font-semibold"> Deleting this account will also remove its projects </div>
<div @click="deleteAccount()" <div @click="deleteAccount()"
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-[#291415]"> 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]">
Delete account Delete account
</div> </div>
</div> </div>

View File

@@ -53,6 +53,9 @@ async function redeemCode() {
<div class="text-lyx-text-darker mt-1 text-[.9rem] poppins"> <div class="text-lyx-text-darker mt-1 text-[.9rem] poppins">
Redeemed codes: {{ valid_codes.data.value?.count || '0' }} Redeemed codes: {{ valid_codes.data.value?.count || '0' }}
</div> </div>
<div class="poppins text-[1.1rem] text-lyx-lightmode-text dark:text-yellow-400 mb-2">
*Plan upgrades are applicable exclusively to this project(workspace).
</div>
</template> </template>
</SettingsTemplate> </SettingsTemplate>
</template> </template>

View File

@@ -106,12 +106,12 @@ const sessionsLabel = computed(() => {
<!-- <div class="text-[.9rem] text-lyx-text-darker"> Select a domain </div> --> <!-- <div class="text-[.9rem] text-lyx-text-darker"> Select a domain </div> -->
<USelectMenu placeholder="Select a domain" :uiMenu="{ <USelectMenu placeholder="Select a domain" :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter', select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
base: '!bg-lyx-widget', base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget',
option: { option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer', base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter' active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
} }
}" :options="domains.data.value ?? []" v-model="selectedDomain"></USelectMenu> }" :options="domains.data.value ?? []" v-model="selectedDomain"></USelectMenu>
<div v-if="selectedDomain" class="flex flex-col gap-2 mt-4"> <div v-if="selectedDomain" class="flex flex-col gap-2 mt-4">
@@ -140,15 +140,17 @@ const sessionsLabel = computed(() => {
</div> </div>
</template> </template>
<template #delete_data> <template #delete_data>
<div <div
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-[#1e1412]"> 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 it's initial state (0
visits 0 events 0 sessions)</div> visits 0 events 0 sessions) </div>
<div @click="openDeleteAllDomainDataDialog()" <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-[#291415]"> 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]">
Delete all data Delete all data
</div> </div>
</div> </div>
</template> </template>
</SettingsTemplate> </SettingsTemplate>
</template> </template>

View File

@@ -20,10 +20,10 @@ const props = defineProps<SettingsTemplateProp>();
<div v-for="(entry, index) of props.entries" class="flex flex-col"> <div v-for="(entry, index) of props.entries" class="flex flex-col">
<div class="flex xl:flex-row flex-col gap-4 xl:gap-0"> <div class="flex xl:flex-row flex-col gap-4 xl:gap-0">
<div class="xl:flex-[2]"> <div class="xl:flex-[2]">
<div class="poppins font-medium text-lyx-text"> <div class="poppins font-medium text-lyx-lightmode-text dark:text-lyx-text">
{{ entry.title }} {{ entry.title }}
</div> </div>
<div class="poppins font-regular text-lyx-text-dark whitespace-pre-wrap"> <div class="poppins font-regular text-lyx-lightmode-text-dark dark:text-lyx-text-dark whitespace-pre-wrap">
{{ entry.text }} {{ entry.text }}
</div> </div>
</div> </div>
@@ -31,7 +31,7 @@ const props = defineProps<SettingsTemplateProp>();
<slot :name="entry.id"></slot> <slot :name="entry.id"></slot>
</div> </div>
</div> </div>
<div v-if="index < props.entries.length - 1" class="h-[2px] bg-lyx-widget-lighter w-full my-10"></div> <div v-if="index < props.entries.length - 1" class="h-[2px] bg-lyx-lightmode-widget dark:bg-lyx-widget-lighter w-full my-10"></div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -68,7 +68,7 @@ function getPremiumPrice(type: number) {
} }
const entries: SettingsTemplateEntry[] = [ const entries: SettingsTemplateEntry[] = [
{ id: 'plan', title: 'Current plan', text: 'Manage current plat for this project' }, { id: 'plan', title: 'Current plan', text: 'Manage current plan for this project' },
{ id: 'usage', title: 'Usage', text: 'Show usage of current project' }, { id: 'usage', title: 'Usage', text: 'Show usage of current project' },
{ id: 'info', title: 'Billing address', text: 'This will be reflected in every upcoming invoice,\npast invoices are not affected' }, { id: 'info', title: 'Billing address', text: 'This will be reflected in every upcoming invoice,\npast invoices are not affected' },
{ id: 'invoices', title: 'Invoices', text: 'Manage invoices of current project' }, { id: 'invoices', title: 'Invoices', text: 'Manage invoices of current project' },
@@ -127,25 +127,25 @@ const { showDrawer } = useDrawer();
<template #info> <template #info>
<div v-if="!isGuest"> <div v-if="!isGuest">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<LyxUiInput class="px-2 py-2 !bg-[#161616]" placeholder="Address line 1" <LyxUiInput class="px-2 py-2 dark:!bg-[#161616]" placeholder="Address line 1"
v-model="currentBillingInfo.line1"> v-model="currentBillingInfo.line1">
</LyxUiInput> </LyxUiInput>
<LyxUiInput class="px-2 py-2 !bg-[#161616]" placeholder="Address line 2" <LyxUiInput class="px-2 py-2 dark:!bg-[#161616]" placeholder="Address line 2"
v-model="currentBillingInfo.line2"> v-model="currentBillingInfo.line2">
</LyxUiInput> </LyxUiInput>
<div class="flex gap-4 w-full"> <div class="flex gap-4 w-full">
<LyxUiInput class="px-2 py-2 w-full !bg-[#161616]" placeholder="Country" <LyxUiInput class="px-2 py-2 w-full dark:!bg-[#161616]" placeholder="Country"
v-model="currentBillingInfo.country"> v-model="currentBillingInfo.country">
</LyxUiInput> </LyxUiInput>
<LyxUiInput class="px-2 py-2 w-full !bg-[#161616]" placeholder="Postal code" <LyxUiInput class="px-2 py-2 w-full dark:!bg-[#161616]" placeholder="Postal code"
v-model="currentBillingInfo.postal_code"> v-model="currentBillingInfo.postal_code">
</LyxUiInput> </LyxUiInput>
</div> </div>
<div class="flex gap-4 w-full"> <div class="flex gap-4 w-full">
<LyxUiInput class="px-2 py-2 w-full !bg-[#161616]" placeholder="City" <LyxUiInput class="px-2 py-2 w-full dark:!bg-[#161616]" placeholder="City"
v-model="currentBillingInfo.city"> v-model="currentBillingInfo.city">
</LyxUiInput> </LyxUiInput>
<LyxUiInput class="px-2 py-2 w-full !bg-[#161616]" placeholder="State" <LyxUiInput class="px-2 py-2 w-full dark:!bg-[#161616]" placeholder="State"
v-model="currentBillingInfo.state"> v-model="currentBillingInfo.state">
</LyxUiInput> </LyxUiInput>
</div> </div>
@@ -175,7 +175,7 @@ const { showDrawer } = useDrawer();
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="poppins font-semibold text-[2rem]"> <div class="poppins font-semibold text-[2rem]">
{{ getPremiumPrice(planData.premium_type) }} </div> {{ getPremiumPrice(planData.premium_type) }} </div>
<div class="poppins text-text-sub mt-2"> per month </div> <div class="poppins text-lyx-lightmode-text-dark dark:text-text-sub mt-2"> per month </div>
</div> </div>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
@@ -194,7 +194,7 @@ const { showDrawer } = useDrawer();
<div class="my-4 w-full bg-gray-400/30 h-[1px]"> <div class="my-4 w-full bg-gray-400/30 h-[1px]">
</div> </div>
<div class="flex justify-between px-8 flex-col lg:flex-row gap-2 lg:gap-0 items-center"> <div class="flex justify-between px-8 flex-col lg:flex-row gap-2 lg:gap-0 items-center">
<div class="flex gap-2 text-text-sub text-[.9rem]"> <div class="flex gap-2 text-lyx-lightmode-text-dark dark:text-text-sub text-[.9rem]">
<div class="poppins"> Expire date:</div> <div class="poppins"> Expire date:</div>
<div> {{ prettyExpireDate }}</div> <div> {{ prettyExpireDate }}</div>
</div> </div>
@@ -212,7 +212,7 @@ const { showDrawer } = useDrawer();
<div class="poppins font-semibold text-[1.1rem]"> <div class="poppins font-semibold text-[1.1rem]">
Usage Usage
</div> </div>
<div class="poppins text-text-sub text-[.9rem]"> <div class="poppins text-lyx-lightmode-text-dark dark:text-text-sub text-[.9rem]">
Check the usage limits of your project. Check the usage limits of your project.
</div> </div>
</div> </div>
@@ -240,7 +240,7 @@ const { showDrawer } = useDrawer();
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex justify-between items-center bg-[#161616] p-4 rounded-lg" <div class="flex justify-between items-center outline-[1px] outline outline-lyx-lightmode-widget dark:outline-none bg-lyx-lightmode-widget-light dark:bg-[#161616] p-4 rounded-lg"
v-for="invoice of invoices"> v-for="invoice of invoices">
<div> <i class="fal fa-file-invoice"></i> </div> <div> <i class="fal fa-file-invoice"></i> </div>

View File

@@ -3,5 +3,5 @@
const app = useRuntimeConfig(); const app = useRuntimeConfig();
export function useSelfhosted() { export function useSelfhosted() {
return app.public.SELFHOSTED === 'TRUE'; return app.public.SELFHOSTED.toString() === 'TRUE' || app.public.SELFHOSTED.toString() === 'true';
} }

View File

@@ -12,6 +12,8 @@ const modal = useModal();
const selfhosted = useSelfhosted(); const selfhosted = useSelfhosted();
console.log({ selfhosted })
const sections: Section[] = [ const sections: Section[] = [
{ {
title: '', title: '',
@@ -32,6 +34,7 @@ const sections: Section[] = [
action() { action() {
modal.open(DialogFeedback, {}); modal.open(DialogFeedback, {});
}, },
disabled: selfhosted
}, },
{ {
label: 'Documentation', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true, label: 'Documentation', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true,
@@ -92,11 +95,11 @@ const { isOpen, close, open } = useMenu();
</CVerticalNavigation> </CVerticalNavigation>
<div class="overflow-hidden w-full bg-lyx-background relative h-full"> <div class="overflow-hidden w-full bg-lyx-lightmode-background dark:bg-lyx-background relative h-full">
<div v-if="showDialog" class="barrier w-full h-full z-[34] absolute bg-black/50 backdrop-blur-[2px]"> <div v-if="showDialog" class="barrier w-full h-full z-[34] absolute bg-black/50 backdrop-blur-[2px]">
<i <i
class="z-[40] absolute right-12 top-8 fas fa-times text-text-sub text-[1.8rem] lg:text-[3rem]"></i> class="z-[40] absolute right-12 top-8 fas fa-times text-lyx-lightmode-text-dark dark:text-text-sub text-[1.8rem] lg:text-[3rem]"></i>
</div> </div>
<div @click="closeDialog()" class="w-full h-full z-[35] absolute top-0 left-0 px-4 lg:px-60 py-20" <div @click="closeDialog()" class="w-full h-full z-[35] absolute top-0 left-0 px-4 lg:px-60 py-20"

View File

@@ -45,7 +45,8 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
to.path != '/login' && to.path != '/login' &&
to.path != '/register' && to.path != '/register' &&
to.path != '/live_demo' && to.path != '/live_demo' &&
to.path != '/jwt_login' to.path != '/jwt_login' &&
to.path != '/forgot_password'
) { ) {
console.log('REDIRECT TO LOGIN', to.path); console.log('REDIRECT TO LOGIN', to.path);
return '/login'; return '/login';

View File

@@ -55,8 +55,8 @@ export default defineNuxtConfig({
STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST, STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST,
STRIPE_WH_SECRET_TEST: process.env.STRIPE_WH_SECRET_TEST, STRIPE_WH_SECRET_TEST: process.env.STRIPE_WH_SECRET_TEST,
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL, NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME || 'FALSE', NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME,
SELFHOSTED: process.env.SELFHOSTED, SELFHOSTED: process.env.SELFHOSTED || 'FALSE',
public: { public: {
AUTH_MODE: process.env.AUTH_MODE, AUTH_MODE: process.env.AUTH_MODE,
GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE', GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE',

View File

@@ -39,6 +39,9 @@
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/components": "^2.2.1",
"@nuxt/types": "^2.18.1",
"@nuxt/typescript-build": "^3.0.2",
"@nuxt/ui": "^2.15.2", "@nuxt/ui": "^2.15.2",
"@types/jsonwebtoken": "^9.0.6", "@types/jsonwebtoken": "^9.0.6",
"@types/pdfkit": "^0.13.4", "@types/pdfkit": "^0.13.4",

View File

@@ -4,6 +4,7 @@ import VueMarkdown from 'vue-markdown-render';
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const selfhosted = useSelfhosted();
const debugModeAi = ref<boolean>(false); const debugModeAi = ref<boolean>(false);
@@ -87,6 +88,7 @@ async function pollSendMessageStatus(chat_id: string, times: number, updateStatu
currentChatMessages.value = messages.map(e => ({ ...e, charts: e.charts.map(k => JSON.parse(k)) })) as any; currentChatMessages.value = messages.map(e => ({ ...e, charts: e.charts.map(k => JSON.parse(k)) })) as any;
currentChatMessageDelta.value = ''; currentChatMessageDelta.value = '';
loading.value = false;
} }
} }
@@ -268,7 +270,9 @@ async function clearAllChats() {
</div> </div>
<div class="flex flex-col xl:grid xl:grid-cols-2 gap-4 mt-6"> <div class="flex flex-col xl:grid xl:grid-cols-2 gap-4 mt-6">
<div v-for="prompt of defaultPrompts" @click="currentText = prompt" <div v-for="prompt of defaultPrompts" @click="currentText = prompt"
class="bg-lyx-widget-light hover:bg-lyx-widget-lighter cursor-pointer p-4 rounded-lg poppins text-center whitespace-pre-wrap flex items-center justify-center text-[.9rem]"> class="
bg-lyx-lightmode-widget-light dark:bg-lyx-widget-light hover:bg-lyx-lightmode-widget dark:hover:bg-lyx-widget outline-[1px] outline outline-lyx-lightmode-widget dark:outline-none
cursor-pointer p-4 rounded-lg poppins text-center whitespace-pre-wrap flex items-center justify-center text-[.9rem]">
{{ prompt }} {{ prompt }}
</div> </div>
</div> </div>
@@ -280,7 +284,7 @@ async function clearAllChats() {
<div class="flex w-full flex-col" v-for="(message, messageIndex) of currentChatMessages"> <div class="flex w-full flex-col" v-for="(message, messageIndex) of currentChatMessages">
<div v-if="message.role === 'user'" class="flex justify-end w-full poppins text-[1.1rem]"> <div v-if="message.role === 'user'" class="flex justify-end w-full poppins text-[1.1rem]">
<div class="bg-lyx-widget-light px-5 py-3 rounded-lg"> <div class="bg-lyx-lightmode-widget dark:bg-lyx-widget-light px-5 py-3 rounded-lg">
{{ message.content }} {{ message.content }}
</div> </div>
</div> </div>
@@ -290,7 +294,7 @@ async function clearAllChats() {
<div class="flex items-center justify-center shrink-0"> <div class="flex items-center justify-center shrink-0">
<img class="h-[3.5rem] w-auto" :src="'analyst.png'"> <img class="h-[3.5rem] w-auto" :src="'analyst.png'">
</div> </div>
<div class="max-w-[70%] text-text/90 ai-message"> <div class="max-w-[70%] text-lyx-lightmode-text dark:text-text/90 ai-message">
<vue-markdown v-if="message.content" :source="message.content" :options="{ <vue-markdown v-if="message.content" :source="message.content" :options="{
html: true, html: true,
@@ -364,9 +368,10 @@ async function clearAllChats() {
<div class="flex gap-2 items-center md:absolute fixed bottom-8 left-0 w-full px-10 xl:px-28"> <div class="flex gap-2 items-center md:absolute fixed bottom-8 left-0 w-full px-10 xl:px-28">
<input @keydown="onKeyDown" v-model="currentText" <input @keydown="onKeyDown" v-model="currentText"
class="bg-lyx-widget-light w-full focus:outline-none px-4 py-2 rounded-lg" type="text"> class="bg-lyx-lightmode-widget-light dark:bg-lyx-widget-light w-full dark:focus:outline-none px-4 py-2 rounded-lg outline-[1px] outline outline-lyx-lightmode-widget dark:outline-none"
type="text">
<div @click="sendMessage()" <div @click="sendMessage()"
class="bg-lyx-widget-light hhover:bg-lyx-widget-light cursor-pointer px-4 py-2 rounded-full"> class="bg-lyx-lightmode-widget-light hover:bg-lyx-lightmode-widget dark:bg-lyx-widget-light dark:hover:bg-lyx-widget-light cursor-pointer px-4 py-2 rounded-full">
<i class="far fa-arrow-up"></i> <i class="far fa-arrow-up"></i>
</div> </div>
<div @click="menuOpen = !menuOpen" <div @click="menuOpen = !menuOpen"
@@ -381,7 +386,8 @@ async function clearAllChats() {
<div :class="{ <div :class="{
'absolute top-0 left-0 w-full': menuOpen, 'absolute top-0 left-0 w-full': menuOpen,
'hidden xl:flex': !menuOpen 'hidden xl:flex': !menuOpen
}" class="flex-[2] bg-lyx-background-light p-6 flex flex-col gap-4 h-full overflow-hidden"> }"
class="flex-[2] bg-lyx-lightmode-background border-l-[1px] dark:border-l-0 dark:bg-lyx-background-light p-6 flex flex-col gap-4 h-full overflow-hidden">
<div class="gap-2 flex flex-col"> <div class="gap-2 flex flex-col">
<div class="xl:hidden absolute right-6 top-2 text-[1.5rem]"> <div class="xl:hidden absolute right-6 top-2 text-[1.5rem]">
@@ -396,10 +402,11 @@ async function clearAllChats() {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="bg-accent w-5 h-5 rounded-full animate-pulse"> <div class="bg-accent w-5 h-5 rounded-full animate-pulse">
</div> </div>
<div class="manrope font-semibold text-text-dirty"> {{ chatsRemaining }} remaining requests <div class="manrope font-semibold text-lyx-lightmode-text dark:text-text-dirty"> {{
chatsRemaining }} remaining requests
</div> </div>
</div> </div>
<LyxUiButton type="primary" class="text-[.9rem] text-center " @click="showDrawer('PRICING')"> <LyxUiButton v-if="!selfhosted" type="primary" class="text-[.9rem] text-center " @click="showDrawer('PRICING')">
Upgrade Upgrade
</LyxUiButton> </LyxUiButton>
</div> </div>
@@ -414,7 +421,7 @@ async function clearAllChats() {
<div class="px-2"> <div class="px-2">
<div @click="openChat()" <div @click="openChat()"
class="bg-lyx-widget-light cursor-pointer hover:bg-lyx-widget rounded-lg px-4 py-3 poppins flex gap-4 items-center"> class="bg-lyx-lightmode-widget-light hover:bg-lyx-lightmode-widget dark:bg-lyx-widget-light cursor-pointer dark:hover:bg-lyx-widget rounded-lg px-4 py-3 poppins flex gap-4 items-center outline-[1px] outline outline-lyx-lightmode-widget dark:outline-none">
<div> <i class="fas fa-plus"></i> </div> <div> <i class="fas fa-plus"></i> </div>
<div> New chat </div> <div> New chat </div>
</div> </div>
@@ -424,7 +431,7 @@ async function clearAllChats() {
<div class="overflow-y-auto"> <div class="overflow-y-auto">
<div class="flex flex-col gap-2 px-2"> <div class="flex flex-col gap-2 px-2">
<div :class="{ '!bg-accent/60': chat._id.toString() === currentChatId }" <div :class="{ '!bg-accent/60': chat._id.toString() === currentChatId }"
class="flex text-lyx-text-dark text-[.9rem] font-light rounded-lg items-center gap-4 w-full px-4 bg-lyx-widget-light hover:bg-lyx-widget" class="flex text-lyx-lightmode-text-dark dark:text-lyx-text-dark text-[.9rem] font-light rounded-lg items-center gap-4 w-full px-4 bg-lyx-lightmode-widget-light dark:bg-lyx-widget-light hover:bg-lyx-lightmode-widget dark:hover:bg-lyx-widget outline-[1px] outline outline-lyx-lightmode-widget dark:outline-none"
v-for="chat of viewChatsList"> v-for="chat of viewChatsList">
<i @click="deleteChat(chat._id.toString())" <i @click="deleteChat(chat._id.toString())"
class="far fa-trash hover:text-gray-300 cursor-pointer"></i> class="far fa-trash hover:text-gray-300 cursor-pointer"></i>
@@ -458,7 +465,6 @@ async function clearAllChats() {
font-weight: bold; font-weight: bold;
margin-top: 1.5em; margin-top: 1.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
color: white;
} }
p:last-of-type { p:last-of-type {

View File

@@ -73,7 +73,7 @@ const showWarning = computed(() => {
const { showDrawer } = useDrawer(); const { showDrawer } = useDrawer();
function goToUpgrade() { function goToUpgrade() {
showDrawer('PRICING'); showDrawer('PRICING');
} }
</script> </script>
@@ -86,7 +86,7 @@ function goToUpgrade() {
<div class="w-full h-dvh flex flex-col"> <div class="w-full h-dvh flex flex-col">
<div v-if="creatingCsv" <div v-if="creatingCsv"
class="fixed z-[100] flex items-center justify-center left-0 top-0 w-full h-full bg-black/60 backdrop-blur-[4px]"> class="fixed z-[100] flex items-center justify-center left-0 top-0 w-full h-full bg-lyx-lightmode-background-light dark:bg-black/60 backdrop-blur-[4px]">
<div class="poppins text-[2rem]"> <div class="poppins text-[2rem]">
Creating csv... Creating csv...
</div> </div>
@@ -101,11 +101,11 @@ function goToUpgrade() {
</div> </div>
<div class="w-[15rem] flex flex-col gap-0"> <div class="w-[15rem] flex flex-col gap-0">
<USelectMenu :uiMenu="{ <USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter', select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
base: '!bg-lyx-widget', base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget',
option: { option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer', base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter' active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
} }
}" v-model="selectedTimeFrom" :options="options"></USelectMenu> }" v-model="selectedTimeFrom" :options="options"></USelectMenu>
</div> </div>
@@ -125,14 +125,14 @@ function goToUpgrade() {
<UTable v-if="tableData" class="utable px-8" :ui="{ <UTable v-if="tableData" class="utable px-8" :ui="{
wrapper: 'overflow-auto w-full h-full', wrapper: 'overflow-auto w-full h-full',
thead: 'sticky top-0 bg-menu', thead: 'sticky top-0 bg-lyx-lightmode-background-light dark:bg-menu',
td: { td: {
color: 'text-[#ffffffb3]', color: 'text-lyx-lightmode-text dark:text-[#ffffffb3]',
base: 'border-r border-l border-gray-300/20' base: 'border-r border-l border-lyx-lightmode-widget dark:border-gray-300/20'
}, },
th: { color: 'text-text-sub' }, th: { color: 'text-lyx-lightmode-text dark:text-text-sub' },
tbody: 'divide-y divide-gray-300/20', tbody: 'divide-y divide-lyx-lightmode-widget dark:divide-gray-300/20',
divide: '', divide: '',
}" v-model:sort="sort" :columns="selectedColumns" :rows="tableData" :loading="loadingData" sort-mode="manual"> }" v-model:sort="sort" :columns="selectedColumns" :rows="tableData" :loading="loadingData" sort-mode="manual">

View File

@@ -91,7 +91,7 @@ function goToUpgrade() {
<div class="w-full h-dvh flex flex-col"> <div class="w-full h-dvh flex flex-col">
<div v-if="creatingCsv" <div v-if="creatingCsv"
class="fixed z-[100] flex items-center justify-center left-0 top-0 w-full h-full bg-black/60 backdrop-blur-[4px]"> class="fixed z-[100] flex items-center justify-center left-0 top-0 w-full h-full bg-lyx-lightmode-background-light dark:bg-black/60 backdrop-blur-[4px]">
<div class="poppins text-[2rem]"> <div class="poppins text-[2rem]">
Creating csv... Creating csv...
</div> </div>
@@ -106,11 +106,11 @@ function goToUpgrade() {
</div> </div>
<div class="w-[15rem] flex flex-col gap-0"> <div class="w-[15rem] flex flex-col gap-0">
<USelectMenu :uiMenu="{ <USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter', select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
base: '!bg-lyx-widget', base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget',
option: { option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer', base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter' active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
} }
}" v-model="selectedTimeFrom" :options="options"></USelectMenu> }" v-model="selectedTimeFrom" :options="options"></USelectMenu>
</div> </div>
@@ -131,13 +131,13 @@ function goToUpgrade() {
<UTable v-if="tableData" class="utable px-8" :ui="{ <UTable v-if="tableData" class="utable px-8" :ui="{
wrapper: 'overflow-auto w-full h-full', wrapper: 'overflow-auto w-full h-full',
thead: 'sticky top-0 bg-menu', thead: 'sticky top-0 bg-lyx-lightmode-background-light dark:bg-menu',
td: { td: {
color: 'text-[#ffffffb3]', color: 'text-lyx-lightmode-text dark:text-[#ffffffb3]',
base: 'border-r border-l border-gray-300/20' base: 'border-r border-l border-lyx-lightmode-widget dark:border-gray-300/20'
}, },
th: { color: 'text-text-sub' }, th: { color: 'text-lyx-lightmode-text dark:text-text-sub' },
tbody: 'divide-y divide-gray-300/20', tbody: 'divide-y divide-lyx-lightmode-widget dark:divide-gray-300/20',
divide: '', divide: '',
}" v-model:sort="sort" :columns="selectedColumns" :rows="tableData" :loading="loadingData" sort-mode="manual"> }" v-model:sort="sort" :columns="selectedColumns" :rows="tableData" :loading="loadingData" sort-mode="manual">

View File

@@ -0,0 +1,150 @@
<script setup lang="ts">
definePageMeta({ layout: 'none' });
const email = ref<string>("");
const emailSended = ref<boolean>(false);
const { createAlert } = useAlert();
async function resetPassword() {
try {
const res = await $fetch('/api/user/password/reset', {
headers: { 'Content-Type': 'application/json' },
method: 'POST',
body: JSON.stringify({ email: email.value })
})
if (!res) throw Error('No response');
if (res.error) return createAlert('Error', res.message, 'far fa-triangle-exclamation', 5000);
emailSended.value = true;
return createAlert('Success', 'Email sent', 'far fa-circle-check', 5000);
} catch (ex) {
console.error(ex);
createAlert('Error', 'Internal error', 'far fa-triangle-exclamation', 5000);
}
}
</script>
<template>
<div class="home w-full h-full">
<div class="flex h-full">
<div class="flex-1 flex flex-col items-center pt-20 xl:pt-[22vh]">
<div class="rotating-thing absolute top-0"></div>
<div class="mb-8 bg-black rounded-xl">
<img class="w-[5rem]" :src="'logo.png'">
</div>
<div class="text-lyx-lightmode-text dark:text-lyx-text text-[2.2rem] font-bold poppins">
Reset password
</div>
<div class="text-lyx-lightmode-text dark:text-lyx-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2">
Enter your user account's verified email address and we will send you a temporary password.
</div>
<div class="mt-12">
<div v-if="!emailSended" class="flex flex-col gap-2 z-[110]">
<div class="flex flex-col gap-4 z-[100] w-[20vw] min-w-[20rem]">
<LyxUiInput class="px-3 py-2" placeholder="Email" v-model="email"></LyxUiInput>
</div>
<div class="flex justify-center mt-4">
<LyxUiButton @click="resetPassword()" class="text-center z-[110]" type="primary">
Reset password
</LyxUiButton>
</div>
<NuxtLink to="/login"
class="mt-4 text-center text-lyx-lightmode-text dark:text-lyx-text-dark underline cursor-pointer z-[110]">
Go back
</NuxtLink>
</div>
<div v-if="emailSended" class="mt-12 flex flex-col text-center text-[1.1rem] z-[100]">
<div>
Check your email inbox.
</div>
<RouterLink tag="div" to="/login"
class="mt-6 text-center text-lyx-lightmode-text dark:text-lyx-text-dark underline cursor-pointer z-[110]">
Go back
</RouterLink>
</div>
</div>
</div>
<div class="grow flex-1 items-center justify-center hidden lg:flex">
<!-- <GlobeSvg></GlobeSvg> -->
<img :src="'image-bg.png'" class="h-full py-6">
</div>
</div>
<!-- <div class="flex flex-col items-center justify-center mt-40 gap-20">
<div class="google-login text-gray-700" :class="{ disabled: !isReady }" @click="login">
<div class="icon">
<i class="fab fa-google"></i>
</div>
<div> Continua con Google </div>
</div>
</div> -->
</div>
</template>
<style scoped lang="scss">
.rotating-thing {
height: 100%;
aspect-ratio: 1 / 1;
opacity: 0.15;
background: radial-gradient(51.24% 31.29% at 50% 50%, rgb(51, 58, 232) 0%, rgba(51, 58, 232, 0) 100%);
animation: 12s linear 0s infinite normal none running spin;
}
.google-login {
cursor: pointer;
font-weight: bold;
background-color: #fcefed;
padding: 1rem 2rem;
border-radius: 1rem;
display: flex;
gap: 1rem;
align-items: center;
&.disabled {
filter: brightness(50%);
}
i {
font-size: 1.5rem;
}
}
</style>

View File

@@ -154,6 +154,7 @@ async function signInWithCredentials() {
} }
} }
</script> </script>
@@ -161,21 +162,21 @@ async function signInWithCredentials() {
<div class="home w-full h-full"> <div class="home w-full h-full">
<div class="flex h-full"> <div class="flex h-full bg-lyx-lightmode-background dark:bg-lyx-background">
<div class="flex-1 flex flex-col items-center pt-20 xl:pt-[22vh]"> <div class="flex-1 flex flex-col items-center pt-20 xl:pt-[22vh]">
<div class="rotating-thing absolute top-0"></div> <!-- <div class="rotating-thing absolute top-0"></div> -->
<div class="mb-8 bg-black rounded-xl"> <div class="mb-8 bg-black rounded-xl">
<img class="w-[5rem]" :src="'logo.png'"> <img class="w-[5rem]" :src="'logo.png'">
</div> </div>
<div class="text-text text-[2.2rem] font-bold poppins"> <div class="text-lyx-lightmode-text dark:text-lyx-text text-[2.2rem] font-bold poppins">
Sign in Sign in
</div> </div>
<div class="text-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2"> <div class="text-lyx-lightmode-text/80 dark:text-lyx-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2">
Track web analytics and custom events Track web analytics and custom events
with extreme simplicity in under 30 sec. with extreme simplicity in under 30 sec.
<br> <br>
@@ -195,6 +196,13 @@ async function signInWithCredentials() {
</LyxUiInput> </LyxUiInput>
</div> </div>
<div class="flex justify-end">
<RouterLink tag="div" to="/forgot_password"
class="text-center text-lyx-lightmode-text dark:text-lyx-text-dark underline cursor-pointer z-[110]">
Forgot password?
</RouterLink>
</div>
<div class="flex justify-center mt-4 z-[100]"> <div class="flex justify-center mt-4 z-[100]">
<LyxUiButton @click="signInWithCredentials()" class="text-center" type="primary"> <LyxUiButton @click="signInWithCredentials()" class="text-center" type="primary">
Sign in Sign in
@@ -202,17 +210,17 @@ async function signInWithCredentials() {
</div> </div>
<div @click="goBackToEmailLogin()" <div @click="goBackToEmailLogin()"
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]"> class="mt-4 text-center text-lyx-lightmode-text dark:text-lyx-text-dark underline cursor-pointer z-[100]">
Go back Go back
</div> </div>
</div> </div>
<div v-if="!isNoAuth && !isEmailLogin" class="flex flex-col gap-2"> <div v-if="!isNoAuth && !isEmailLogin" class="flex flex-col text-lyx-lightmode-text dark:text-lyx-text gap-2">
<div @click="login" <div @click="login"
class="hover:bg-lyx-primary cursor-pointer flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]"> class="hover:bg-lyx-primary bg-white dark:bg-transparent cursor-pointer flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]">
<div class="flex items-center"> <div class="flex items-center">
<i class="fab fa-google"></i> <i class="fab fa-google"></i>
</div> </div>
@@ -220,7 +228,7 @@ async function signInWithCredentials() {
</div> </div>
<div @click="isEmailLogin = true" <div @click="isEmailLogin = true"
class="hover:bg-[#262626] cursor-pointer flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]"> class="hover:bg-[#d3d3d3] dark:hover:bg-[#262626] bg-white dark:bg-transparent cursor-pointer flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]">
<div class="flex items-center"> <div class="flex items-center">
<i class="far fa-envelope"></i> <i class="far fa-envelope"></i>
</div> </div>
@@ -228,10 +236,14 @@ async function signInWithCredentials() {
</div> </div>
<RouterLink tag="div" to="/register" <div class="flex flex-col gap-2 mt-4">
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
You don't have an account ? Sign up <RouterLink tag="div" to="/register"
</RouterLink> class="text-center text-lyx-lightmode-text-dark dark:text-lyx-text-dark underline cursor-pointer z-[100]">
You don't have an account ? Sign up
</RouterLink>
</div>
</div> </div>
@@ -245,7 +257,7 @@ async function signInWithCredentials() {
</div> </div>
<div class="text-[.9rem] poppins mt-20 text-text-sub text-center relative z-[2]"> <div class="text-[.9rem] poppins mt-20 text-lyx-lightmode-text-dark dark:text-lyx-text-dark text-center relative z-[2]">
By continuing you are accepting By continuing you are accepting
<br> <br>
our our

View File

@@ -56,6 +56,7 @@ async function createProject() {
} }
</script> </script>
@@ -69,7 +70,7 @@ async function createProject() {
<div class="flex flex-col items-center justify-center pt-[12rem] gap-12 relative z-[10]"> <div class="flex flex-col items-center justify-center pt-[12rem] gap-12 relative z-[10]">
<div class="text-[3rem] font-semibold text-center"> <div class="text-[3rem] font-semibold text-center text-lyx-lightmode-text dark:text-lyx-text">
Create your {{ isFirstProject ? 'first' : '' }} project Create your {{ isFirstProject ? 'first' : '' }} project
</div> </div>
@@ -78,15 +79,20 @@ async function createProject() {
</div> </div>
<div class="w-[20rem] flex flex-col gap-2"> <div class="w-[20rem] flex flex-col gap-2">
<div class="text-lg text-text-sub font-semibold"> <div class="text-lg text-lyx-lightmode-text-dark dark:text-text-sub font-semibold">
{{ isFirstProject ? 'Choose a name' : 'Project name' }} {{ isFirstProject ? 'Choose a name' : 'Project name' }}
</div> </div>
<CInput placeholder="ProjectName" :readonly="creating" v-model="projectName"></CInput> <!-- <CInput placeholder="ProjectName" :readonly="creating" v-model="projectName"></CInput> -->
<LyxUiInput class="py-2 px-2" placeholder="Insert" :readonly="creating" v-model="projectName">
</LyxUiInput>
</div> </div>
<div> <div>
<CButton :loading="creating" @click="createProject()" :disabled="projectName.length < 2"
class="rounded-lg w-[10rem] text-md font-semibold" label="Create"></CButton> <LyxUiButton type="primary" @click="createProject()" :disabled="projectName.length < 2">
Create
</LyxUiButton>
</div> </div>
</div> </div>

View File

@@ -47,21 +47,21 @@ async function registerAccount() {
<div class="home w-full h-full"> <div class="home w-full h-full">
<div class="flex h-full"> <div class="flex h-full bg-lyx-lightmode-background dark:bg-lyx-background">
<div class="flex-1 flex flex-col items-center pt-20 xl:pt-[22vh]"> <div class="flex-1 flex flex-col items-center pt-20 xl:pt-[22vh]">
<div class="rotating-thing absolute top-0"></div> <!-- <div class="rotating-thing absolute top-0"></div> -->
<div class="mb-8 bg-black rounded-xl"> <div class="mb-8 bg-black rounded-xl">
<img class="w-[5rem]" :src="'logo.png'"> <img class="w-[5rem]" :src="'logo.png'">
</div> </div>
<div class="text-text text-[2.2rem] font-bold poppins"> <div class="text-lyx-lightmode-text dark:text-lyx-text text-[2.2rem] font-bold poppins">
Sign up Sign up
</div> </div>
<div class="text-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2"> <div class="text-lyx-lightmode-text dark:text-lyx-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2">
Track web analytics and custom events Track web analytics and custom events
with extreme simplicity in under 30 sec. with extreme simplicity in under 30 sec.
<br> <br>
@@ -95,7 +95,7 @@ async function registerAccount() {
</div> </div>
<RouterLink to="/login" <RouterLink to="/login"
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]"> class="mt-4 text-center text-lyx-lightmode-text dark:text-lyx-text-dark underline cursor-pointer z-[100]">
Go back to login Go back to login
</RouterLink> </RouterLink>
@@ -112,13 +112,13 @@ async function registerAccount() {
Please check your inbox to confirm your account and complete your registration. Please check your inbox to confirm your account and complete your registration.
</div> </div>
<RouterLink tag="div" to="/login" <RouterLink tag="div" to="/login"
class="mt-6 text-center text-lyx-text-dark underline cursor-pointer"> class="mt-6 text-center text-lyx-lightmode-text dark:text-lyx-text-dark underline cursor-pointer">
Go back Go back
</RouterLink> </RouterLink>
</div> </div>
<div v-if="!emailSended" <div v-if="!emailSended"
class="text-[.9rem] poppins mt-5 xl:mt-20 text-text-sub text-center relative z-[2]"> class="text-[.9rem] poppins mt-5 xl:mt-20 text-lyx-lightmode-text dark:text-lyx-text-dark text-center relative z-[2]">
By continuing you are accepting By continuing you are accepting
<br> <br>
our our

View File

@@ -33,7 +33,7 @@ const columns = [
<div class="home w-full h-full px-10 pt-6 overflow-y-auto"> <div class="home w-full h-full px-10 pt-6 overflow-y-auto">
<div class="flex gap-2 items-center text-text/90 justify-end"> <div class="flex gap-2 items-center text-lyx-lightmode-text dark:text-text/90 justify-end">
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div> <div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
<div class="poppins font-regular text-[1rem]"> AI Anomaly Detector </div> <div class="poppins font-regular text-[1rem]"> AI Anomaly Detector </div>
<div class="flex items-center"> <div class="flex items-center">
@@ -47,7 +47,7 @@ const columns = [
<template #scan-data="{ row }"> <template #scan-data="{ row }">
<div class="text-lyx-text-dark"> <div class="text-lyx-lightmode-text dark:text-lyx-text-dark">
{{ new Date(row.data.created_at).toLocaleString() }} {{ new Date(row.data.created_at).toLocaleString() }}
</div> </div>
</template> </template>
@@ -59,7 +59,7 @@ const columns = [
</template> </template>
<template #data-data="{ row }"> <template #data-data="{ row }">
<div class="text-lyx-text-dark"> <div class="text-lyx-lightmode-text dark:text-lyx-text-dark">
<div v-if="row.type === 'domain'"> <div v-if="row.type === 'domain'">
{{ row.data.domain }} {{ row.data.domain }}
</div> </div>

View File

@@ -2,6 +2,7 @@
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const selfhosted = useSelfhosted();
const items = [ const items = [
{ label: 'General', slot: 'general' }, { label: 'General', slot: 'general' },
@@ -16,7 +17,7 @@ const items = [
<template> <template>
<div class="lg:px-10 lg:py-8 h-dvh overflow-y-auto overflow-x-hidden hide-scrollbars"> <div class="lg:px-10 lg:py-8 h-dvh overflow-y-auto overflow-x-hidden hide-scrollbars">
<div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Settings </div> <div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Settings </div>
<CustomTab :items="items" class="mt-8"> <CustomTab :items="items" class="mt-8">
@@ -30,10 +31,18 @@ const items = [
<SettingsMembers :key="refreshKey"></SettingsMembers> <SettingsMembers :key="refreshKey"></SettingsMembers>
</template> </template>
<template #billing> <template #billing>
<SettingsBilling :key="refreshKey"></SettingsBilling> <SettingsBilling v-if="!selfhosted" :key="refreshKey"></SettingsBilling>
<div class="flex popping text-[1.2rem] font-semibold justify-center mt-[20vh] text-lyx-lightmode-text dark:text-lyx-text"
v-if="selfhosted">
Billing disabled in self-host mode
</div>
</template> </template>
<template #codes> <template #codes>
<SettingsCodes :key="refreshKey"></SettingsCodes> <SettingsCodes v-if="!selfhosted" :key="refreshKey"></SettingsCodes>
<div class="flex popping text-[1.2rem] font-semibold justify-center mt-[20vh] text-lyx-lightmode-text dark:text-lyx-text"
v-if="selfhosted">
Codes disabled in self-host mode
</div>
</template> </template>
<template #account> <template #account>
<SettingsAccount :key="refreshKey"></SettingsAccount> <SettingsAccount :key="refreshKey"></SettingsAccount>

View File

@@ -1,11 +1,17 @@
import { OnboardingModel } from '@schema/OnboardingSchema'; import { OnboardingModel } from '@schema/OnboardingSchema';
const { SELFHOSTED } = useRuntimeConfig();
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const data = await getRequestData(event); const data = await getRequestData(event);
if (!data) return; if (!data) return;
const exist = await OnboardingModel.exists({ user_id: data.user.id }); const exist = await OnboardingModel.exists({ user_id: data.user.id });
if (SELFHOSTED.toString() === 'TRUE' || SELFHOSTED.toString() === 'true') {
return { exists: true }
}
return { exist: exist != null } return { exist: exist != null }
}); });

View File

@@ -66,7 +66,7 @@ export default defineEventHandler(async event => {
const { project, project_id, user } = data; const { project, project_id, user } = data;
if (SELFHOSTED !== 'TRUE') { if (SELFHOSTED.toString() !== 'TRUE' && SELFHOSTED.toString() !== 'true') {
const PREMIUM_TYPE = project.premium_type; const PREMIUM_TYPE = project.premium_type;
if (PREMIUM_TYPE === 0) return setResponseStatus(event, 400, 'Project not premium'); if (PREMIUM_TYPE === 0) return setResponseStatus(event, 400, 'Project not premium');
} }

View File

@@ -0,0 +1,10 @@
import { PasswordModel } from "@schema/PasswordSchema";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
const hasPassword = await PasswordModel.exists({ email: userData.user.email });
if (hasPassword) return { can_change: true };
return { can_change: false }
});

View File

@@ -0,0 +1,33 @@
import crypto from 'crypto';
import { PasswordModel } from '@schema/PasswordSchema';
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
const { old_password, new_password } = await readBody(event);
if (new_password.length < 6) return { error: true, message: 'Password too short' }
const target = await PasswordModel.findOne({ email: userData.user.email });
if (!target) return { error: true, message: 'Internal error. User not found.' }
const hashOld = crypto.createHash('sha256');
const hashedPasswordOld = hashOld.update(old_password + '_litlyx').digest('hex');
if (target.password !== hashedPasswordOld) {
return { error: true, message: 'Old password not correct' }
}
const hashNew = crypto.createHash('sha256');
const hashedPasswordNew = hashNew.update(new_password + '_litlyx').digest('hex');
target.password = hashedPasswordNew;
await target.save();
return { error: false, message: 'Success' }
});

View File

@@ -0,0 +1,26 @@
import crypto from 'crypto';
import { PasswordModel } from '@schema/PasswordSchema';
import EmailService from '@services/EmailService'
export default defineEventHandler(async event => {
const { email } = await readBody(event);
const target = await PasswordModel.findOne({ email });
if (!target) return { error: true, message: 'Internal error. User not found.' }
const newPass = crypto.randomBytes(5).toString('hex');
const hash = crypto.createHash('sha256');
const hashedPassword = hash.update(newPass + '_litlyx').digest('hex');
target.password = hashedPassword;
await target.save();
await EmailService.sendResetPasswordEmail(email, newPass);
return { error: false, message: 'Password changed' }
});

View File

@@ -42,6 +42,12 @@ export default async () => {
logger.info('[SERVER] Completed'); logger.info('[SERVER] Completed');
logger.warn('[ANOMALY LOOP] Disabled'); logger.warn('[ANOMALY LOOP] Disabled');
logger.warn(`[SELFHOSTED_SERVER] ${config.SELFHOSTED}`);
logger.warn(`[SELFHOSTED_CLIENT] ${config.public.SELFHOSTED}`);
logger.warn(`[AUTH] ${config.public.AUTH_MODE}`);
// anomalyLoop(); // anomalyLoop();
}; };

View File

@@ -34,16 +34,31 @@ module.exports = {
dark: '#222A42', dark: '#222A42',
hover: '#2A3450' hover: '#2A3450'
}, },
"lyx-lightmode-text": {
DEFAULT: '#0A0A0A',
dark: '#121212',
darker: '#212121',
},
"lyx-text": { "lyx-text": {
DEFAULT: '#FFFFFF', DEFAULT: '#FFFFFF',
dark: '#D4D4D4', dark: '#D4D4D4',
darker: '#6A6A6A' darker: '#6A6A6A',
},
"lyx-lightmode-widget": {
DEFAULT: '#D9D9E0',
light: '#EEEDEF',
lighter: '#FF0000'
}, },
"lyx-widget": { "lyx-widget": {
DEFAULT: '#0E0E0E', DEFAULT: '#0E0E0E',
light: '#1E1E1E', light: '#1E1E1E',
lighter: '#262626' lighter: '#262626'
}, },
"lyx-lightmode-background": {
DEFAULT: '#F2F2F2',
light: '#D4D4D4',
lighter: '#6A6A6A'
},
"lyx-background": { "lyx-background": {
DEFAULT: '#0A0A0A', DEFAULT: '#0A0A0A',
light: '#121212', light: '#121212',

View File

@@ -95,6 +95,7 @@ services:
NUXT_NOAUTH_USER_NAME: "defaultuser" NUXT_NOAUTH_USER_NAME: "defaultuser"
NUXT_SELFHOSTED: 'true' NUXT_SELFHOSTED: 'true'
NUXT_PUBLIC_SELFHOSTED: 'true'
# Optional - Used for tests # Optional - Used for tests
# NUXT_STRIPE_SECRET_TEST: "" # NUXT_STRIPE_SECRET_TEST: ""
@@ -111,4 +112,4 @@ services:
# GOOGLE_AUTH_CLIENT_ID: "" # GOOGLE_AUTH_CLIENT_ID: ""
volumes: volumes:
mongo-data: mongo-data:

840
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ import { PURCHASE_EMAIL } from './email_templates/PurchaseEmail';
import { ANOMALY_VISITS_EVENTS_EMAIL } from './email_templates/AnomalyUsageEmail'; import { ANOMALY_VISITS_EVENTS_EMAIL } from './email_templates/AnomalyUsageEmail';
import { ANOMALY_DOMAIN_EMAIL } from './email_templates/AnomalyDomainEmail'; import { ANOMALY_DOMAIN_EMAIL } from './email_templates/AnomalyDomainEmail';
import { CONFIRM_EMAIL } from './email_templates/ConfirmEmail'; import { CONFIRM_EMAIL } from './email_templates/ConfirmEmail';
import { RESET_PASSWORD_EMAIL } from './email_templates/ResetPasswordEmail';
class EmailService { class EmailService {
@@ -168,6 +169,24 @@ class EmailService {
} }
} }
async sendResetPasswordEmail(target: string, newPassword: string) {
try {
const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "Password reset";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = RESET_PASSWORD_EMAIL
.replace(/\[NEW_PASSWORD\]/, newPassword)
.toString();
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

@@ -0,0 +1,109 @@
export const RESET_PASSWORD_EMAIL = `<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
<!--$-->
</head>
<body
style='background-color:#fff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif'>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="max-width:37.5em">
<tbody>
<tr style="width:100%">
<td>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:1px solid rgb(0,0,0, 0.1);border-radius:3px;overflow:hidden">
<tbody>
<tr>
<td>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0"
role="presentation">
<tbody style="width:100%">
<tr style="width:100%">
<img src="https://litlyx.com/images/locker2.png"
style="display:block;outline:none;border:none;text-decoration:none;max-width:100%"
width="620" />
</tr>
</tbody>
</table>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0"
role="presentation" style="padding:20px;padding-bottom:0">
<tbody style="width:100%">
<tr style="width:100%">
<td data-id="__react-email-column">
<h1 style="font-size:32px;font-weight:bold;text-align:center">
Dear user
</h1>
<h2 style="font-size:26px;font-weight:bold;text-align:center">
Below is the temporary password for logging in again to the
Litlyx dashboard.
</h2>
<p style="font-size:16px;line-height:24px;margin:16px 0">
<b>Temporary Password: </b> [NEW_PASSWORD]
<!-- September 7, 2022 at 10:58 AM -->
</p>
<p style="font-size:16px;line-height:24px;margin:16px 0">
Please ensure that you change your password as soon as possible.
To do so, go to <b>Settings > Account</b> in the dashboard. <br>
If you need further assistance, feel free to contact us at
<a href="mailto:help@litlyx.com">help@litlyx.com</a>.
</p>
</td>
</tr>
</tbody>
</table>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0"
role="presentation" style="padding:20px;padding-top:0">
<tbody style="width:100%">
<tr style="width:100%">
<td colspan="2" data-id="__react-email-column"
style="display:flex;justify-content:center;width:100%">
<a style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#5680f8;border-radius:3px;color:#FFF;font-weight:bold;border:1px solid rgb(0,0,0, 0.1);cursor:pointer;padding:12px 30px 12px 30px"
target="_blank"
href="https://dashboard.litlyx.com"><span></span><span
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">
Go to Dashboard</span><span></span></a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="padding:45px 0 0 0">
<tbody>
<tr>
<td>
<img src="https://react-email-demo-lpdmf0ryo-resend.vercel.app/static/yelp-footer.png"
style="display:block;outline:none;border:none;text-decoration:none;max-width:100%"
width="620" />
</td>
</tr>
</tbody>
</table>
<p style="font-size:12px;line-height:24px;margin:16px 0;text-align:center;color:rgb(0,0,0, 0.7)">
2024 © Litlyx. All rights reserved.
<br>
Litlyx S.R.L. - Viale Tirreno, 187 - 00141 Rome - P.IVA: 17814721001- REA: RM-1743194
</p>
</td>
</tr>
</tbody>
</table>
</body>
</html>
`