mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f18cdc8278 | ||
|
|
a7ebbc22c0 | ||
|
|
346eecc928 | ||
|
|
abc485a9ef | ||
|
|
0292829805 | ||
|
|
4e2c8468f8 | ||
|
|
38cfd4315d | ||
|
|
b592695a49 | ||
|
|
0963201a32 | ||
|
|
4da840f2ec | ||
|
|
a1718875d9 | ||
|
|
e931235533 | ||
|
|
881a7800ce | ||
|
|
487c3ac7b4 | ||
|
|
0dd94be6e6 | ||
|
|
29a220b21e | ||
|
|
8cc2f07b95 | ||
|
|
88cec21df1 | ||
|
|
8183ae1e68 | ||
|
|
0f39cab26a | ||
|
|
a2e4ed9ee0 | ||
|
|
30b5db4200 | ||
|
|
bfeee8673c | ||
|
|
39b8dd84f1 | ||
|
|
19b7c7664a | ||
|
|
a3e74adf9c | ||
|
|
ad9aabcbf6 | ||
|
|
510bc2545a | ||
|
|
65c682c75d | ||
|
|
04acc0b18e | ||
|
|
852fea45a5 | ||
|
|
6f3e59e72e | ||
|
|
3960eaa8ad | ||
|
|
e4bdf7e4c3 | ||
|
|
afeaac1b0d | ||
|
|
8922507a64 | ||
|
|
13e94cb0f0 | ||
|
|
3923a06e9b | ||
|
|
6b5d23566c | ||
|
|
dbcda95823 | ||
|
|
fb89c87489 | ||
|
|
b59eea47e9 | ||
|
|
473331047d | ||
|
|
5af77ff63e | ||
|
|
e6e2340432 | ||
|
|
0b90c2fe3c | ||
|
|
a6d1797a4f | ||
|
|
d1abe1a91f | ||
|
|
b733cd2a68 | ||
|
|
88ebfc188c |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,4 +4,7 @@ PROCESS_EVENT
|
||||
docker
|
||||
dev
|
||||
docker-compose.admin.yml
|
||||
full_reload.sh
|
||||
full_reload.sh
|
||||
build-all.sh
|
||||
tmp
|
||||
ecosystem.config.js
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/node_modules": true
|
||||
}
|
||||
}
|
||||
30
Dockerfile
30
Dockerfile
@@ -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" ]
|
||||
12
consumer/.gitignore
vendored
12
consumer/.gitignore
vendored
@@ -1,8 +1,10 @@
|
||||
|
||||
node_modules
|
||||
static
|
||||
|
||||
ecosystem.config.cjs
|
||||
dist
|
||||
ecosystem.config.js
|
||||
|
||||
scripts/start_dev.js
|
||||
package-lock.json
|
||||
build_all.bat
|
||||
tests
|
||||
scripts/start_dev_prod.js
|
||||
dist
|
||||
src/shared
|
||||
@@ -1,21 +0,0 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'consumer',
|
||||
port: '3031',
|
||||
exec_mode: 'cluster',
|
||||
instances: '2',
|
||||
script: './dist/consumer/src/index.js',
|
||||
env: {
|
||||
EMAIL_SERVICE: '',
|
||||
BREVO_API_KEY: '',
|
||||
MONGO_CONNECTION_STRING: '',
|
||||
REDIS_URL: "",
|
||||
REDIS_USERNAME: "",
|
||||
REDIS_PASSWORD: "",
|
||||
STREAM_NAME: "",
|
||||
GROUP_NAME: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"express": "^4.19.2",
|
||||
"mongoose": "^8.9.5",
|
||||
"redis": "^4.7.0",
|
||||
"ua-parser-js": "^1.0.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^20.12.13",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"ts-node": "^10.9.2",
|
||||
@@ -14,12 +18,14 @@
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "node scripts/start_dev.js",
|
||||
"dev_prod": "node scripts/start_dev_prod.js",
|
||||
"compile": "tsc",
|
||||
"build_project": "node ../scripts/build.js",
|
||||
"build": "npm run compile && npm run build_project && npm run create_db",
|
||||
"build": "npm run compile && npm run create_db",
|
||||
"create_db": "cd scripts && ts-node create_database.ts",
|
||||
"docker-build": "docker build -t litlyx-consumer -f Dockerfile ../",
|
||||
"docker-inspect": "docker run -it litlyx-consumer sh"
|
||||
"docker-inspect": "docker run -it litlyx-consumer sh",
|
||||
"workspace:shared": "ts-node ../scripts/consumer/shared.ts",
|
||||
"workspace:deploy": "ts-node ../scripts/consumer/deploy.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Emily",
|
||||
|
||||
1493
consumer/pnpm-lock.yaml
generated
1493
consumer/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,10 @@
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { LimitNotifyModel } from "@schema/broker/LimitNotifySchema";
|
||||
import EmailService from '@services/EmailService';
|
||||
import { requireEnv } from "@utils/requireEnv";
|
||||
import { TProjectLimit } from "@schema/project/ProjectsLimits";
|
||||
import { ProjectModel } from "./shared/schema/project/ProjectSchema";
|
||||
import { UserModel } from "./shared/schema/UserSchema";
|
||||
import { LimitNotifyModel } from "./shared/schema/broker/LimitNotifySchema";
|
||||
import { EmailService } from './shared/services/EmailService';
|
||||
import { TProjectLimit } from "./shared/schema/project/ProjectsLimits";
|
||||
import { EmailServiceHelper } from "./EmailServiceHelper";
|
||||
|
||||
if (process.env.EMAIL_SERVICE) {
|
||||
EmailService.init(requireEnv('BREVO_API_KEY'));
|
||||
}
|
||||
|
||||
export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
||||
|
||||
@@ -27,7 +24,14 @@ export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
||||
const owner = await UserModel.findById(project.owner);
|
||||
if (!owner) return;
|
||||
|
||||
if (process.env.EMAIL_SERVICE) await EmailService.sendLimitEmailMax(owner.email, project.name);
|
||||
setImmediate(() => {
|
||||
const emailData = EmailService.getEmailServerInfo('limit_max', {
|
||||
target: owner.email,
|
||||
projectName: project.name
|
||||
});
|
||||
EmailServiceHelper.sendEmail(emailData);
|
||||
});
|
||||
|
||||
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: true });
|
||||
|
||||
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.9)) {
|
||||
@@ -40,7 +44,14 @@ export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
||||
const owner = await UserModel.findById(project.owner);
|
||||
if (!owner) return;
|
||||
|
||||
if (process.env.EMAIL_SERVICE) await EmailService.sendLimitEmail90(owner.email, project.name);
|
||||
setImmediate(() => {
|
||||
const emailData = EmailService.getEmailServerInfo('limit_90', {
|
||||
target: owner.email,
|
||||
projectName: project.name
|
||||
});
|
||||
EmailServiceHelper.sendEmail(emailData);
|
||||
});
|
||||
|
||||
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: false });
|
||||
|
||||
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.5)) {
|
||||
@@ -53,7 +64,14 @@ export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
||||
const owner = await UserModel.findById(project.owner);
|
||||
if (!owner) return;
|
||||
|
||||
if (process.env.EMAIL_SERVICE) await EmailService.sendLimitEmail50(owner.email, project.name);
|
||||
setImmediate(() => {
|
||||
const emailData = EmailService.getEmailServerInfo('limit_50', {
|
||||
target: owner.email,
|
||||
projectName: project.name
|
||||
});
|
||||
EmailServiceHelper.sendEmail(emailData);
|
||||
});
|
||||
|
||||
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: false, limit3: false });
|
||||
|
||||
}
|
||||
|
||||
19
consumer/src/EmailServiceHelper.ts
Normal file
19
consumer/src/EmailServiceHelper.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
import { EmailServerInfo } from './shared/services/EmailService'
|
||||
import axios from 'axios';
|
||||
|
||||
const EMAIL_SECRET = process.env.EMAIL_SECRET;
|
||||
|
||||
export class EmailServiceHelper {
|
||||
static async sendEmail(data: EmailServerInfo) {
|
||||
try {
|
||||
await axios(data.url, {
|
||||
method: 'POST',
|
||||
data: data.body,
|
||||
headers: { ...data.headers, 'x-litlyx-token': EMAIL_SECRET }
|
||||
})
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
import { ProjectLimitModel } from '@schema/project/ProjectsLimits';
|
||||
import { MAX_LOG_LIMIT_PERCENT } from '@data/broker/Limits';
|
||||
import { ProjectLimitModel } from './shared/schema/project/ProjectsLimits';
|
||||
import { MAX_LOG_LIMIT_PERCENT } from './shared/data/broker/Limits';
|
||||
import { checkLimitsForEmail } from './EmailController';
|
||||
|
||||
export async function checkLimits(project_id: string) {
|
||||
|
||||
28
consumer/src/Metrics.ts
Normal file
28
consumer/src/Metrics.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
import { Router } from 'express';
|
||||
import { RedisStreamService } from './shared/services/RedisStreamService';
|
||||
import { requireEnv } from './shared/utils/requireEnv';
|
||||
|
||||
const stream_name = requireEnv('STREAM_NAME');
|
||||
|
||||
export const metricsRouter = Router();
|
||||
|
||||
metricsRouter.get('/queue', async (req, res) => {
|
||||
try {
|
||||
const size = await RedisStreamService.getQueueInfo(stream_name);
|
||||
res.json({ size });
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
res.status(500).json({ error: ex.message });
|
||||
}
|
||||
})
|
||||
|
||||
metricsRouter.get('/durations', async (req, res) => {
|
||||
try {
|
||||
const durations = RedisStreamService.METRICS_get()
|
||||
res.json({ durations });
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
res.status(500).json({ error: ex.message });
|
||||
}
|
||||
})
|
||||
@@ -1,39 +1,31 @@
|
||||
|
||||
import { requireEnv } from '@utils/requireEnv';
|
||||
import { connectDatabase } from '@services/DatabaseService';
|
||||
import { RedisStreamService } from '@services/RedisStreamService';
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { requireEnv } from './shared/utils/requireEnv';
|
||||
import { connectDatabase } from './shared/services/DatabaseService';
|
||||
import { RedisStreamService } from './shared/services/RedisStreamService';
|
||||
import { ProjectModel } from "./shared/schema/project/ProjectSchema";
|
||||
import { VisitModel } from "./shared/schema/metrics/VisitSchema";
|
||||
import { SessionModel } from "./shared/schema/metrics/SessionSchema";
|
||||
import { EventModel } from "./shared/schema/metrics/EventSchema";
|
||||
import { lookup } from './lookup';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
import { checkLimits } from './LimitChecker';
|
||||
import express from 'express';
|
||||
|
||||
import { ProjectLimitModel } from '@schema/project/ProjectsLimits';
|
||||
import { ProjectCountModel } from '@schema/project/ProjectsCounts';
|
||||
import { ProjectLimitModel } from './shared/schema/project/ProjectsLimits';
|
||||
import { ProjectCountModel } from './shared/schema/project/ProjectsCounts';
|
||||
import { metricsRouter } from './Metrics';
|
||||
|
||||
|
||||
const app = express();
|
||||
|
||||
let durations: number[] = [];
|
||||
app.use('/metrics', metricsRouter);
|
||||
|
||||
app.get('/status', async (req, res) => {
|
||||
try {
|
||||
return res.json({ status: 'ALIVE', durations })
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
return res.setStatus(500).json({ error: ex.message });
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(process.env.PORT);
|
||||
app.listen(process.env.PORT, () => console.log(`Listening on port ${process.env.PORT}`));
|
||||
|
||||
connectDatabase(requireEnv('MONGO_CONNECTION_STRING'));
|
||||
main();
|
||||
|
||||
|
||||
const CONSUMER_NAME = `CONSUMER_${process.env.NODE_APP_INSTANCE || 'DEFAULT'}`
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -43,7 +35,7 @@ async function main() {
|
||||
const group_name = requireEnv('GROUP_NAME') as any; // Checks are inside "startReadingLoop"
|
||||
|
||||
await RedisStreamService.startReadingLoop({
|
||||
stream_name, group_name, consumer_name: `CONSUMER_${process.env.NODE_APP_INSTANCE || 'DEFAULT'}`
|
||||
stream_name, group_name, consumer_name: CONSUMER_NAME
|
||||
}, processStreamEntry);
|
||||
|
||||
}
|
||||
@@ -55,7 +47,7 @@ async function processStreamEntry(data: Record<string, string>) {
|
||||
try {
|
||||
|
||||
const eventType = data._type;
|
||||
if (!eventType) return;
|
||||
if (!eventType) return console.log('No type');
|
||||
|
||||
const { pid, sessionHash } = data;
|
||||
|
||||
@@ -73,18 +65,13 @@ async function processStreamEntry(data: Record<string, string>) {
|
||||
await process_visit(data, sessionHash);
|
||||
}
|
||||
|
||||
// console.log('Entry processed in', duration, 'ms');
|
||||
|
||||
} catch (ex: any) {
|
||||
console.error('ERROR PROCESSING STREAM EVENT', ex.message);
|
||||
}
|
||||
|
||||
const duration = Date.now() - start;
|
||||
|
||||
durations.push(duration);
|
||||
if (durations.length > 1000) {
|
||||
durations = durations.splice(500);
|
||||
}
|
||||
RedisStreamService.METRICS_onProcess(CONSUMER_NAME, duration);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"target": "ESNext",
|
||||
"esModuleInterop": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [
|
||||
|
||||
8
dashboard/.gitignore
vendored
8
dashboard/.gitignore
vendored
@@ -24,7 +24,6 @@ winston-*.ndjson
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
|
||||
# Test reports
|
||||
*.report.txt
|
||||
|
||||
@@ -35,5 +34,10 @@ out.pdf
|
||||
tests
|
||||
|
||||
# EXPLAINS MONGODB
|
||||
explains
|
||||
|
||||
explains
|
||||
#Ecosystem
|
||||
ecosystem.config.cjs
|
||||
ecosystem.config.js
|
||||
|
||||
shared
|
||||
|
||||
@@ -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>
|
||||
|
||||
13
dashboard/assets/main.css
Normal file
13
dashboard/assets/main.css
Normal file
@@ -0,0 +1,13 @@
|
||||
@import './font-awesome/css/all.css';
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;0,1000;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900;1,1000&display=swap');
|
||||
@import url('https://fonts.cdnfonts.com/css/brockmann');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
||||
|
||||
|
||||
@import url('https://fonts.cdnfonts.com/css/geometric-sans-serif-v1');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0');
|
||||
@@ -1,20 +1,6 @@
|
||||
@use './utilities.scss';
|
||||
@use './colors.scss';
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;0,1000;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900;1,1000&display=swap');
|
||||
@import url('https://fonts.cdnfonts.com/css/brockmann');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
||||
|
||||
@import '../font-awesome/css/all.css';
|
||||
|
||||
@import url('https://fonts.cdnfonts.com/css/geometric-sans-serif-v1');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0');
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: "Geist";
|
||||
src: url("../fonts/GeistVF.ttf");
|
||||
|
||||
@@ -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-[19] 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>
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ async function showMore() {
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" :data="browsersData.data.value || []"
|
||||
desc="The browsers most used to search your website." :dataIcons="true" :iconProvider="iconProvider"
|
||||
:loading="browsersData.pending.value" label="Browsers" sub-label="Browsers">
|
||||
|
||||
@@ -5,17 +5,17 @@ import type { IconProvider } from './Base.vue';
|
||||
|
||||
function iconProvider(e: { _id: string, count: number }): ReturnType<IconProvider> {
|
||||
if (e._id === 'desktop') return ['icon','far fa-desktop'];
|
||||
if (e._id === 'tablet') return ['icon','far fa-tablet'];
|
||||
if (e._id === 'mobile') return ['icon','far fa-mobile'];
|
||||
if (e._id === 'tablet') return ['icon','far fa-tablet ml-1'];
|
||||
if (e._id === 'mobile') return ['icon','far fa-mobile ml-1'];
|
||||
if (e._id === 'smarttv') return ['icon','far fa-tv'];
|
||||
if (e._id === 'console') return ['icon','far fa-game-console-handheld'];
|
||||
return ['icon', 'far fa-question']
|
||||
return ['icon', 'far fa-question ml-1 mr-1']
|
||||
}
|
||||
|
||||
|
||||
function transform(data: { _id: string, count: number }[]) {
|
||||
console.log(data);
|
||||
return data.map(e => ({ ...e, _id: e._id == null ? 'unknown' : e._id }))
|
||||
return data.map(e => ({ ...e, _id: e._id == null ? 'others' : e._id }))
|
||||
}
|
||||
|
||||
const devicesData = useFetch('/api/data/devices', {
|
||||
|
||||
@@ -49,7 +49,7 @@ async function showMore() {
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()"
|
||||
:data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value"
|
||||
label="Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"
|
||||
|
||||
44
dashboard/components/BarCard/Pages.vue
Normal file
44
dashboard/components/BarCard/Pages.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const pagesData = useFetch('/api/data/pages', {
|
||||
headers: useComputedHeaders({
|
||||
limit: 10,
|
||||
}), lazy: true
|
||||
});
|
||||
|
||||
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||
|
||||
async function showMore() {
|
||||
|
||||
dialogBarData.value = [];
|
||||
|
||||
showDialog.value = true;
|
||||
isDataLoading.value = true;
|
||||
|
||||
const res = await $fetch('/api/data/pages', {
|
||||
headers: useComputedHeaders({ limit: 1000 }).value
|
||||
});
|
||||
|
||||
dialogBarData.value = (res || []);
|
||||
|
||||
isDataLoading.value = false;
|
||||
}
|
||||
|
||||
function goToView() {
|
||||
router.push('/dashboard/visits');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<BarCardBase @showRawData="goToView()" @showMore="showMore()" @dataReload="pagesData.refresh()" :showLink=true
|
||||
:data="pagesData.data.value || []" :interactive="false" desc="Most visited pages."
|
||||
:rawButton="!isLiveDemo"
|
||||
:dataIcons="true" :loading="pagesData.pending.value" label="Top Pages" sub-label="Referrers">
|
||||
</BarCardBase>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,7 +43,7 @@ async function showMore() {
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
|
||||
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
|
||||
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,13 +9,16 @@ const activeTabIndex = ref<number>(0);
|
||||
|
||||
<template>
|
||||
<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="{
|
||||
'!border-[#88A7FF] !text-[#88A7FF]': activeTabIndex === index,
|
||||
'hover:border-lyx-text-dark hover:text-lyx-text-dark cursor-pointer': activeTabIndex !== index
|
||||
}">
|
||||
{{ tab.label }}
|
||||
<div class="flex overflow-y-auto hide-scrollbars">
|
||||
<div class="flex">
|
||||
<div v-for="(tab, index) of items" @click="activeTabIndex = index"
|
||||
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-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>
|
||||
</div>
|
||||
<div class="border-b-[1px] border-lyx-text-darker w-full">
|
||||
|
||||
|
||||
@@ -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()">
|
||||
@@ -80,7 +80,7 @@ function reloadPage() {
|
||||
|
||||
|
||||
<div class="flex items-center justify-center mt-10">
|
||||
<div class="flex flex-col-reverse gap-6">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<div class="flex gap-6 xl:flex-row flex-col">
|
||||
|
||||
@@ -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>
|
||||
@@ -135,8 +135,8 @@ function reloadPage() {
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<CardTitled class="w-full h-full" title="Documentation"
|
||||
sub="Learn how to use Litlyx in every tech stack">
|
||||
<CardTitled class="w-full h-full" title="Modules"
|
||||
sub="Get started with your favorite framework.">
|
||||
<template #header>
|
||||
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
|
||||
to="https://docs.litlyx.com">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import DateService, { type Slice } from '@services/DateService';
|
||||
import type { ChartData, ChartOptions, TooltipModel } from 'chart.js';
|
||||
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||
@@ -113,7 +112,7 @@ const chartData = ref<ChartData<'line' | 'bar' | 'bubble'>>({
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Unique sessions',
|
||||
label: 'Unique visitors',
|
||||
data: [],
|
||||
backgroundColor: ['#4abde8'],
|
||||
borderColor: '#4abde8',
|
||||
@@ -254,8 +253,6 @@ watch(readyToDisplay, () => {
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
function onDataReady() {
|
||||
if (!visitsData.data.value) return;
|
||||
if (!eventsData.data.value) return;
|
||||
@@ -333,8 +330,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 +349,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>
|
||||
|
||||
@@ -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">
|
||||
@@ -52,7 +52,7 @@ const { showDrawer } = useDrawer();
|
||||
</div>
|
||||
<div v-if="!ready" class="flex justify-center items-center w-full h-full flex-col gap-2">
|
||||
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||
<div v-if="props.slow"> Can be very slow on large snapshots </div>
|
||||
<div v-if="props.slow"> Can be very slow on large timeframes </div>
|
||||
</div>
|
||||
</LyxUiCard>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -27,7 +27,6 @@ const chartOptions = ref<ChartOptions<'doughnut'>>({
|
||||
position: 'top',
|
||||
align: 'center',
|
||||
labels: {
|
||||
color: 'white',
|
||||
font: {
|
||||
family: 'Poppins',
|
||||
size: 16
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import DateService from '@services/DateService';
|
||||
import type { Slice } from '@services/DateService';
|
||||
import DateService, { type Slice } from '../../shared/services/DateService';
|
||||
|
||||
|
||||
const { snapshot, safeSnapshotDates, snapshotDuration } = useSnapshot()
|
||||
|
||||
@@ -75,7 +75,7 @@ const avgSessionDuration = computed(() => {
|
||||
.filter(e => e > 0)
|
||||
.reduce((a, e) => e + a, 0);
|
||||
|
||||
const avg = counts / Math.max(sessionsDurationData.data.value.data.filter(e => e > 0).length, 1);
|
||||
const avg = counts / (Math.max(sessionsDurationData.data.value.data.filter(e => e > 0).length, 1)) / 5;
|
||||
|
||||
let hours = 0;
|
||||
let minutes = 0;
|
||||
@@ -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>
|
||||
|
||||
@@ -18,27 +18,15 @@ function copyProjectId() {
|
||||
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
||||
}
|
||||
|
||||
|
||||
function showAnomalyInfoAlert() {
|
||||
createAlert('AI Anomaly Detector info',
|
||||
`Anomaly detector is running. It helps you detect a spike in visits or events, it could mean an
|
||||
attack or simply higher traffic due to good performance. Additionally, it can detect if someone is
|
||||
stealing parts of your website and hosting a duplicate version—an unfortunately common practice.
|
||||
Litlyx will notify you via email with actionable advices`,
|
||||
'far fa-shield',
|
||||
10000
|
||||
)
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<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="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
||||
<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-[.8rem] h-[.8rem] bg-green-400 rounded-full"> </div>
|
||||
<div class="poppins font-medium text-[.9rem]"> {{ onlineUsers.data }} Online users</div>
|
||||
</div>
|
||||
|
||||
@@ -62,15 +50,16 @@ function showAnomalyInfoAlert() {
|
||||
</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="poppins font-regular text-[.9rem]"> AI Anomaly Detector </div>
|
||||
<div class="flex items-center">
|
||||
<i class="far fa-info-circle text-[.9rem] hover:text-lyx-primary cursor-pointer"
|
||||
@click="showAnomalyInfoAlert"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,7 +36,7 @@ function onColorChange() {
|
||||
|
||||
const snapshotName = ref<string>("");
|
||||
|
||||
const { updateSnapshots } = useSnapshot();
|
||||
const { updateSnapshots, snapshot, snapshots } = useSnapshot();
|
||||
const { createAlert } = useAlert()
|
||||
|
||||
async function confirmSnapshot() {
|
||||
@@ -53,7 +53,10 @@ async function confirmSnapshot() {
|
||||
|
||||
await updateSnapshots();
|
||||
closeDialog();
|
||||
createAlert('Snapshot created', 'Snapshot created successfully', 'far fa-circle-check', 5000);
|
||||
createAlert('Timeframe created', 'Timeframe created successfully', 'far fa-circle-check', 5000);
|
||||
const newSnapshot = snapshots.value.at(-1);
|
||||
if (newSnapshot) snapshot.value = newSnapshot;
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -61,8 +64,8 @@ async function confirmSnapshot() {
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
|
||||
<div class="poppins text-center">
|
||||
Create a snapshot
|
||||
<div class="poppins text-center text-lyx-lightmode-text dark:text-lyx-text">
|
||||
Create a timeframe
|
||||
</div>
|
||||
|
||||
<div class="mt-10 flex items-center gap-2">
|
||||
@@ -71,7 +74,7 @@ async function confirmSnapshot() {
|
||||
<input @input="onColorChange" ref="colorpicker" class="relative w-0 h-0 z-[-100]" type="color">
|
||||
</div>
|
||||
<div class="grow">
|
||||
<LyxUiInput placeholder="Snapshot name" v-model="snapshotName" class="px-4 py-1 w-full"></LyxUiInput>
|
||||
<LyxUiInput placeholder="Timeframe name" v-model="snapshotName" class="px-4 py-1 w-full"></LyxUiInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ButtonType } from '../LyxUi/Button.vue';
|
||||
|
||||
const emit = defineEmits(['success', 'cancel'])
|
||||
|
||||
const props = defineProps<{
|
||||
buttonType: string,
|
||||
buttonType: ButtonType,
|
||||
message: string,
|
||||
deleteData: { isAll: boolean, visits: boolean, sessions: boolean, events: boolean, domain: string }
|
||||
}>();
|
||||
@@ -47,7 +48,7 @@ async function deleteData() {
|
||||
overlay: {
|
||||
background: 'bg-lyx-background/85'
|
||||
},
|
||||
background: 'bg-lyx-widget',
|
||||
background: 'bg-lyx-lightmode-widget dark:bg-lyx-widget',
|
||||
ring: 'border-solid border-[1px] border-[#262626]'
|
||||
}">
|
||||
<div class="h-full flex flex-col gap-2 p-4">
|
||||
@@ -71,7 +72,8 @@ async function deleteData() {
|
||||
|
||||
<div v-if="!isDone" class="flex justify-end gap-2">
|
||||
<LyxUiButton type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton>
|
||||
<LyxUiButton :disabled="!canDelete" @click="canDelete ? deleteData() : () => { }" :type="buttonType"> Confirm </LyxUiButton>
|
||||
<LyxUiButton :disabled="!canDelete" @click="canDelete ? deleteData() : () => { }" :type="buttonType">
|
||||
Confirm </LyxUiButton>
|
||||
</div>
|
||||
|
||||
<div v-if="isDone" class="flex justify-end w-full">
|
||||
|
||||
@@ -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>
|
||||
|
||||
58
dashboard/components/dialog/Help.vue
Normal file
58
dashboard/components/dialog/Help.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { createAlert } = useAlert();
|
||||
const { close } = useModal()
|
||||
|
||||
function copyEmail() {
|
||||
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
||||
navigator.clipboard.writeText('help@litlyx.com');
|
||||
createAlert('Success', 'Email copied successfully.', 'far fa-circle-check', 5000);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :ui="{
|
||||
strategy: 'override',
|
||||
overlay: {
|
||||
background: 'bg-lyx-background/85'
|
||||
},
|
||||
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">
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
<div class="font-medium">
|
||||
Contact Support
|
||||
</div>
|
||||
|
||||
<div class="dark:text-lyx-text-dark">
|
||||
Contact Support for any questions or issues you have.
|
||||
</div>
|
||||
|
||||
<div class="dark:bg-lyx-widget-lighter bg-lyx-lightmode-widget h-[1px]"></div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="p-2 bg-lyx-lightmode-widget dark:bg-[#1c1b1b] rounded-md w-full">
|
||||
<div class="w-full text-[.9rem] dark:text-[#acacac]"> help@litlyx.com </div>
|
||||
</div>
|
||||
<LyxUiButton type="secondary" @click="copyEmail()"> Copy </LyxUiButton>
|
||||
<LyxUiButton type="secondary" to="mailto:help@litlyx.com"> Send </LyxUiButton>
|
||||
</div>
|
||||
|
||||
<div class="dark:text-lyx-text-dark mt-2">
|
||||
or text us on Discord, we will reply to you personally.
|
||||
</div>
|
||||
|
||||
<LyxUiButton to="https://discord.gg/9cQykjsmWX" target="_blank" type="secondary">
|
||||
Discord Support
|
||||
</LyxUiButton>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</UModal>
|
||||
|
||||
</template>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import DateService, { type Slice } from '@services/DateService';
|
||||
import { type Slice } from '@services/DateService';
|
||||
|
||||
const props = defineProps<{ slice: Slice }>();
|
||||
const slice = computed(() => props.slice);
|
||||
@@ -10,45 +10,23 @@ const { safeSnapshotDates } = useSnapshot()
|
||||
|
||||
function transformResponse(input: { _id: string, name: string, count: number }[]) {
|
||||
|
||||
const fixed = fixMetrics({
|
||||
data: input,
|
||||
from: input[0]._id,
|
||||
to: safeSnapshotDates.value.to
|
||||
},
|
||||
const fixed = fixMetrics(
|
||||
{ data: input, from: input[0]._id, to: safeSnapshotDates.value.to },
|
||||
slice.value,
|
||||
{ advanced: true, advancedGroupKey: 'name' });
|
||||
{ advanced: true, advancedGroupKey: 'name' }
|
||||
);
|
||||
|
||||
const parsedDatasets: any[] = [];
|
||||
|
||||
const colors = [
|
||||
"#5655d0",
|
||||
"#6bbbe3",
|
||||
"#a6d5cb",
|
||||
"#fae0b9",
|
||||
"#f28e8e",
|
||||
"#e3a7e4",
|
||||
"#c4a8e1",
|
||||
"#8cc1d8",
|
||||
"#f9c2cd",
|
||||
"#b4e3b2",
|
||||
"#ffdfba",
|
||||
"#e9c3b5",
|
||||
"#d5b8d6",
|
||||
"#add7f6",
|
||||
"#ffd1dc",
|
||||
"#ffe7a1",
|
||||
"#a8e6cf",
|
||||
"#d4a5a5",
|
||||
"#f3d6e4",
|
||||
"#c3aed6"
|
||||
"#5655d0", "#6bbbe3", "#a6d5cb", "#fae0b9", "#f28e8e",
|
||||
"#e3a7e4", "#c4a8e1", "#8cc1d8", "#f9c2cd", "#b4e3b2",
|
||||
"#ffdfba", "#e9c3b5", "#d5b8d6", "#add7f6", "#ffd1dc",
|
||||
"#ffe7a1", "#a8e6cf", "#d4a5a5", "#f3d6e4", "#c3aed6"
|
||||
];
|
||||
|
||||
for (let i = 0; i < fixed.allKeys.length; i++) {
|
||||
const line: any = {
|
||||
data: [],
|
||||
color: colors[i] || '#FF0000',
|
||||
label: fixed.allKeys[i]
|
||||
};
|
||||
const line: any = { data: [], color: colors[i] || '#FF0000', label: fixed.allKeys[i] };
|
||||
parsedDatasets.push(line)
|
||||
fixed.data.forEach((e: { key: string, value: number }[]) => {
|
||||
const target = e.find(e => e.key == fixed.allKeys[i]);
|
||||
@@ -56,12 +34,7 @@ function transformResponse(input: { _id: string, name: string, count: number }[]
|
||||
line.data.push(target.value);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
datasets: parsedDatasets,
|
||||
labels: fixed.labels
|
||||
}
|
||||
|
||||
return { datasets: parsedDatasets, labels: fixed.labels }
|
||||
}
|
||||
|
||||
const errorData = ref<{ errored: boolean, text: string }>({
|
||||
@@ -88,7 +61,6 @@ const eventsStackedData = useFetch(`/api/timeline/events_stacked`, {
|
||||
onResponse
|
||||
});
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
eventsStackedData.execute();
|
||||
});
|
||||
|
||||
@@ -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`"
|
||||
|
||||
@@ -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>
|
||||
|
||||
53
dashboard/components/layout/TopNavigation.vue
Normal file
53
dashboard/components/layout/TopNavigation.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { DialogFeedback, DialogHelp } from '#components';
|
||||
|
||||
const modal = useModal();
|
||||
const selfhosted = useSelfhosted();
|
||||
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const isDark = computed({
|
||||
get() {
|
||||
return colorMode.value === 'dark'
|
||||
},
|
||||
set() {
|
||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full overflow-y-auto hide-scrollbars h-[4rem] border-solid border-[#D9D9E0] dark:border-[#202020] border-b-[1px] bg-lyx-lightmode-background dark:bg-lyx-background flex dark:shadow-[1px_0_10px_#000000]">
|
||||
|
||||
<div class="flex items-center px-6">
|
||||
<SelectorDomainSelector></SelectorDomainSelector>
|
||||
</div>
|
||||
|
||||
<div class="grow"></div>
|
||||
<div class="flex items-center gap-6 mr-10">
|
||||
<div v-if="!selfhosted" @click="modal.open(DialogFeedback, {});"
|
||||
class="flex gap-2 items-center cursor-pointer">
|
||||
<i class="far fa-message"></i>
|
||||
Feedback
|
||||
</div>
|
||||
<div @click="modal.open(DialogHelp, {});" class="cursor-pointer"> Help </div>
|
||||
<NuxtLink to="https://docs.litlyx.com" target="_blank" class="cursor-pointer">
|
||||
Docs
|
||||
</NuxtLink>
|
||||
|
||||
|
||||
<div>
|
||||
<UTooltip :text="isDark ? 'Toggle light mode' : 'Toggle dark mode'">
|
||||
<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>
|
||||
</UTooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import CreateSnapshot from './dialog/CreateSnapshot.vue';
|
||||
import CreateSnapshot from '../dialog/CreateSnapshot.vue';
|
||||
|
||||
export type Entry = {
|
||||
label: string,
|
||||
@@ -23,6 +23,7 @@ type Props = {
|
||||
sections: Section[]
|
||||
}
|
||||
|
||||
|
||||
const route = useRoute();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
@@ -108,7 +109,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
|
||||
@@ -126,7 +127,7 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
||||
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
|
||||
<ProjectSelector></ProjectSelector>
|
||||
<SelectorProjectSelector></SelectorProjectSelector>
|
||||
|
||||
<div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden">
|
||||
<i @click="close()" class="fas fa-close"></i>
|
||||
@@ -157,9 +158,9 @@ 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
|
||||
Timeframes
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
@@ -168,9 +169,9 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
||||
<div><i class="far fa-download text-[.8rem]"></i></div>
|
||||
</LyxUiButton>
|
||||
</UTooltip> -->
|
||||
<UTooltip text="Create new snapshot">
|
||||
<UTooltip text="Create new timeframe">
|
||||
<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 +180,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 +205,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 +239,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 @click="generatePDF()" 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 +255,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 +283,20 @@ 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> -->
|
||||
|
||||
<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>
|
||||
@@ -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">
|
||||
|
||||
48
dashboard/components/selector/DomainSelector.vue
Normal file
48
dashboard/components/selector/DomainSelector.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { domainList, domain, setActiveDomain, refreshDomains, refreshingDomains } = useDomain();
|
||||
|
||||
function onChange(e: string) {
|
||||
setActiveDomain(e);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2 absolute">
|
||||
<USelectMenu :uiMenu="{
|
||||
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 w-max',
|
||||
option: {
|
||||
base: 'z-[990] 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'
|
||||
},
|
||||
input: 'z-[999] !bg-lyx-lightmode-widget dark:!bg-lyx-widget-light'
|
||||
}" class="w-full" searchable searchable-placeholder="Search domain..." v-if="domainList" @change="onChange"
|
||||
:value="domain" value-attribute="_id" :options="domainList">
|
||||
|
||||
<template #option="{ option, active, selected }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||
</div>
|
||||
<div> {{ option._id }} </div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #label="e">
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||
</div>
|
||||
<div>
|
||||
{{ domain || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
<div @click="refreshDomains" v-if="!refreshingDomains"
|
||||
class="flex items-center hover:rotate-[60deg] transition-all duration-200 ease-in-out cursor-pointer">
|
||||
<i class="far fa-refresh"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,8 +4,8 @@ import type { TProject } from '@schema/project/ProjectSchema';
|
||||
|
||||
const { user } = useLoggedUser()
|
||||
|
||||
const { projectList, guestProjectList,allProjectList, actions, project } = useProject();
|
||||
|
||||
const { projectList, guestProjectList, allProjectList, actions, project } = useProject();
|
||||
const { setActiveDomain } = useDomain();
|
||||
|
||||
function isProjectMine(owner?: string) {
|
||||
if (!owner) return false;
|
||||
@@ -16,17 +16,18 @@ function isProjectMine(owner?: string) {
|
||||
|
||||
function onChange(e: TProject) {
|
||||
actions.setActiveProject(e._id.toString());
|
||||
setActiveDomain('ALL DOMAINS');
|
||||
}
|
||||
</script>
|
||||
|
||||
<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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { TApiSettings } from '@schema/ApiSettingsSchema';
|
||||
import type { SettingsTemplateEntry } from './Template.vue';
|
||||
|
||||
const { project } = useProject();
|
||||
const { project, isGuest } = useProject();
|
||||
|
||||
const entries: SettingsTemplateEntry[] = [
|
||||
{ id: 'acodes', title: 'Appsumo codes', text: 'Redeem appsumo codes' },
|
||||
@@ -39,7 +39,7 @@ async function redeemCode() {
|
||||
|
||||
|
||||
<template>
|
||||
<SettingsTemplate :entries="entries" :key="project?.name || 'NONE'">
|
||||
<SettingsTemplate v-if="!isGuest" :entries="entries" :key="project?.name || 'NONE'">
|
||||
<template #acodes>
|
||||
<div class="flex items-center gap-4">
|
||||
<LyxUiInput class="w-full px-4 py-2" placeholder="Appsumo code" v-model="currentCode"></LyxUiInput>
|
||||
@@ -53,6 +53,14 @@ 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>
|
||||
|
||||
<div v-if="isGuest" class="text-lyx-text-darker flex w-full h-full justify-center mt-20">
|
||||
Guests cannot view billing
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import DeleteDomainData from '../dialog/DeleteDomainData.vue';
|
||||
import type { SettingsTemplateEntry } from './Template.vue';
|
||||
|
||||
|
||||
const { isGuest } = useProject();
|
||||
|
||||
const entries: SettingsTemplateEntry[] = [
|
||||
{ id: 'delete_dns', title: 'Delete domain data', text: 'Delete data of a specific domain from this project' },
|
||||
{ id: 'delete_data', title: 'Delete project data', text: 'Delete all data from this project' },
|
||||
@@ -105,15 +108,17 @@ const sessionsLabel = computed(() => {
|
||||
<div class="flex flex-col">
|
||||
|
||||
<!-- <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',
|
||||
<USelectMenu v-if="!isGuest" placeholder="Select a domain" :uiMenu="{
|
||||
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'
|
||||
}
|
||||
}" :options="domains.data.value ?? []" v-model="selectedDomain"></USelectMenu>
|
||||
|
||||
<div v-if="isGuest" class="text-lyx-text-darker"> Guests cannot delete data</div>
|
||||
|
||||
<div v-if="selectedDomain" class="flex flex-col gap-2 mt-4">
|
||||
<div class="text-[.9rem] text-lyx-text-dark"> Select data to delete </div>
|
||||
|
||||
@@ -140,15 +145,18 @@ 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]">
|
||||
|
||||
<div v-if="!isGuest"
|
||||
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-lyx-lightmode-widget-light dark:bg-[#1e1412]">
|
||||
<div class="poppins font-semibold"> This operation will reset this project to it's initial state (0
|
||||
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>
|
||||
|
||||
<div v-if="isGuest" class="text-lyx-text-darker"> Guests cannot delete data</div>
|
||||
</template>
|
||||
</SettingsTemplate>
|
||||
</template>
|
||||
|
||||
@@ -156,20 +156,28 @@ function copyProjectId() {
|
||||
<template>
|
||||
<SettingsTemplate :entries="entries" :key="project?.name || 'NONE'">
|
||||
<template #pname>
|
||||
<div class="flex items-center gap-4">
|
||||
<LyxUiInput class="w-full px-4 py-2" :disabled="isGuest" v-model="projectNameInputVal"></LyxUiInput>
|
||||
<LyxUiButton v-if="!isGuest" @click="changeProjectName()" :disabled="!canChange" type="primary"> Change
|
||||
</LyxUiButton>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<LyxUiInput class="w-full px-4 py-2" :disabled="isGuest" v-model="projectNameInputVal"></LyxUiInput>
|
||||
<LyxUiButton v-if="!isGuest" @click="changeProjectName()" :disabled="!canChange" type="primary">
|
||||
Change
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
<div v-if="isGuest" class="text-lyx-text-darker"> *Guests cannot change project name </div>
|
||||
</div>
|
||||
</template>
|
||||
<template #api>
|
||||
<div class="flex items-center gap-4" v-if="apiKeys && apiKeys.length < 5">
|
||||
<LyxUiInput class="grow px-4 py-2" :disabled="isGuest" placeholder="ApiKeyName" v-model="newApiKeyName">
|
||||
</LyxUiInput>
|
||||
<LyxUiButton v-if="!isGuest" @click="createApiKey()" :disabled="newApiKeyName.length < 3"
|
||||
type="primary">
|
||||
<i class="far fa-plus"></i>
|
||||
</LyxUiButton>
|
||||
<div class="flex flex-col gap-2" v-if="apiKeys && apiKeys.length < 5">
|
||||
<div class="flex items-center gap-4">
|
||||
<LyxUiInput class="grow px-4 py-2" :disabled="isGuest" placeholder="ApiKeyName"
|
||||
v-model="newApiKeyName">
|
||||
</LyxUiInput>
|
||||
<LyxUiButton v-if="!isGuest" @click="createApiKey()" :disabled="newApiKeyName.length < 3"
|
||||
type="primary">
|
||||
<i class="far fa-plus"></i>
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
<div v-if="isGuest" class="text-lyx-text-darker"> *Guests cannot manage api keys </div>
|
||||
</div>
|
||||
<LyxUiCard v-if="apiKeys && apiKeys.length > 0" class="w-full flex flex-col gap-4 items-center mt-4">
|
||||
<div v-for="apiKey of apiKeys" class="flex flex-col w-full">
|
||||
@@ -201,10 +209,10 @@ function copyProjectId() {
|
||||
<div class="hidden lg:flex"><i class="far fa-copy" @click="copyScript()"></i></div>
|
||||
</LyxUiCard>
|
||||
<div class="flex justify-end w-full">
|
||||
<LyxUiButton type="outline" class="flex lg:hidden mt-4">
|
||||
Copy script
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
<LyxUiButton type="outline" class="flex lg:hidden mt-4">
|
||||
Copy script
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
</template>
|
||||
<template #pdelete>
|
||||
<div class="flex lg:justify-end" v-if="!isGuest">
|
||||
@@ -212,6 +220,7 @@ function copyProjectId() {
|
||||
Delete project
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
<div v-if="isGuest"> *Guests cannot delete project </div>
|
||||
</template>
|
||||
</SettingsTemplate>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
@@ -123,29 +123,29 @@ const { showDrawer } = useDrawer();
|
||||
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||
</div>
|
||||
|
||||
<SettingsTemplate v-if="!invoicesPending && !planPending" :entries="entries">
|
||||
<SettingsTemplate v-if="!invoicesPending && !planPending && !isGuest" :entries="entries">
|
||||
<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,8 @@ 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 +195,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 +213,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 +241,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>
|
||||
@@ -266,6 +267,10 @@ const { showDrawer } = useDrawer();
|
||||
</CardTitled>
|
||||
</template>
|
||||
</SettingsTemplate>
|
||||
|
||||
<div v-if="isGuest" class="text-lyx-text-darker flex w-full h-full justify-center mt-20">
|
||||
Guests cannot view billing
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ const entries: SettingsTemplateEntry[] = [
|
||||
User should have been registered to Litlyx
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isGuest" class="text-lyx-text-darker"> Guests cannot add members</div>
|
||||
</template>
|
||||
<template #members>
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
||||
color: '#FF8531',
|
||||
default: true
|
||||
}
|
||||
|
||||
|
||||
|
||||
const lastMonth: DefaultSnapshot = {
|
||||
project_id,
|
||||
@@ -76,7 +76,7 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
||||
project_id,
|
||||
_id: '___allTime' as any,
|
||||
name: 'All Time',
|
||||
from: new Date(project_created_at.toString()),
|
||||
from: fns.addMinutes(fns.startOfMonth(new Date(project_created_at.toString())), 0),
|
||||
to: new Date(Date.now()),
|
||||
color: '#9362FF',
|
||||
default: true
|
||||
|
||||
@@ -3,16 +3,18 @@ type RefOrPrimitive<T> = T | Ref<T> | ComputedRef<T>
|
||||
|
||||
export type CustomOptions = {
|
||||
useSnapshotDates?: boolean,
|
||||
useActiveDomain?: boolean,
|
||||
useActivePid?: boolean,
|
||||
useTimeOffset?: boolean,
|
||||
slice?: RefOrPrimitive<string>,
|
||||
limit?: RefOrPrimitive<number | string>,
|
||||
custom?: Record<string, RefOrPrimitive<string>>
|
||||
custom?: Record<string, RefOrPrimitive<string>>,
|
||||
}
|
||||
|
||||
const { token } = useAccessToken();
|
||||
const { projectId } = useProject();
|
||||
const { safeSnapshotDates } = useSnapshot()
|
||||
const { domain } = useDomain();
|
||||
|
||||
function getValueFromRefOrPrimitive<T>(data?: T | Ref<T> | ComputedRef<T>) {
|
||||
if (!data) return;
|
||||
@@ -24,6 +26,7 @@ export function useComputedHeaders(customOptions?: CustomOptions) {
|
||||
const useSnapshotDates = customOptions?.useSnapshotDates || true;
|
||||
const useActivePid = customOptions?.useActivePid || true;
|
||||
const useTimeOffset = customOptions?.useTimeOffset || true;
|
||||
const useActiveDomain = customOptions?.useActiveDomain || true;
|
||||
|
||||
const headers = computed<Record<string, string>>(() => {
|
||||
// console.trace('Computed recalculated');
|
||||
@@ -41,6 +44,7 @@ export function useComputedHeaders(customOptions?: CustomOptions) {
|
||||
'x-time-offset': useTimeOffset ? (new Date().getTimezoneOffset().toString()) : '',
|
||||
'x-slice': getValueFromRefOrPrimitive(customOptions?.slice) ?? '',
|
||||
'x-limit': getValueFromRefOrPrimitive(customOptions?.limit)?.toString() ?? '',
|
||||
'x-domain': useActiveDomain ? (domain.value ?? '') : '',
|
||||
...parsedCustom
|
||||
}
|
||||
})
|
||||
|
||||
47
dashboard/composables/useDomain.ts
Normal file
47
dashboard/composables/useDomain.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
|
||||
const { token } = useAccessToken();
|
||||
const { projectId } = useProject();
|
||||
|
||||
const domainsRequest = useFetch<{ _id: string, visits: number }[]>('/api/domains/list', {
|
||||
headers: computed(() => {
|
||||
return {
|
||||
'Authorization': `Bearer ${token.value}`,
|
||||
'x-pid': projectId.value || ''
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function refreshDomains() {
|
||||
domainsRequest.refresh();
|
||||
}
|
||||
|
||||
const refreshingDomains = computed(() => domainsRequest.pending.value);
|
||||
|
||||
const domainList = computed(() => {
|
||||
return [
|
||||
{
|
||||
_id: 'ALL DOMAINS', visits: domainsRequest.data.value?.reduce((a, e) => a + e.visits, 0)
|
||||
},
|
||||
...(domainsRequest.data.value?.sort((a, b) => b.visits - a.visits) || [])
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
const activeDomain = ref<string>();
|
||||
|
||||
const domain = computed(() => {
|
||||
if (activeDomain.value) return activeDomain.value;
|
||||
if (!domainList.value) return;
|
||||
if (domainList.value.length == 0) return;
|
||||
setActiveDomain(domainList.value[0]._id);
|
||||
return domainList.value[0]._id;
|
||||
})
|
||||
|
||||
function setActiveDomain(domain: string) {
|
||||
activeDomain.value = domain;
|
||||
}
|
||||
|
||||
export function useDomain() {
|
||||
return { domainList, domain, setActiveDomain, refreshDomains, refreshingDomains }
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
port: '3010',
|
||||
exec_mode: 'fork',
|
||||
script: './.output/server/index.mjs',
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import type { Section } from '~/components/CVerticalNavigation.vue';
|
||||
import type { Section } from '~/components/layout/VerticalNavigation.vue';
|
||||
|
||||
import { Lit } from 'litlyx-js';
|
||||
import { DialogFeedback } from '#components';
|
||||
@@ -19,29 +19,32 @@ const sections: Section[] = [
|
||||
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
||||
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
||||
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
|
||||
{ label: 'Security', to: '/security', icon: 'fal fa-shield', disabled: selfhosted },
|
||||
|
||||
// { label: 'Security', to: '/security', icon: 'fal fa-shield', disabled: selfhosted },
|
||||
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||
// { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
|
||||
|
||||
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
|
||||
{
|
||||
grow: true,
|
||||
label: 'Leave a Feedback', icon: 'fal fa-message',
|
||||
action() {
|
||||
modal.open(DialogFeedback, {});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Documentation', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true,
|
||||
action() { Lit.event('docs_clicked') },
|
||||
},
|
||||
{
|
||||
label: 'Discord support', icon: 'fab fa-discord',
|
||||
to: 'https://discord.gg/9cQykjsmWX',
|
||||
external: true,
|
||||
},
|
||||
{ grow: true, label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
|
||||
// {
|
||||
// grow: true,
|
||||
// label: 'Leave a Feedback', icon: 'fal fa-message',
|
||||
// action() {
|
||||
// modal.open(DialogFeedback, {});
|
||||
// },
|
||||
// disabled: selfhosted
|
||||
// },
|
||||
// {
|
||||
// grow: true,
|
||||
// label: 'Documentation', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true,
|
||||
// action() { Lit.event('docs_clicked') },
|
||||
// },
|
||||
// {
|
||||
// grow: true,
|
||||
// label: 'Discord support', icon: 'fab fa-discord',
|
||||
// to: 'https://discord.gg/9cQykjsmWX',
|
||||
// external: true,
|
||||
// },
|
||||
// {
|
||||
// label: 'Slack support', icon: 'fab fa-slack',
|
||||
// to: '#',
|
||||
@@ -73,11 +76,11 @@ const { isOpen, close, open } = useMenu();
|
||||
|
||||
|
||||
<div
|
||||
class="px-6 py-3 flex items-center justify-center shadow-[0_0_10px_#000000CC] z-[20] rounded-xl mx-2 my-2 lg:hidden">
|
||||
class="px-6 py-3 flex items-center justify-center dark:bg-lyx-background-light z-[20] rounded-xl mx-2 my-2 lg:hidden">
|
||||
<i @click="open()" class="fas fa-bars text-[1.2rem] absolute left-6"></i>
|
||||
<div class="nunito font-semibold text-[1.2rem]">
|
||||
<!-- <div class="nunito font-semibold text-[1.2rem]">
|
||||
Litlyx
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="flex h-full">
|
||||
@@ -88,15 +91,15 @@ const { isOpen, close, open } = useMenu();
|
||||
</div>
|
||||
|
||||
|
||||
<CVerticalNavigation :sections="sections">
|
||||
</CVerticalNavigation>
|
||||
<LayoutVerticalNavigation :sections="sections">
|
||||
</LayoutVerticalNavigation>
|
||||
|
||||
|
||||
<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"
|
||||
@@ -104,7 +107,12 @@ const { isOpen, close, open } = useMenu();
|
||||
<DashboardDialogBarCard @click.stop="null" class="z-[36]"></DashboardDialogBarCard>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
<LayoutTopNavigation class="flex"></LayoutTopNavigation>
|
||||
|
||||
<div class="h-full pb-[3rem]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -26,15 +26,16 @@ export default defineNuxtConfig({
|
||||
|
||||
pages: true,
|
||||
ssr: false,
|
||||
css: ['~/assets/scss/main.scss'],
|
||||
|
||||
css: [
|
||||
'~/assets/main.css',
|
||||
'~/assets/scss/main.scss',
|
||||
],
|
||||
alias: {
|
||||
'@schema': fileURLToPath(new URL('../shared/schema', import.meta.url)),
|
||||
'@services': fileURLToPath(new URL('../shared/services', import.meta.url)),
|
||||
'@data': fileURLToPath(new URL('../shared/data', import.meta.url)),
|
||||
'@functions': fileURLToPath(new URL('../shared/functions', import.meta.url)),
|
||||
'@schema': fileURLToPath(new URL('./shared/schema', import.meta.url)),
|
||||
'@services': fileURLToPath(new URL('./shared/services', import.meta.url)),
|
||||
'@data': fileURLToPath(new URL('./shared/data', import.meta.url)),
|
||||
'@functions': fileURLToPath(new URL('./shared/functions', import.meta.url)),
|
||||
},
|
||||
|
||||
runtimeConfig: {
|
||||
MONGO_CONNECTION_STRING: process.env.MONGO_CONNECTION_STRING,
|
||||
REDIS_URL: process.env.REDIS_URL,
|
||||
@@ -43,8 +44,7 @@ export default defineNuxtConfig({
|
||||
AI_ORG: process.env.AI_ORG,
|
||||
AI_PROJECT: process.env.AI_PROJECT,
|
||||
AI_KEY: process.env.AI_KEY,
|
||||
EMAIL_SERVICE: process.env.EMAIL_SERVICE,
|
||||
BREVO_API_KEY: process.env.BREVO_API_KEY,
|
||||
EMAIL_SECRET: process.env.EMAIL_SECRET,
|
||||
AUTH_JWT_SECRET: process.env.AUTH_JWT_SECRET,
|
||||
GOOGLE_AUTH_CLIENT_ID: process.env.GOOGLE_AUTH_CLIENT_ID,
|
||||
GOOGLE_AUTH_CLIENT_SECRET: process.env.GOOGLE_AUTH_CLIENT_SECRET,
|
||||
@@ -55,8 +55,9 @@ 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,
|
||||
MODE: process.env.MODE || 'NONE',
|
||||
SELFHOSTED: process.env.SELFHOSTED || 'FALSE',
|
||||
public: {
|
||||
AUTH_MODE: process.env.AUTH_MODE,
|
||||
GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE',
|
||||
|
||||
@@ -3,32 +3,39 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"build": "npm run workspace:shared && nuxt build --dotenv .env.testmode",
|
||||
"build:prod": "npm run workspace:shared && nuxt build --dotenv .env.prod",
|
||||
"dev": "npm run workspace:shared && nuxt dev --dotenv .env.testmode",
|
||||
"dev:prod": "npm run workspace:shared && nuxi dev --dotenv .env.prod",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"test": "vitest",
|
||||
"docker-build": "docker build -t litlyx-dashboard -f Dockerfile ../",
|
||||
"docker-inspect": "docker run -it litlyx-dashboard sh",
|
||||
"docker-run": "docker run -p 3000:3000 litlyx-dashboard"
|
||||
"docker-run": "docker run -p 3000:3000 litlyx-dashboard",
|
||||
"workspace:shared": "ts-node ../scripts/dashboard/shared.ts",
|
||||
"workspace:deploy": "ts-node ../scripts/dashboard/deploy.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||
"chart.js": "^3.9.1",
|
||||
"chartjs-chart-funnel": "^4.2.1",
|
||||
"chartjs-plugin-annotation": "^2.2.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"google-auth-library": "^9.10.0",
|
||||
"googleapis": "^144.0.0",
|
||||
"highlight.js": "^11.10.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"litlyx-js": "^1.0.3",
|
||||
"mongoose": "^8.9.5",
|
||||
"nuxt": "^3.11.2",
|
||||
"nuxt-vue3-google-signin": "^0.0.11",
|
||||
"openai": "^4.61.0",
|
||||
"pdfkit": "^0.15.0",
|
||||
"primevue": "^3.52.0",
|
||||
"sass": "^1.81.0",
|
||||
"redis": "^4.7.0",
|
||||
"sass": "^1.83.4",
|
||||
"stripe": "^17.3.1",
|
||||
"v-calendar": "^3.1.2",
|
||||
"vue": "^3.4.21",
|
||||
@@ -39,6 +46,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",
|
||||
@@ -46,4 +56,4 @@
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]">
|
||||
@@ -392,29 +398,35 @@ async function clearAllChats() {
|
||||
<div :class="{ '!text-green-500': debugModeAi }" class="cursor-pointer text-red-500 w-fit"
|
||||
v-if="userRoles.isAdmin.value" @click="debugModeAi = !debugModeAi"> Debug mode </div>
|
||||
|
||||
<div class="flex justify-between items-center pt-3">
|
||||
<div class="flex pt-3 px-4">
|
||||
<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="bg-accent w-4 h-4 rounded-full animate-pulse">
|
||||
</div> -->
|
||||
<div class="manrope font-semibold text-lyx-lightmode-text dark:text-text-dirty">
|
||||
{{ chatsRemaining }} messages left
|
||||
</div>
|
||||
</div>
|
||||
<LyxUiButton type="primary" class="text-[.9rem] text-center " @click="showDrawer('PRICING')">
|
||||
<div class="grow"></div>
|
||||
<LyxUiButton v-if="!selfhosted" type="primary" class="text-[.9rem] text-center "
|
||||
@click="showDrawer('PRICING')">
|
||||
Upgrade
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="dark:bg-lyx-widget-light bg-lyx-lightmode-widget-light h-[1px]"></div>
|
||||
|
||||
<div class="flex items-center gap-4 px-4 mt-4">
|
||||
<div class="poppins font-semibold text-[1.1rem]"> History </div>
|
||||
<div class="grow"></div>
|
||||
<LyxUiButton v-if="chatsList && chatsList.length > 0" @click="clearAllChats()" type="secondary"
|
||||
class="text-center text-[.8rem]">
|
||||
Clear all
|
||||
Clear all chats
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
|
||||
<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 +436,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 +470,6 @@ async function clearAllChats() {
|
||||
font-weight: bold;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
p:last-of-type {
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const canDownload = computed(() => {
|
||||
const metricsInfo = ref<number>(0);
|
||||
|
||||
const columns = [
|
||||
{ key: 'website', label: 'Website', sortable: true },
|
||||
{ key: 'website', label: 'Domain', sortable: true },
|
||||
{ key: 'page', label: 'Page', sortable: true },
|
||||
{ key: 'referrer', label: 'Referrer', sortable: true },
|
||||
{ key: 'browser', label: 'Browser', sortable: true },
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import EventsFunnelChart from '~/components/events/EventsFunnelChart.vue';
|
||||
|
||||
import DateService, { type Slice } from '@services/DateService';
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const selectLabelsEvents = [
|
||||
|
||||
const { snapshotDuration } = useSnapshot();
|
||||
|
||||
const selectedLabelIndex = ref<number>(1);
|
||||
|
||||
const selectLabels: { label: string, value: Slice }[] = [
|
||||
{ label: 'Hour', value: 'hour' },
|
||||
{ label: 'Day', value: 'day' },
|
||||
{ label: 'Month', value: 'month' },
|
||||
];
|
||||
const eventsStackedSelectIndex = ref<number>(0);
|
||||
|
||||
const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeaders({ custom: { 'x-schema': 'events' } }), lazy: true });
|
||||
const selectLabelsAvailable = computed<{ label: string, value: Slice, disabled: boolean }[]>(() => {
|
||||
return selectLabels.map(e => {
|
||||
return { ...e, disabled: !DateService.canUseSliceFromDays(snapshotDuration.value, e.value)[0] }
|
||||
});
|
||||
})
|
||||
|
||||
const eventsData = await useFetch(`/api/data/count`, {
|
||||
headers: useComputedHeaders({ custom: { 'x-schema': 'events' } }),
|
||||
lazy: true
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -24,9 +38,6 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
||||
<div>
|
||||
Total events: {{ eventsData.data.value?.[0]?.count || '0' }}
|
||||
</div>
|
||||
<div v-if="(eventsData.data.value?.[0]?.count || 0) === 0">
|
||||
Waiting for your first event...
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<LyxUiButton type="secondary" target="_blank" to="https://docs.litlyx.com/custom-events">
|
||||
@@ -45,12 +56,14 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
||||
<CardTitled :key="refreshKey" class="p-4 xl:flex-[4] w-full h-full" title="Events"
|
||||
sub="Events stacked bar chart.">
|
||||
<template #header>
|
||||
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
|
||||
:currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents">
|
||||
|
||||
<SelectButton class="w-fit" @changeIndex="selectedLabelIndex = $event"
|
||||
:currentIndex="selectedLabelIndex" :options="selectLabelsAvailable">
|
||||
</SelectButton>
|
||||
|
||||
</template>
|
||||
<div class="h-full">
|
||||
<EventsStackedBarChart :slice="(selectLabelsEvents[eventsStackedSelectIndex].value as any)">
|
||||
<EventsStackedBarChart :slice="(selectLabelsAvailable[selectedLabelIndex].value as any)">
|
||||
</EventsStackedBarChart>
|
||||
</div>
|
||||
</CardTitled>
|
||||
|
||||
150
dashboard/pages/forgot_password.vue
Normal file
150
dashboard/pages/forgot_password.vue
Normal 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>
|
||||
@@ -18,7 +18,6 @@ onMounted(async () => {
|
||||
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
||||
const loggedUser = useLoggedUser();
|
||||
loggedUser.user = user;
|
||||
// setTimeout(() => { location.reload(); }, 100);
|
||||
}
|
||||
|
||||
if (justLogged.value) { setTimeout(() => { location.href = '/' }, 500) }
|
||||
@@ -41,31 +40,33 @@ const selfhosted = useSelfhosted();
|
||||
|
||||
|
||||
<div v-if="showDashboard">
|
||||
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
|
||||
<BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer>
|
||||
<!-- <BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer> -->
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DashboardTopSection :key="refreshKey"></DashboardTopSection>
|
||||
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
||||
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-center mt-6 px-6">
|
||||
|
||||
<div class="flex w-full justify-center mt-6 px-6">
|
||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||
<div class="flex-1">
|
||||
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
|
||||
<BarCardPages :key="refreshKey"></BarCardPages>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex w-full justify-center mt-6 px-6">
|
||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||
<div class="flex-1">
|
||||
@@ -75,8 +76,9 @@ const selfhosted = useSelfhosted();
|
||||
<BarCardDevices :key="refreshKey"></BarCardDevices>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex w-full justify-center mt-6 px-6">
|
||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||
<div class="flex-1">
|
||||
@@ -86,7 +88,7 @@ const selfhosted = useSelfhosted();
|
||||
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const selectLabelsEvents = [
|
||||
Litlyx open metrics
|
||||
</div>
|
||||
<div v-if="project" class="flex gap-2 items-center text-text/90">
|
||||
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
||||
<div class="animate-pulse w-[.8rem] h-[.8rem] bg-green-400 rounded-full"> </div>
|
||||
<div> {{ onlineUsers }} Online users</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -56,6 +56,7 @@ async function createProject() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -69,8 +70,8 @@ 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">
|
||||
Create your {{ isFirstProject ? 'first' : '' }} project
|
||||
<div class="text-[3rem] font-semibold text-center text-lyx-lightmode-text dark:text-lyx-text">
|
||||
Create {{ isFirstProject ? '' : 'a new' }} {{ isFirstProject ? 'your first' : '' }} project
|
||||
</div>
|
||||
|
||||
<div v-if="isFirstProject" class="text-[1.5rem]">
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,18 +6,6 @@ const reportList = useFetch(`/api/security/list`, { headers: useComputedHeaders(
|
||||
|
||||
const { createAlert } = useAlert();
|
||||
|
||||
function showAnomalyInfoAlert() {
|
||||
createAlert('AI Anomaly Detector info',
|
||||
`Anomaly detector is running. It helps you detect a spike in visits or events, it could mean an
|
||||
attack or simply higher traffic due to good performance. Additionally, it can detect if someone is
|
||||
stealing parts of your website and hosting a duplicate version—an unfortunately common practice.
|
||||
Litlyx will notify you via email with actionable advices`,
|
||||
'far fa-shield',
|
||||
10000
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const rows = computed(() => reportList.data.value || [])
|
||||
|
||||
const columns = [
|
||||
@@ -33,21 +21,21 @@ 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">
|
||||
<i class="far fa-info-circle text-[.9rem] hover:text-lyx-primary cursor-pointer"
|
||||
@click="showAnomalyInfoAlert"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="pb-[10rem]">
|
||||
<UTable :rows="rows" :columns="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 +47,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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const selfhosted = useSelfhosted();
|
||||
|
||||
const items = [
|
||||
{ label: 'General', slot: 'general' },
|
||||
@@ -15,8 +16,8 @@ const items = [
|
||||
</script>
|
||||
|
||||
<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 !pb-[10rem]">
|
||||
|
||||
<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">
|
||||
@@ -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>
|
||||
|
||||
2325
dashboard/pnpm-lock.yaml
generated
2325
dashboard/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,12 +1,11 @@
|
||||
import { AuthContext } from "./middleware/01-authorization";
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
import { LITLYX_PROJECT_ID } from '@data/LITLYX'
|
||||
import { hasAccessToProject } from "./utils/hasAccessToProject";
|
||||
|
||||
export async function getUserProjectFromId(project_id: string, user: AuthContext | undefined, allowGuest: boolean = true) {
|
||||
if (!project_id) return;
|
||||
|
||||
if (project_id === LITLYX_PROJECT_ID) {
|
||||
if (project_id === "6643cd08a1854e3b81722ab5") {
|
||||
return await ProjectModel.findOne({ _id: project_id });
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ const getVisitsCountsTool: AIPlugin_TTool<'getVisitsCount'> = {
|
||||
from: { type: 'string', description: 'ISO string of start date' },
|
||||
to: { type: 'string', description: 'ISO string of end date' },
|
||||
website: { type: 'string', description: 'The website of the visits' },
|
||||
page: { type: 'string', description: 'The page of the visit' }
|
||||
page: { type: 'string', description: 'The page of the visit' },
|
||||
domain: { type: 'string', description: 'Used only to filter a specific domain' }
|
||||
},
|
||||
required: ['from', 'to']
|
||||
}
|
||||
@@ -33,6 +34,7 @@ const getVisitsTimelineTool: AIPlugin_TTool<'getVisitsTimeline'> = {
|
||||
to: { type: 'string', description: 'ISO string of end date' },
|
||||
website: { type: 'string', description: 'The website of the visits' },
|
||||
page: { type: 'string', description: 'The page of the visit' },
|
||||
domain: { type: 'string', description: 'Used only to filter a specific domain' }
|
||||
},
|
||||
required: ['from', 'to']
|
||||
}
|
||||
@@ -45,14 +47,15 @@ export class AiVisits extends AIPlugin<['getVisitsCount', 'getVisitsTimeline']>
|
||||
|
||||
super({
|
||||
'getVisitsCount': {
|
||||
handler: async (data: { project_id: string, from: string, to: string, website?: string, page?: string }) => {
|
||||
handler: async (data: { project_id: string, from: string, to: string, website?: string, page?: string, domain?: string }) => {
|
||||
|
||||
const query: any = {
|
||||
project_id: data.project_id,
|
||||
created_at: {
|
||||
$gt: new Date(data.from),
|
||||
$lt: new Date(data.to),
|
||||
}
|
||||
},
|
||||
website: data.domain || { $ne: '_NODOMAIN_' }
|
||||
}
|
||||
|
||||
if (data.website) query.website = data.website;
|
||||
@@ -67,7 +70,7 @@ export class AiVisits extends AIPlugin<['getVisitsCount', 'getVisitsTimeline']>
|
||||
tool: getVisitsCountsTool
|
||||
},
|
||||
'getVisitsTimeline': {
|
||||
handler: async (data: { project_id: string, from: string, to: string, time_offset: number, website?: string, page?: string }) => {
|
||||
handler: async (data: { project_id: string, from: string, to: string, time_offset: number, website?: string, page?: string, domain?: string }) => {
|
||||
|
||||
const timelineData = await executeTimelineAggregation({
|
||||
projectId: new Types.ObjectId(data.project_id),
|
||||
@@ -75,7 +78,8 @@ export class AiVisits extends AIPlugin<['getVisitsCount', 'getVisitsTimeline']>
|
||||
from: data.from,
|
||||
to: data.to,
|
||||
slice: 'day',
|
||||
timeOffset: data.time_offset
|
||||
timeOffset: data.time_offset,
|
||||
domain: data.domain || { $ne: '_NODOMAIN_' } as any
|
||||
});
|
||||
return { data: timelineData };
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event);
|
||||
const data = await getRequestDataOld(event);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
@@ -4,7 +4,7 @@ import type OpenAI from "openai";
|
||||
import { getChartsInMessage } from "~/server/services/AiService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event);
|
||||
const data = await getRequestDataOld(event);
|
||||
if (!data) return;
|
||||
|
||||
const isAdmin = data.user.user.roles.includes('ADMIN');
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event);
|
||||
const data = await getRequestDataOld(event);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event);
|
||||
const data = await getRequestDataOld(event);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function getAiChatRemainings(project_id: string) {
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event);
|
||||
const data = await getRequestDataOld(event);
|
||||
if (!data) return;
|
||||
|
||||
const { pid } = data;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event);
|
||||
const data = await getRequestDataOld(event);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event);
|
||||
const data = await getRequestDataOld(event);
|
||||
if (!data) return;
|
||||
|
||||
const { pid } = data;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
import { createUserJwt, readRegisterJwt } from '~/server/AuthManager';
|
||||
import { createUserJwt, readRegisterJwt } from '~/server/AuthManager';
|
||||
import { UserModel } from '@schema/UserSchema';
|
||||
import { PasswordModel } from '@schema/PasswordSchema';
|
||||
import EmailService from '@services/EmailService';
|
||||
import { EmailService } from '@services/EmailService';
|
||||
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
@@ -14,9 +15,12 @@ export default defineEventHandler(async event => {
|
||||
try {
|
||||
await PasswordModel.create({ email: data.email, password: data.password })
|
||||
await UserModel.create({ email: data.email, given_name: '', name: 'EmailLogin', locale: '', picture: '', created_at: Date.now() });
|
||||
setImmediate(() => { EmailService.sendWelcomeEmail(data.email); });
|
||||
setImmediate(() => {
|
||||
const emailData = EmailService.getEmailServerInfo('welcome', { target: data.email });
|
||||
EmailServiceHelper.sendEmail(emailData);
|
||||
});
|
||||
const jwt = createUserJwt({ email: data.email, name: 'EmailLogin' });
|
||||
return sendRedirect(event,`https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`);
|
||||
return sendRedirect(event, `https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`);
|
||||
} catch (ex) {
|
||||
return setResponseStatus(event, 400, 'Error creating user');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
import { createUserJwt } from '~/server/AuthManager';
|
||||
import { UserModel } from '@schema/UserSchema';
|
||||
import EmailService from '@services/EmailService';
|
||||
import { EmailService } from '@services/EmailService';
|
||||
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
|
||||
|
||||
const { GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_CLIENT_ID } = useRuntimeConfig()
|
||||
|
||||
@@ -60,7 +61,9 @@ export default defineEventHandler(async event => {
|
||||
|
||||
setImmediate(() => {
|
||||
console.log('SENDING WELCOME EMAIL TO', payload.email);
|
||||
if (payload.email) EmailService.sendWelcomeEmail(payload.email);
|
||||
if (!payload.email) return;
|
||||
const emailData = EmailService.getEmailServerInfo('welcome', { target: payload.email });
|
||||
EmailServiceHelper.sendEmail(emailData);
|
||||
});
|
||||
|
||||
return { error: false, access_token: createUserJwt({ email: savedUser.email, name: savedUser.name }) }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user