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>
<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">
<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>
</Transition>
<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"
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> <i :class="alert.icon"></i> </div>
<div class="grow">
@@ -56,8 +56,8 @@ const { drawerVisible, hideDrawer, drawerClasses } = useDrawer();
</div>
<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">
<div :style="dialogStyle" class="bg-lyx-widget rounded-xl relative outline outline-1 outline-lyx-widget-lighter">
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-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">
<i @click="closeDialog()" class="fas fa-close text-[1.6rem] hover:text-gray-500 cursor-pointer"></i>
</div>

View File

@@ -54,7 +54,7 @@ function openExternalLink(link: string) {
<div class="flex justify-between mb-3">
<div class="flex flex-col gap-1">
<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 }}
</div>
<div class="flex items-center">
@@ -63,7 +63,7 @@ function openExternalLink(link: string) {
</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 }}
</div>
</div>
@@ -81,7 +81,8 @@ function openExternalLink(link: string) {
</div>
<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 v-if="isDetailView" class="flex items-center justify-center">
<i @click="$emit('showGeneral')"
@@ -107,7 +108,7 @@ function openExternalLink(link: string) {
<div class="flex gap-1 items-center" @click="showDetails(element._id)"
: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>
<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>
</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 }}
</span>
</div>
</div>
</div>
<div class="text-text font-semibold text-[.9rem] md:text-[1rem] manrope"> {{
formatNumberK(element.count) }} </div>
<div
class="text-lyx-lightmode-text dark:text-lyx-text font-semibold text-[.9rem] md:text-[1rem] manrope">
{{
formatNumberK(element.count) }} </div>
</div>
<div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-light text-[1.1rem]">
No data yet
</div>
</div>
<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
</div>
</LyxUiButton>
</div>
</div>

View File

@@ -23,6 +23,19 @@ type Props = {
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 props = defineProps<Props>();
@@ -108,7 +121,7 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
</script>
<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="{
'absolute top-0 w-full md:w-[20rem] z-[45] open': 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="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]">
Snapshots
</div>
@@ -170,7 +183,7 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
</UTooltip> -->
<UTooltip text="Create new snapshot">
<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>
</UTooltip>
</div>
@@ -179,11 +192,11 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
<div class="flex items-center gap-2">
<USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
base: '!bg-lyx-widget',
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-lightmode-widget dark:!bg-lyx-widget',
option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter'
base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
}
}" class="w-full" v-model="snapshot" :options="snapshotsItems">
<template #label>
@@ -204,7 +217,8 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
</div>
<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">
{{ new Date(snapshot.from).toLocaleString().split(',')[0].trim() }}
</div>
@@ -237,14 +251,14 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
</div>
<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
</LyxUiButton>
</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">
@@ -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-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="{
'!text-lyx-text-darker pointer-events-none': entry.disabled,
'bg-lyx-background-lighter !text-lyx-text/90': route.path == (entry.to || '#'),
'hover:bg-lyx-background-light hover:!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-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' : ''"
@@ -281,36 +295,25 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
<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="grow flex gap-3">
<!-- <NuxtLink to="https://github.com/litlyx/litlyx" target="_blank"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fab fa-github"></i>
</NuxtLink> -->
<!-- <NuxtLink to="https://discord.gg/9cQykjsmWX" target="_blank"
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> -->
<div>
<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"
:class="isDark ? 'far fa-moon' : 'far fa-sun'"></i>
</div>
<NuxtLink to="/admin" v-if="userRoles.isAdmin.value"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fas fa-cat"></i>
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-cat"></i>
</NuxtLink>
</div>
<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>
</div>
</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 items-center">
<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 }}
</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 }}
</div>
</div>

View File

@@ -11,9 +11,9 @@ const activeTabIndex = ref<number>(0);
<div>
<div class="flex">
<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,
'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 }}
</div>

View File

