mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
Fix
This commit is contained in:
@@ -68,21 +68,47 @@ const avgBouncingRate = computed(() => {
|
||||
return avg.toFixed(2) + ' %';
|
||||
})
|
||||
|
||||
function weightedAverage(data: number[]): number {
|
||||
if (data.length === 0) return 0;
|
||||
|
||||
// Compute median
|
||||
const sortedData = [...data].sort((a, b) => a - b);
|
||||
const middle = Math.floor(sortedData.length / 2);
|
||||
const median = sortedData.length % 2 === 0
|
||||
? (sortedData[middle - 1] + sortedData[middle]) / 2
|
||||
: sortedData[middle];
|
||||
|
||||
// Define a threshold (e.g., 3 times the median) to filter out extreme values
|
||||
const threshold = median * 3;
|
||||
const filteredData = data.filter(num => num <= threshold);
|
||||
|
||||
if (filteredData.length === 0) return median; // Fallback to median if all are removed
|
||||
|
||||
// Compute weights based on inverse absolute deviation from median
|
||||
const weights = filteredData.map(num => 1 / (1 + Math.abs(num - median)));
|
||||
|
||||
// Compute weighted sum and sum of weights
|
||||
const weightedSum = filteredData.reduce((sum, num, i) => sum + num * weights[i], 0);
|
||||
const sumOfWeights = weights.reduce((sum, weight) => sum + weight, 0);
|
||||
|
||||
return weightedSum / sumOfWeights;
|
||||
}
|
||||
const avgSessionDuration = computed(() => {
|
||||
if (!sessionsDurationData.data.value) return '0.00 %'
|
||||
|
||||
const counts = sessionsDurationData.data.value.data
|
||||
.filter(e => e > 0)
|
||||
// .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)) / 5;
|
||||
const avg = weightedAverage(sessionsDurationData.data.value.data);
|
||||
// counts / (Math.max(sessionsDurationData.data.value.data.length, 1));
|
||||
|
||||
let hours = 0;
|
||||
let minutes = 0;
|
||||
let seconds = 0;
|
||||
seconds += avg * 60;
|
||||
while (seconds > 60) { seconds -= 60; minutes += 1; }
|
||||
while (minutes > 60) { minutes -= 60; hours += 1; }
|
||||
while (seconds >= 60) { seconds -= 60; minutes += 1; }
|
||||
while (minutes >= 60) { minutes -= 60; hours += 1; }
|
||||
return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s`
|
||||
});
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ const guestProjectList = computed(() => {
|
||||
return guestProjectsRequest.data.value;
|
||||
})
|
||||
|
||||
const refreshProjectsList = () => {
|
||||
projectsRequest.refresh();
|
||||
guestProjectsRequest.refresh();
|
||||
const refreshProjectsList = async () => {
|
||||
await projectsRequest.refresh();
|
||||
await guestProjectsRequest.refresh();
|
||||
}
|
||||
|
||||
const activeProjectId = ref<string | undefined>();
|
||||
|
||||
@@ -40,8 +40,10 @@ async function createProject() {
|
||||
await actions.refreshProjectsList();
|
||||
|
||||
const newActiveProjectId = projectList.value?.[projectList.value?.length - 1]._id.toString();
|
||||
|
||||
if (newActiveProjectId) {
|
||||
await actions.setActiveProject(newActiveProjectId);
|
||||
console.log('Set active project', newActiveProjectId);
|
||||
}
|
||||
|
||||
setPageLayout('dashboard');
|
||||
|
||||
@@ -8,7 +8,7 @@ export default defineEventHandler(async event => {
|
||||
|
||||
const { name } = await readBody(event);
|
||||
|
||||
if (name.trim()) return setResponseStatus(event, 400, 'name is required');
|
||||
if (name.trim().length == 0) return setResponseStatus(event, 400, 'name is required');
|
||||
if (name.trim().length < 2) return setResponseStatus(event, 400, 'name too short');
|
||||
if (name.trim().length > 32) return setResponseStatus(event, 400, 'name too long');
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export default defineEventHandler(async event => {
|
||||
}
|
||||
|
||||
|
||||
const link = `http://127.0.0.1:3000/accept_invite?project_id=${project_id.toString()}`;
|
||||
const link = `https://dashboard.litlyx.com/accept_invite?project_id=${project_id.toString()}`;
|
||||
|
||||
if (!targetUser) {
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export default defineEventHandler(async event => {
|
||||
})
|
||||
|
||||
for (const member of members) {
|
||||
const userMember = await UserModel.findById(member.user_id);
|
||||
const userMember = member.user_id ? await UserModel.findById(member.user_id) : await UserModel.findOne({ email: member.email });
|
||||
if (!userMember) continue;
|
||||
|
||||
const permission: TPermission = {
|
||||
|
||||
@@ -24,6 +24,28 @@ export default defineEventHandler(async event => {
|
||||
customProjection: {
|
||||
count: { $divide: ["$duration", "$count"] }
|
||||
},
|
||||
customQueries: [
|
||||
{
|
||||
index: 1,
|
||||
query: {
|
||||
"$lookup": {
|
||||
"from": "visits",
|
||||
"localField": "session",
|
||||
"foreignField": "session",
|
||||
"as": "visits",
|
||||
"pipeline": [{ "$limit": 1 }]
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
query: {
|
||||
"$match": {
|
||||
"visits.0": { "$exists": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
|
||||
return timelineFilledMerged;
|
||||
|
||||
@@ -20,7 +20,8 @@ export type AdvancedTimelineAggregationOptions = TimelineAggregationOptions & {
|
||||
customGroup?: Record<string, any>,
|
||||
customProjection?: Record<string, any>,
|
||||
customIdGroup?: Record<string, any>,
|
||||
customAfterMatch?: Record<string, any>
|
||||
customAfterMatch?: Record<string, any>,
|
||||
customQueries?: { index: number, query: Record<string, any> }[]
|
||||
}
|
||||
|
||||
export async function executeAdvancedTimelineAggregation<T = {}>(options: AdvancedTimelineAggregationOptions) {
|
||||
@@ -29,6 +30,7 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
|
||||
options.customGroup = options.customGroup || {};
|
||||
options.customProjection = options.customProjection || {};
|
||||
options.customIdGroup = options.customIdGroup || {};
|
||||
options.customQueries = options.customQueries || [];
|
||||
|
||||
const { dateFromParts, granularity } = DateService.getGranularityData(options.slice, '$tmpDate');
|
||||
if (!dateFromParts) throw Error('Slice is probably not correct');
|
||||
@@ -103,6 +105,9 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
|
||||
}
|
||||
] as any[];
|
||||
|
||||
for (const customQuery of options.customQueries) {
|
||||
aggregation.splice(customQuery.index, 0, customQuery.query);
|
||||
}
|
||||
|
||||
if (options.customAfterMatch) aggregation.splice(1, 0, options.customAfterMatch);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export class EmailService {
|
||||
static async sendInviteEmailNoAccount(target: string, projectName: string, link: string) {
|
||||
try {
|
||||
const sendSmtpEmail = new SendSmtpEmail();
|
||||
sendSmtpEmail.subject = "⚡ Invite - No account";
|
||||
sendSmtpEmail.subject = "⚡ Invite on a Litlyx project";
|
||||
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
|
||||
sendSmtpEmail.to = [{ "email": target }];
|
||||
|
||||
|
||||
@@ -1,21 +1,102 @@
|
||||
export const PROJECT_INVITE_EMAIL = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Thank You for Upgrading Your Litlyx Plan!</title>
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f9fafb;
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 10px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: black;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: bold;
|
||||
color: #facc15;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
margin-top: 15px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
|
||||
<!-- Email Content -->
|
||||
<body>
|
||||
<div class="container">
|
||||
<img class="icon"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAP0SURBVHgB7Zy9ixNBGMZfPwor8xecf8EFrE3Awi6CfQ7sFay9wvoOzlpBa8XrD7QPxM5COK0Pzj5gaec+ZF7yZt3NzmY/ZmZ9fjDsxsyeME/ej5l5Z0UIIYQQQgghhBBCCCGEEEIIIYQQMmQusnZPSDSsXDsWEgUr075LwtZyU9JnlPsMMeDCHgsJAgSw1mGt5Zkkxi1Jn3HW5u7+LGvfsjZ1nx+561chvTGTjUWomzqWxC0lZTDYOvBT8+9Hsi3KkZBesNaQz66em++uZO3eSMe8lc2gF2EFSzolTgWkuDrYZZzKRpQLIZ2yEL+BVuEY5DtGB/m0oh9c1ZXpH2U8SX2mbgf1uqIvvn9qPr+RCEldkANz/8Oj/zJr79w9xORiZMvYYD3yfAb91HXhGlXWlbqFHLrrZdZ+ez6Dfq/cPcR5KaQ19Jf+Uepjsy5OGFsAg9hkWWQqEc5NUnZZE3N/KfVZugYgThSxJGVBZu6KdHYfQcBrcz8X0gh1N/vED4vGEsQj30ytM1K1kJm5/yLN0OchRnArGYIgS2nGuWxS5pmQvdC984W0g02Bg7qtFC0Ev2LNiM6lHezfCeq2UhVEaRo/iv7OVIg3tuSnLXel2GwrGLclLexk8L3UB4KOZDtOXLuGUqGp+w6rAPvObRqRmiC6XI4B/OTRHwM8cQ2DvCtg28VJ9KcgFSDY+gbzuWt14oEVK9hi4w1JA63XxRXWcb+kHwQ4kX8HFL/+z1n76Z631gAhDl0bu/8D1vFQSCnYbq1a2bWbVdpQIlQ3a+JOYgW2rqpomXwk2xM7Xd9i/VUH2LrdoiI3iLGQ7epEHkPoCLgOW7ZT5KqsGKxK7BAUslkxiny6jRkQJvjS+RBBAM7HgyIxbMX7oMQImfaqC7or65Qzv42K1PSFrNNVC/qoCEhhn0h1kRzxYFXS4KpgFWW/ep8UmOxBXgS4Kpzn2OV+8ucJSYvoJlOdPXE7JxmkdYTcD9Hli4Maz+jmke/iYnKEFESLo33nDmPTt62NqegIKYgubyNm+Kw3HZp7CtIB9vjAxKO/XcFtWmlCSrDHAqrQCWPbW7dREbrIQQ/P+LgtjR++xw6SJAZBdICjPGLWN6EFgRhqJbAAno6NALgrnSTuettC29WKZAf28EzZvsYHiaBuqmtiqVxEGnvm7m1Bg+WXu46EG1G9YVdyYSk285qb73i4pkesKLpBpdWG0Z0J/F/Iv4AM1gKriOq0UxfE+oo/1NkiZmD9Sq0D1SQaO+5k7Y/w1X29AwHy1mI3tVjYEAgIgw2p/FtHT2RgpFLba8HEEdnXA1kLhUKIIJXqhBBCCCGEEEIIIYQQQgghhBBCCOmPv8OsFHqaHzwyAAAAAElFTkSuQmCC"
|
||||
alt="litlyx-logo">
|
||||
|
||||
<p>Invited by <strong>[Project Name]</strong> on <strong>Litlyx</strong></p>
|
||||
|
||||
<p>Best regards,</p>
|
||||
<h2>You're invited to the Litlyx project [Project Name]!</h2>
|
||||
<p>Join now by clicking the button below.</p>
|
||||
<a href="[Link]" class="confirm-btn">
|
||||
Join the Project
|
||||
</a>
|
||||
<p class="footer">See you there,<br> The <span class="brand">Litlyx</span> Team</p>
|
||||
<p class="footer">If you need any help feel free to reach out at help@litlyx.com.</p>
|
||||
<p class="footer">Litlyx Srl<br>Viale Tirreno 187<br>Rome, 00141</p>
|
||||
|
||||
<p>Antonio,</p>
|
||||
<p>CEO @ Litlyx</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function confirmEmail() {
|
||||
alert("Email confirmed! Thank you for joining the waitlist.");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>`
|
||||
@@ -1,21 +1,102 @@
|
||||
export const PROJECT_INVITE_EMAIL_NO_ACCOUNT = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Thank You for Upgrading Your Litlyx Plan!</title>
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f9fafb;
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 10px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: black;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: bold;
|
||||
color: #facc15;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
margin-top: 15px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
|
||||
<!-- Email Content -->
|
||||
<body>
|
||||
<div class="container">
|
||||
<img class="icon"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAP0SURBVHgB7Zy9ixNBGMZfPwor8xecf8EFrE3Awi6CfQ7sFay9wvoOzlpBa8XrD7QPxM5COK0Pzj5gaec+ZF7yZt3NzmY/ZmZ9fjDsxsyeME/ej5l5Z0UIIYQQQgghhBBCCCGEEEIIIYQQMmQusnZPSDSsXDsWEgUr075LwtZyU9JnlPsMMeDCHgsJAgSw1mGt5Zkkxi1Jn3HW5u7+LGvfsjZ1nx+561chvTGTjUWomzqWxC0lZTDYOvBT8+9Hsi3KkZBesNaQz66em++uZO3eSMe8lc2gF2EFSzolTgWkuDrYZZzKRpQLIZ2yEL+BVuEY5DtGB/m0oh9c1ZXpH2U8SX2mbgf1uqIvvn9qPr+RCEldkANz/8Oj/zJr79w9xORiZMvYYD3yfAb91HXhGlXWlbqFHLrrZdZ+ez6Dfq/cPcR5KaQ19Jf+Uepjsy5OGFsAg9hkWWQqEc5NUnZZE3N/KfVZugYgThSxJGVBZu6KdHYfQcBrcz8X0gh1N/vED4vGEsQj30ytM1K1kJm5/yLN0OchRnArGYIgS2nGuWxS5pmQvdC984W0g02Bg7qtFC0Ev2LNiM6lHezfCeq2UhVEaRo/iv7OVIg3tuSnLXel2GwrGLclLexk8L3UB4KOZDtOXLuGUqGp+w6rAPvObRqRmiC6XI4B/OTRHwM8cQ2DvCtg28VJ9KcgFSDY+gbzuWt14oEVK9hi4w1JA63XxRXWcb+kHwQ4kX8HFL/+z1n76Z631gAhDl0bu/8D1vFQSCnYbq1a2bWbVdpQIlQ3a+JOYgW2rqpomXwk2xM7Xd9i/VUH2LrdoiI3iLGQ7epEHkPoCLgOW7ZT5KqsGKxK7BAUslkxiny6jRkQJvjS+RBBAM7HgyIxbMX7oMQImfaqC7or65Qzv42K1PSFrNNVC/qoCEhhn0h1kRzxYFXS4KpgFWW/ep8UmOxBXgS4Kpzn2OV+8ucJSYvoJlOdPXE7JxmkdYTcD9Hli4Maz+jmke/iYnKEFESLo33nDmPTt62NqegIKYgubyNm+Kw3HZp7CtIB9vjAxKO/XcFtWmlCSrDHAqrQCWPbW7dREbrIQQ/P+LgtjR++xw6SJAZBdICjPGLWN6EFgRhqJbAAno6NALgrnSTuettC29WKZAf28EzZvsYHiaBuqmtiqVxEGnvm7m1Bg+WXu46EG1G9YVdyYSk285qb73i4pkesKLpBpdWG0Z0J/F/Iv4AM1gKriOq0UxfE+oo/1NkiZmD9Sq0D1SQaO+5k7Y/w1X29AwHy1mI3tVjYEAgIgw2p/FtHT2RgpFLba8HEEdnXA1kLhUKIIJXqhBBCCCGEEEIIIYQQQgghhBBCCOmPv8OsFHqaHzwyAAAAAElFTkSuQmCC"
|
||||
alt="litlyx-logo">
|
||||
|
||||
<p>Invited by <strong>[Project Name]</strong> on <strong>Litlyx</strong> NO_ACCOUNT</p>
|
||||
|
||||
<p>Best regards,</p>
|
||||
<h2>You're invited to the Litlyx project [Project Name]!</h2>
|
||||
<p>Join now by clicking the button below.</p>
|
||||
<a href="[Link]" class="confirm-btn">
|
||||
Join the Project
|
||||
</a>
|
||||
<p class="footer">See you there,<br> The <span class="brand">Litlyx</span> Team</p>
|
||||
<p class="footer">If you need any help feel free to reach out at help@litlyx.com.</p>
|
||||
<p class="footer">Litlyx Srl<br>Viale Tirreno 187<br>Rome, 00141</p>
|
||||
|
||||
<p>Antonio,</p>
|
||||
<p>CEO @ Litlyx</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function confirmEmail() {
|
||||
alert("Email confirmed! Thank you for joining the waitlist.");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>`
|
||||
Reference in New Issue
Block a user