@@ -67,7 +67,7 @@ function reloadPage() {
<div class="flex items-center justify-center">
<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
</div>
<LyxUiButton class="ml-6" type="secondary" @click="reloadPage()">
@@ -122,8 +122,8 @@ function reloadPage() {
<CardTitled class="h-full w-full" title="Project id"
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="p-2 bg-[#1c1b1b] rounded-md w-full">
<div class="w-full text-[.9rem] text-[#acacac]"> {{ project?._id }} </div>
<div class="p-2 bg-lyx-lightmode-widget dark:bg-[#1c1b1b] rounded-md w-full">
<div class="w-full text-[.9rem] dark:text-[#acacac]"> {{ project?._id }} </div>
</div>
<LyxUiButton type="secondary" @click="copyProjectId()"> Copy </LyxUiButton>
</div>

View File

@@ -8,12 +8,18 @@ const props = defineProps<{ type: ButtonType, link?: string, target?: string, di
<template>
<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="{
'bg-lyx-primary-dark outline-lyx-primary hover:bg-lyx-primary-hover': type === 'primary',
'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-lyx-danger-dark outline-lyx-danger hover:bg-lyx-danger': type === 'danger',
'!bg-lyx-widget !outline-lyx-widget-lighter !cursor-not-allowed': disabled === true,
class="poppins w-fit cursor-pointer px-4 py-1 rounded-md outline outline-[1px] text-lyx-lightmode-text dark:text-lyx-text"
:class="{
'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-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>
</NuxtLink>

View File

@@ -4,7 +4,7 @@
</script>
<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>
</div>
</template>

View File

@@ -19,6 +19,6 @@ const handleChange = (event: Event) => {
<template>
<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">
</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
going to be your first/main analytics?
</div>
@@ -122,7 +122,7 @@ const showOnboarding = computed(() => {
<div v-for="(e, i) of analyticsList">
<div @click="selectIndex(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 }}
</div>
</div>
@@ -140,11 +140,11 @@ const showOnboarding = computed(() => {
</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 ?
</div>
@@ -152,7 +152,7 @@ const showOnboarding = computed(() => {
<div v-for="(e, i) of jobsList">
<div @click="selectIndex2(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 }}
</div>
</div>

View File

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

View File

@@ -16,12 +16,12 @@ const emits = defineEmits<{
<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"
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="{
'bg-lyx-widget-lighter hover:!bg-lyx-widget-lighter': currentIndex == index && !opt.disabled,
'hover:!bg-lyx-widget !cursor-not-allowed text-lyx-widget-lighter': 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-lightmode-widget-light text-lyx-lightmode-widget dark:hover:!bg-lyx-widget !cursor-not-allowed dark:!text-lyx-widget-lighter': opt.disabled
}">
{{ opt.label }}
</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">
<LyxUiButton type="secondary" :to="isLiveDemo ? '#' : '/analyst'" :disabled="isLiveDemo">
<div class="flex items-center gap-2 px-10">
<i class="far fa-sparkles text-yellow-400"></i>
<div class="poppins text-lyx-text"> Ask AI </div>
<i class="far fa-sparkles text-yellow-600 dark:text-yellow-400"></i>
<div class="poppins text-lyx-lightmode-text dark:text-lyx-text"> Ask AI </div>
</div>
</LyxUiButton>
<div class="flex gap-6">
@@ -352,7 +352,7 @@ const legendClasses = ref<string[]>([
<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> Date: </div>
<div v-if="currentTooltipData"> {{ currentTooltipData.date }}</div>

View File

@@ -29,12 +29,12 @@ const { showDrawer } = useDrawer();
</div>
<div class="flex flex-col grow">
<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 }}
</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 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 class="flex flex-col items-center gap-1">

View File

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

View File

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

View File

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

View File

@@ -34,10 +34,10 @@ function showAnomalyInfoAlert() {
<template>
<div
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="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">
<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="poppins font-medium text-[.9rem]"> {{ onlineUsers.data }} Online users</div>
</div>
@@ -63,7 +63,8 @@ function showAnomalyInfoAlert() {
</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="poppins font-regular text-[.9rem]"> AI Anomaly Detector </div>
<div class="flex items-center">

View File

@@ -36,7 +36,7 @@ function onColorChange() {
const snapshotName = ref<string>("");
const { updateSnapshots } = useSnapshot();
const { updateSnapshots, snapshot, snapshots } = useSnapshot();
const { createAlert } = useAlert()
async function confirmSnapshot() {
@@ -54,6 +54,9 @@ async function confirmSnapshot() {
await updateSnapshots();
closeDialog();
createAlert('Snapshot created', 'Snapshot created successfully', 'far fa-circle-check', 5000);
const newSnapshot = snapshots.value.at(-1);
if (newSnapshot) snapshot.value = newSnapshot;
}
</script>
@@ -61,7 +64,7 @@ async function confirmSnapshot() {
<template>
<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
</div>

View File

@@ -35,7 +35,7 @@ async function sendFeedback() {
overlay: {
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]'
}">
<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> Share everything with us. </div>
<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>Need help ? Check the docs <a href="https://docs.litlyx.com" target="_blank"
class="text-blue-500">here</a> </div>

View File

@@ -10,7 +10,7 @@ const { drawerComponent } = useDrawer();
<div class="p-8 overflow-y-auto">
<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>
</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 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">
Do you need help ?
</div>
<div class="poppins text-[1.2rem] text-text/90">
<div class="poppins text-[1.2rem]">
We respond in max. 1-2 days
</div>
</div>
<div class="mt-2">
<div class="rounded-lg px-10 py-3 bg-[#151515]">
<a href="mailto:help@litlyx.com" class="poppins text-[1.3rem]">
<div class="flex flex-col gap-2">
<LyxUiButton type="secondary">
<a href="mailto:help@litlyx.com" class="poppins text-[1.1rem]">
help@litlyx.com
</a>
</div>
</LyxUiButton>
<LyxUiButton type="secondary">
<a href="https://discord.com/invite/9cQykjsmWX" class="poppins text-[1.1rem]">
Discord support
</a>
</LyxUiButton>
</div>
</div>

View File

@@ -81,11 +81,11 @@ const canSearch = computed(() => {
<div class="flex-[2]">
<div class="flex flex-col gap-2">
<USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
base: '!bg-lyx-widget',
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-lightmode-widget dark:!bg-lyx-widget',
option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter'
base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
}
}" searchable searchable-placeholder="Search an event..." class="w-full"
placeholder="Select an event" :options="eventNames.data.value || []"
@@ -93,11 +93,11 @@ const canSearch = computed(() => {
</USelectMenu>
<USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
base: '!bg-lyx-widget',
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-lightmode-widget dark:!bg-lyx-widget',
option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter'
base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
}
}" searchable searchable-placeholder="Search a field..." class="w-full"
placeholder="Select a field" :options="metadataFields" v-model="selectedMetadataField">
@@ -110,7 +110,7 @@ const canSearch = computed(() => {
</div>
<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>
<input class="bg-transparent px-4 py-2 text-[1rem] outline-none" type="text"
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="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">
<div class="flex gap-2 items-center">
<div> {{ item._id || 'OLD_EVENTS' }} </div>

View File

@@ -46,12 +46,12 @@ async function analyzeEvent() {
<div class="py-2 flex items-center gap-3">
<USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
base: '!bg-lyx-widget',
option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-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-lightmode-widget dark:!bg-lyx-widget',
option: {
base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
}
}" searchable searchable-placeholder="Search an event..." class="w-full" placeholder="Select an event"
:options="eventNames.data.value || []" v-model="selectedEventName">
</USelectMenu>
@@ -71,7 +71,7 @@ async function analyzeEvent() {
</div>
<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">
<div class="w-5 h-5 flex items-center justify-center">
<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;
});
const colorMode = useColorMode();
</script>
<template>
<div class="w-fit h-fit">
<svg xmlns="http://www.w3.org/2000/svg" :width="widthHeight" :height="widthHeight" :style="`opacity: ${props.opacity};`"
fill="none">
<template v-for="(p, x) 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)" />
</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>
</div>

View File

@@ -41,7 +41,7 @@ async function onUpgradeClick() {
<template>
<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 v-if="data.active"
@@ -56,7 +56,7 @@ async function onUpgradeClick() {
<div class="poppins text-4xl font-medium"> {{ data.price }} </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 v-if="datas.length > 1">
@@ -76,7 +76,7 @@ async function onUpgradeClick() {
</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 gap-2" v-for="feature of data.features">

View File

@@ -2,12 +2,18 @@
import type { SettingsTemplateEntry } from './Template.vue';
const entries: SettingsTemplateEntry[] = [
{ id: 'change_pass', title: 'Change password', text: 'Change your password' },
{ id: 'delete', title: 'Delete account', text: 'Delete your account' },
]
const { user } = useLoggedUser();
const { setToken } = useAccessToken();
const canChangePassword = useFetch('/api/user/password/can_change', {
headers: useComputedHeaders({ useSnapshotDates: false })
});
async function deleteAccount() {
const sure = confirm("Are you sure you want to delete this account ?");
if (!sure) return;
@@ -20,17 +26,63 @@ async function deleteAccount() {
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>
<template>
<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>
<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 @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
</div>
</div>

View File

@@ -53,6 +53,9 @@ async function redeemCode() {
<div class="text-lyx-text-darker mt-1 text-[.9rem] poppins">
Redeemed codes: {{ valid_codes.data.value?.count || '0' }}
</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>
</SettingsTemplate>
</template>

View File

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

View File

@@ -20,10 +20,10 @@ const props = defineProps<SettingsTemplateProp>();
<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="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 }}
</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 }}
</div>
</div>
@@ -31,7 +31,7 @@ const props = defineProps<SettingsTemplateProp>();
<slot :name="entry.id"></slot>
</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>
</template>

View File

@@ -68,7 +68,7 @@ function getPremiumPrice(type: number) {
}
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: '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' },
@@ -127,25 +127,25 @@ const { showDrawer } = useDrawer();
<template #info>
<div v-if="!isGuest">
<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">
</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">
</LyxUiInput>
<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">
</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">
</LyxUiInput>
</div>
<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">
</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">
</LyxUiInput>
</div>
@@ -175,7 +175,7 @@ const { showDrawer } = useDrawer();
<div class="flex items-center gap-1">
<div class="poppins font-semibold text-[2rem]">
{{ 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 class="flex flex-col">
@@ -194,7 +194,7 @@ const { showDrawer } = useDrawer();
<div class="my-4 w-full bg-gray-400/30 h-[1px]">
</div>
<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> {{ prettyExpireDate }}</div>
</div>
@@ -212,7 +212,7 @@ const { showDrawer } = useDrawer();
<div class="poppins font-semibold text-[1.1rem]">
Usage
</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.
</div>
</div>
@@ -240,7 +240,7 @@ const { showDrawer } = useDrawer();
<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">
<div> <i class="fal fa-file-invoice"></i> </div>

View File

@@ -3,5 +3,5 @@
const app = useRuntimeConfig();
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();
console.log({ selfhosted })
const sections: Section[] = [
{
title: '',
@@ -32,6 +34,7 @@ const sections: Section[] = [
action() {
modal.open(DialogFeedback, {});
},
disabled: selfhosted
},
{
label: 'Documentation', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true,
@@ -92,11 +95,11 @@ const { isOpen, close, open } = useMenu();
</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]">
<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 @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 != '/register' &&
to.path != '/live_demo' &&
to.path != '/jwt_login'
to.path != '/jwt_login' &&
to.path != '/forgot_password'
) {
console.log('REDIRECT TO LOGIN', to.path);
return '/login';

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import VueMarkdown from 'vue-markdown-render';
definePageMeta({ layout: 'dashboard' });
const selfhosted = useSelfhosted();
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;
currentChatMessageDelta.value = '';
loading.value = false;
}
}
@@ -268,7 +270,9 @@ async function clearAllChats() {
</div>
<div class="flex flex-col xl:grid xl:grid-cols-2 gap-4 mt-6">
<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 }}
</div>
</div>
@@ -280,7 +284,7 @@ async function clearAllChats() {
<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 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 }}
</div>
</div>
@@ -290,7 +294,7 @@ async function clearAllChats() {
<div class="flex items-center justify-center shrink-0">
<img class="h-[3.5rem] w-auto" :src="'analyst.png'">
</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="{
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">
<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()"
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>
</div>
<div @click="menuOpen = !menuOpen"
@@ -381,7 +386,8 @@ async function clearAllChats() {
<div :class="{
'absolute top-0 left-0 w-full': 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="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="bg-accent w-5 h-5 rounded-full animate-pulse">
</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>
<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
</LyxUiButton>
</div>
@@ -414,7 +421,7 @@ async function clearAllChats() {
<div class="px-2">
<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> New chat </div>
</div>
@@ -424,7 +431,7 @@ async function clearAllChats() {
<div class="overflow-y-auto">
<div class="flex flex-col gap-2 px-2">
<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">
<i @click="deleteChat(chat._id.toString())"
class="far fa-trash hover:text-gray-300 cursor-pointer"></i>
@@ -458,7 +465,6 @@ async function clearAllChats() {
font-weight: bold;
margin-top: 1.5em;
margin-bottom: 0.5em;
color: white;
}
p:last-of-type {

View File

@@ -73,7 +73,7 @@ const showWarning = computed(() => {
const { showDrawer } = useDrawer();
function goToUpgrade() {
showDrawer('PRICING');
showDrawer('PRICING');
}
</script>
@@ -86,7 +86,7 @@ function goToUpgrade() {
<div class="w-full h-dvh flex flex-col">
<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]">
Creating csv...
</div>
@@ -101,11 +101,11 @@ function goToUpgrade() {
</div>
<div class="w-[15rem] flex flex-col gap-0">
<USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
base: '!bg-lyx-widget',
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-lightmode-widget dark:!bg-lyx-widget',
option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter'
base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
}
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
</div>
@@ -125,14 +125,14 @@ function goToUpgrade() {
<UTable v-if="tableData" class="utable px-8" :ui="{
wrapper: 'overflow-auto w-full h-full',
thead: 'sticky top-0 bg-menu',
wrapper: 'overflow-auto w-full h-full',
thead: 'sticky top-0 bg-lyx-lightmode-background-light dark:bg-menu',
td: {
color: 'text-[#ffffffb3]',
base: 'border-r border-l border-gray-300/20'
color: 'text-lyx-lightmode-text dark:text-[#ffffffb3]',
base: 'border-r border-l border-lyx-lightmode-widget dark:border-gray-300/20'
},
th: { color: 'text-text-sub' },
tbody: 'divide-y divide-gray-300/20',
th: { color: 'text-lyx-lightmode-text dark:text-text-sub' },
tbody: 'divide-y divide-lyx-lightmode-widget dark:divide-gray-300/20',
divide: '',
}" 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 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]">
Creating csv...
</div>
@@ -106,11 +106,11 @@ function goToUpgrade() {
</div>
<div class="w-[15rem] flex flex-col gap-0">
<USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
base: '!bg-lyx-widget',
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-lightmode-widget dark:!bg-lyx-widget',
option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter'
base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
}
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
</div>
@@ -131,13 +131,13 @@ function goToUpgrade() {
<UTable v-if="tableData" class="utable px-8" :ui="{
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: {
color: 'text-[#ffffffb3]',
base: 'border-r border-l border-gray-300/20'
color: 'text-lyx-lightmode-text dark:text-[#ffffffb3]',
base: 'border-r border-l border-lyx-lightmode-widget dark:border-gray-300/20'
},
th: { color: 'text-text-sub' },
tbody: 'divide-y divide-gray-300/20',
th: { color: 'text-lyx-lightmode-text dark:text-text-sub' },
tbody: 'divide-y divide-lyx-lightmode-widget dark:divide-gray-300/20',
divide: '',
}" 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>
@@ -161,21 +162,21 @@ async function signInWithCredentials() {
<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="rotating-thing absolute top-0"></div>
<!-- <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-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
</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
with extreme simplicity in under 30 sec.
<br>
@@ -195,6 +196,13 @@ async function signInWithCredentials() {
</LyxUiInput>
</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]">
<LyxUiButton @click="signInWithCredentials()" class="text-center" type="primary">
Sign in
@@ -202,17 +210,17 @@ async function signInWithCredentials() {
</div>
<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
</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"
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">
<i class="fab fa-google"></i>
</div>
@@ -220,7 +228,7 @@ async function signInWithCredentials() {
</div>
<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">
<i class="far fa-envelope"></i>
</div>
@@ -228,10 +236,14 @@ async function signInWithCredentials() {
</div>
<RouterLink tag="div" to="/register"
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
You don't have an account ? Sign up
</RouterLink>
<div class="flex flex-col gap-2 mt-4">
<RouterLink tag="div" to="/register"
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>
@@ -245,7 +257,7 @@ async function signInWithCredentials() {
</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
<br>
our

View File

@@ -56,6 +56,7 @@ async function createProject() {
}
</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="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
</div>
@@ -78,15 +79,20 @@ async function createProject() {
</div>
<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' }}
</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>
<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>

View File

@@ -47,21 +47,21 @@ async function registerAccount() {
<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="rotating-thing absolute top-0"></div>
<!-- <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-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
</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
with extreme simplicity in under 30 sec.
<br>
@@ -95,7 +95,7 @@ async function registerAccount() {
</div>
<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
</RouterLink>
@@ -112,13 +112,13 @@ async function registerAccount() {
Please check your inbox to confirm your account and complete your registration.
</div>
<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
</RouterLink>
</div>
<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
<br>
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="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="poppins font-regular text-[1rem]"> AI Anomaly Detector </div>
<div class="flex items-center">
@@ -47,7 +47,7 @@ const columns = [
<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() }}
</div>
</template>
@@ -59,7 +59,7 @@ const columns = [
</template>
<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'">
{{ row.data.domain }}
</div>

View File

@@ -2,6 +2,7 @@
definePageMeta({ layout: 'dashboard' });
const selfhosted = useSelfhosted();
const items = [
{ label: 'General', slot: 'general' },
@@ -30,10 +31,18 @@ const items = [
<SettingsMembers :key="refreshKey"></SettingsMembers>
</template>
<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 #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 #account>
<SettingsAccount :key="refreshKey"></SettingsAccount>

View File

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

View File

@@ -66,7 +66,7 @@ export default defineEventHandler(async event => {
const { project, project_id, user } = data;
if (SELFHOSTED !== 'TRUE') {
if (SELFHOSTED.toString() !== 'TRUE' && SELFHOSTED.toString() !== 'true') {
const PREMIUM_TYPE = project.premium_type;
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.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();
};

View File

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

View File

@@ -95,6 +95,7 @@ services:
NUXT_NOAUTH_USER_NAME: "defaultuser"
NUXT_SELFHOSTED: 'true'
NUXT_PUBLIC_SELFHOSTED: 'true'
# Optional - Used for tests
# NUXT_STRIPE_SECRET_TEST: ""

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_DOMAIN_EMAIL } from './email_templates/AnomalyDomainEmail';
import { CONFIRM_EMAIL } from './email_templates/ConfirmEmail';
import { RESET_PASSWORD_EMAIL } from './email_templates/ResetPasswordEmail';
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();

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>
`